From 53b9b8619ba7911ac4a02b0e50f072368622c752 Mon Sep 17 00:00:00 2001 From: Daniel Vedder <daniel.vedder@idiv.de> Date: Thu, 8 Aug 2024 16:01:33 +0200 Subject: [PATCH] Started working on farm model --- src/Persefone.jl | 2 ++ src/crop/almass.jl | 6 ++++-- src/crop/cropmodels.jl | 29 +++++++++++++---------------- src/crop/farmplot.jl | 19 +++++++++++++++---- src/crop/simplecrop.jl | 2 +- src/farm/farm.jl | 18 +++++++++++------- 6 files changed, 46 insertions(+), 30 deletions(-) diff --git a/src/Persefone.jl b/src/Persefone.jl index 9660688..bff3e22 100644 --- a/src/Persefone.jl +++ b/src/Persefone.jl @@ -95,6 +95,8 @@ export @walk, @follow, @destroynest, + @sow, + @harvest, #functions simulate, simulate!, diff --git a/src/crop/almass.jl b/src/crop/almass.jl index 5ac4c53..4e5bfe7 100644 --- a/src/crop/almass.jl +++ b/src/crop/almass.jl @@ -58,6 +58,7 @@ implemented in ALMaSS. """ struct CropType name::String + group::String minsowdate::Union{Missing,Date} maxsowdate::Union{Missing,Date} minharvestdate::Union{Missing,Date} @@ -93,6 +94,7 @@ cropname(cs::CropState) = cropname(croptype(cs)) cropheight(cs::CropState) = cs.height cropcover(cs::CropState) = 0.0 # TODO: related to LAItotal, LAIgreen? cropyield(cs::CropState) = 0.0 # TODO: units? needs biomass? +isharvestable(cs::CropState) = false #FIXME how do we do this? """ Base.tryparse(type, str) @@ -147,7 +149,7 @@ Parse a CSV file containing the required parameter values for each crop function readcropparameters(generalcropfile::String, growthfile::String) @debug "Reading crop parameters" cropdata = CSV.File(generalcropfile, missingstring="NA", dateformat="d U", - types=[String,Date,Date,Date,Date,Float64]) + types=[String,Date,Date,Date,Date,Float64,String]) growthdata = CSV.File(growthfile, missingstring="NA", types=[Int,String,String,GrowthPhase,String, Float64,Float64,Float64,Float64]) @@ -159,7 +161,7 @@ function readcropparameters(generalcropfile::String, growthfile::String) filter(x -> x.nutrient_status=="high")) lownuts = buildgrowthcurve(cropgrowthdata |> filter(x -> x.nutrient_status=="low")) - croptypes[crop.name] = CropType(crop.name, crop.minsowdate, crop.maxsowdate, + croptypes[crop.name] = CropType(crop.name, crop.group, crop.minsowdate, crop.maxsowdate, crop.minharvestdate, crop.maxharvestdate, crop.mingrowthtemp, highnuts, lownuts) end diff --git a/src/crop/cropmodels.jl b/src/crop/cropmodels.jl index bbbd925..d947254 100644 --- a/src/crop/cropmodels.jl +++ b/src/crop/cropmodels.jl @@ -31,14 +31,12 @@ end Initialise the farm plots in the simulation model. """ function initfields!(model::SimulationModel) - n = 0 convertid = Dict{Int64,Int64}() width, height = size(model.landscape) for x in 1:width for y in 1:height # for each pixel, we need to extract the field ID given by the map input - # file, and convert it into the internal object ID used by Agents.jl, - # creating a new agent object if necessary + # file, and convert it into an internal object ID for `model.farmplots` rawid = model.landscape[x,y].fieldid (ismissing(rawid)) && continue if rawid in keys(convertid) @@ -46,38 +44,37 @@ function initfields!(model::SimulationModel) model.landscape[x,y].fieldid = objectid push!(model.farmplots[objectid].pixels, (x,y)) else - cropstate = make_cropstate(model, @param(crop.cropmodel)) - fp = FarmPlot( - length(model.farmplots) + 1, - [(x, y)], - cropstate - ) + cropstate = makecropstate(model) + fp = FarmPlot(length(model.farmplots) + 1, [(x, y)], -1, cropstate) push!(model.farmplots, fp) model.landscape[x,y].fieldid = fp.id convertid[rawid] = fp.id - n += 1 end end end - @info "Initialised $n farm plots." + @info "Initialised $(length(model.farmplots)) farm plots." end -# internal utility function -function make_cropstate(model::SimulationModel, cropmodel::AbstractString) - if cropmodel == "almass" +""" + makecropstate(model, cropmodel) + +An internal utility function to initialise one instance of the configured crop growth model. +""" +function makecropstate(model::SimulationModel) + if @param(crop.cropmodel) == "almass" phase = (month(model.date) < 3 ? ALMaSS.janfirst : ALMaSS.marchfirst) cs = ALMaSS.CropState( model.crops["natural grass"], phase, 0.0, 0.0m, 0.0, 0.0, Vector{Management}() ) - elseif cropmodel == "simple" + elseif @param(crop.cropmodel) == "simple" cs = SimpleCrop.CropState( model.crops["natural grass"], 0.0m ) else - error("Unhandled crop model '$cropmodel' in make_cropstate") + Base.error("Unhandled crop model '$(@param(crop.cropmodel))' in makecropstate().") end return cs end diff --git a/src/crop/farmplot.jl b/src/crop/farmplot.jl index ebe7c84..35061b3 100644 --- a/src/crop/farmplot.jl +++ b/src/crop/farmplot.jl @@ -3,10 +3,17 @@ ### This file contains code for the fields that farmers manage. ### +""" + FarmPlot + +A struct representing a single field, on which a crop can be grown. +""" mutable struct FarmPlot{T} <: ModelAgent const id::Int64 pixels::Vector{Tuple{Int64, Int64}} - cropstate :: T + farmer::Int64 + croprotation::Vector{String} + cropstate::T end croptype(f::FarmPlot{T}) where {T} = croptype(f.cropstate) @@ -14,6 +21,7 @@ cropname(f::FarmPlot{T}) where {T} = cropname(croptype(f)) cropheight(f::FarmPlot{T}) where {T} = cropheight(f.cropstate) cropcover(f::FarmPlot{T}) where {T} = cropcover(f.cropstate) cropyield(f::FarmPlot{T}) where {T} = cropyield(f.cropstate) +isharvestable(f::FarmPlot{T}) where {T} = isharvestable(f.cropstate) """ stepagent!(farmplot, model) @@ -47,18 +55,21 @@ end ## UTILITY FUNCTIONS +function isgrassland(farmplot::FarmPlot, model::SimulationModel) + #TODO +end + """ averagefieldsize(model) Calculate the average field size in hectares for the model landscape. """ function averagefieldsize(model::SimulationModel) - conversionfactor = 100 #our pixels are currently 10x10m, so 100 pixels per hectare sizes::Vector{Float64} = [] for fp in model.farmplots - push!(sizes, size(fp.pixels)[1]/conversionfactor) + push!(sizes, length(fp.pixels)*@param(world.mapresolution)^2) end - round(sum(sizes)/size(sizes)[1], digits=2) + return sum(sizes)/length(sizes) |> ha end """ diff --git a/src/crop/simplecrop.jl b/src/crop/simplecrop.jl index 6bfe858..a8aac2d 100644 --- a/src/crop/simplecrop.jl +++ b/src/crop/simplecrop.jl @@ -30,7 +30,7 @@ cropname(cs::CropState) = cropname(croptype(cs)) cropheight(cs::CropState) = cs.height cropcover(cs::CropState) = 0.0 cropyield(cs::CropState) = 0.0 # TODO: units? - +isharvestable(cs::CropState) = true """ stepagent!(farmplot, model) diff --git a/src/farm/farm.jl b/src/farm/farm.jl index 6d7b158..29390ed 100644 --- a/src/farm/farm.jl +++ b/src/farm/farm.jl @@ -1,7 +1,10 @@ ### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe. ### ### This file is responsible for managing the farm module(s). -### +### + +###XXXXXXX In future, I want to expand Persefone into a proper ABM with multiple farm actors. +###XXXXXXX However, at the moment, all fields are controlled centrally -> see farm/farm.jl """ Farmer @@ -9,13 +12,10 @@ This is the agent type for the farm ABM. (Not yet implemented.) """ mutable struct Farmer <: ModelAgent - #XXX make this into an abstract type and create subtypes for different - # farm submodels? (#69) + #XXX make this into an abstract type and create subtypes for different farm submodels? (#69) const id::Int64 - # TODO: hardcoded ALMaSS crop model - fields::Vector{FarmPlot{ALMaSS.CropState}} - croprotation::Vector{ALMaSS.CropType} - #TODO add AES + fields::Vector{Int64} # IDs of the farmplots this farmer owns + totalincome::Float64 end """ @@ -25,6 +25,10 @@ Update a farmer by one day. """ function stepagent!(farmer::Farmer, model::SimulationModel) #TODO + # - check each field, whether it can be harvested + # - if so, harvest it and set its crop to "no growth" + # - [later: calculate income based on yield and annual price] + # - if a field has been harvested, check if the next crop can be sown end """ -- GitLab