From c912735d580a8b7ec4991a9ecb53b7956cee24b7 Mon Sep 17 00:00:00 2001 From: Marco Matthies <71844+marcom@users.noreply.github.com> Date: Mon, 10 Mar 2025 08:07:23 +0100 Subject: [PATCH] Allow for using multiple crop models at the same time in one simulation --- data/crops/aquacrop/crop_data.csv | 4 - src/core/input.jl | 20 +++-- src/core/simulation.jl | 4 +- src/crop/almass.jl | 2 +- src/crop/aquacrop.jl | 129 ++++++++++++++++----------- src/crop/cropmodels.jl | 45 ++++++---- src/crop/farmplot.jl | 7 +- src/crop/simplecrop.jl | 4 +- src/parameters.toml | 4 +- test/paramscan.toml | 2 +- test/test_parameters.toml | 2 +- test/test_parameters_almass.toml | 2 +- test/test_parameters_aquacrop.toml | 4 +- test/test_parameters_simplecrop.toml | 2 +- 14 files changed, 135 insertions(+), 96 deletions(-) diff --git a/data/crops/aquacrop/crop_data.csv b/data/crops/aquacrop/crop_data.csv index c9a871f..89ed915 100644 --- a/data/crops/aquacrop/crop_data.csv +++ b/data/crops/aquacrop/crop_data.csv @@ -1,8 +1,4 @@ persefone_name,aquacrop_name,crop_group,min_sow_date,max_sow_date -natural grass,alfalfaGDD,semi-natural,NA,NA -permanent grassland (grazed),alfalfaGDD,grass,NA,NA -permanent set-aside,alfalfaGDD,grass,NA,NA -no growth,alfalfaGDD,semi-natural,NA,NA winter rape,rapeseed,grain,20 August,25 August winter wheat,wheatGDD,grain,15 October,31 October maize,maize,grain,15 April,30 April diff --git a/src/core/input.jl b/src/core/input.jl index b149699..9437f0e 100644 --- a/src/core/input.jl +++ b/src/core/input.jl @@ -104,21 +104,23 @@ function preprocessparameters(settings::Dict{String,Any}, defaultoutdir::String) Base.error("Couldn't find map directory $(settings["world.mapdirectory"]).") end end - if !isdir(settings["crop.cropdirectory"]) - if isdir(abspath(pkgdir(@__MODULE__), settings["crop.cropdirectory"])) - settings["crop.cropdirectory"] = abspath(pkgdir(@__MODULE__), settings["crop.cropdirectory"]) - @debug "Using package directory to load crop data: $(settings["crop.cropdirectory"])." - else - Base.error("Couldn't find crop directory $(settings["crop.cropdirectory"]).") + # check cropmodel settings + cropmodels = string.(split(settings["crop.cropmodel"], ",")) + for cropmodel in cropmodels + if !(cropmodel in AVAILABLE_CROPMODELS) + error("cropmodel = \"$cropmodel\", but has to be one of: $AVAILABLE_CROPMODELS") + end + end + cropdirs = string.(split(settings["crop.cropdirectory"], ",")) + for cropdir in cropdirs + if !isdir(cropdir) + error("Can't access crop model data dir $cropdir") end end # sanity checks if settings["core.startdate"] > settings["core.enddate"] Base.error("Enddate is earlier than startdate.") end - if !(settings["crop.cropmodel"] in AVAILABLE_CROPMODELS) - Base.error("crop.cropmodel = \"$(settings["crop.cropmodel"])\", but has to be one of: $AVAILABLE_CROPMODELS") - end #FIXME enable parallelisation # if !isempty(settings["internal.scanparams"]) && (nprocs() < 2) # @warn "To parallelise multiple simulation runs, use `julia -p <n>`." diff --git a/src/core/simulation.jl b/src/core/simulation.jl index 667f766..2aa11ce 100644 --- a/src/core/simulation.jl +++ b/src/core/simulation.jl @@ -140,8 +140,8 @@ function initmodel(settings::Dict{String, Any}) settings["world.weatherfile"]), settings["core.startdate"], settings["core.enddate"]) - crops = initcropmodel(settings["crop.cropmodel"], - settings["crop.cropdirectory"]) + crops = initcropmodels(settings["crop.cropmodel"], + settings["crop.cropdirectory"]) model = AgricultureModel(; settings, rng = StableRNG(settings["core.seed"]), diff --git a/src/crop/almass.jl b/src/crop/almass.jl index ecd155c..de804b3 100644 --- a/src/crop/almass.jl +++ b/src/crop/almass.jl @@ -417,7 +417,7 @@ function readcropparameters(cropdirectory::String) growthdata = CSV.File(joinpath(cropdirectory, GROWTHFILE), missingstring="NA", types=[Int,String,String,GrowthPhase,String, Float64,Float64,Float64,Float64]) - croptypes = Dict{String,CropType}() + croptypes = Dict{String,AbstractCropType}() for crop in cropdata cropgrowthdata = growthdata |> filter(x -> !ismissing(x.crop_name) && x.crop_name == crop.name) diff --git a/src/crop/aquacrop.jl b/src/crop/aquacrop.jl index f95e516..d9c9f1c 100644 --- a/src/crop/aquacrop.jl +++ b/src/crop/aquacrop.jl @@ -91,7 +91,7 @@ needed for Persefone (cropgroup, minsowdate, maxsowdate). """ function readcropparameters(cropdirectory::String) @debug "Reading extra crop parameters for AquaCrop crop model" - croptypes = Dict{String,CropType}() + croptypes = Dict{String, AbstractCropType}() df = CSV.read(joinpath(cropdirectory, CROPFILE), DataFrame; missingstring="NA", types=[String, String, String, AnnualDate, AnnualDate]) for row in eachrow(df) @@ -105,61 +105,68 @@ function readcropparameters(cropdirectory::String) return croptypes end +function make_aquacrop_cropfield(croptype::CropType, + model::SimulationModel, + soiltype::SoilType) + # TODO: get this mapping for soil types from a CSV file? + soiltype_to_aquacrop = Dict(soiltype => replace(string(soiltype), "_" => " ") + for soiltype in instances(SoilType)) + aquacrop_soiltype = soiltype_to_aquacrop[soiltype] + + start_date = model.date # TODO: maybe `today(model)` + end_date = model.weather.lastdate # TODO: maybe `lastdate(model)` + start_daynum = daynumber(model.weather, start_date) + end_daynum = daynumber(model.weather, end_date) + dayrange = start_daynum:end_daynum + input_Tmin = @view model.weather.mintemp[dayrange] + input_Tmax = @view model.weather.maxtemp[dayrange] + input_ETo = @view model.weather.evapotranspiration[dayrange] + input_Rain = @view model.weather.precipitation[dayrange] + + # Generate the keyword object for the AquaCrop simulation + kwargs = ( + ## Necessary keywords + # runtype + runtype = AquaCrop.NoFileRun(), + # project input + Simulation_DayNr1 = start_date, + Simulation_DayNrN = end_date, + Crop_Day1 = start_date, # TODO: originally start_date + Week(1) + Crop_DayN = end_date, + # soil + soil_type = aquacrop_soiltype, + # crop + crop_type = croptype.aquacrop_name, + # Climate + InitialClimDate = start_date, + ## Optional keyworkds + # Climate + Tmin = input_Tmin, + Tmax = input_Tmax, + ETo = input_ETo, + Rain = input_Rain, + # change soil properties + soil_layers = Dict("Thickness" => 5.0) + ) + + aquacrop_cropfield, all_ok = AquaCrop.start_cropfield(; kwargs...) + if ! all_ok.logi + @error "Failed calling AquaCrop.start_cropfield()\nAquaCrop error: $(all_ok.msg)" + end + + return aquacrop_cropfield +end + mutable struct CropState <: AbstractCropState croptype::CropType height::Tlength # TODO: remove height field, supply from cropstate soiltype::SoilType cropstate::AquaCrop.AquaCropField - function CropState(crop_type::CropType, soiltype::SoilType, + function CropState(croptype::CropType, soiltype::SoilType, model::SimulationModel, height::Tlength=0.0cm) - # TODO: get this mapping for soil types from a CSV file? - soiltype_to_aquacrop = Dict(soiltype => replace(string(soiltype), "_" => " ") - for soiltype in instances(SoilType)) - aquacrop_soiltype = soiltype_to_aquacrop[soiltype] - - start_date = model.date # TODO: maybe `today(model)` - end_date = model.weather.lastdate # TODO: maybe `lastdate(model)` - start_daynum = daynumber(model.weather, start_date) - end_daynum = daynumber(model.weather, end_date) - dayrange = start_daynum:end_daynum - input_Tmin = @view model.weather.mintemp[dayrange] - input_Tmax = @view model.weather.maxtemp[dayrange] - input_ETo = @view model.weather.evapotranspiration[dayrange] - input_Rain = @view model.weather.precipitation[dayrange] - - # Generate the keyword object for the AquaCrop simulation - kwargs = ( - ## Necessary keywords - # runtype - runtype = AquaCrop.NoFileRun(), - # project input - Simulation_DayNr1 = start_date, - Simulation_DayNrN = end_date, - Crop_Day1 = start_date, # TODO: originally start_date + Week(1) - Crop_DayN = end_date, - # soil - soil_type = aquacrop_soiltype, - # crop - crop_type = crop_type.aquacrop_name, - # Climate - InitialClimDate = start_date, - ## Optional keyworkds - # Climate - Tmin = input_Tmin, - Tmax = input_Tmax, - ETo = input_ETo, - Rain = input_Rain, - # change soil properties - soil_layers = Dict("Thickness" => 5.0) - ) - - aquacrop_cropfield, all_ok = AquaCrop.start_cropfield(; kwargs...) - if ! all_ok.logi - @error "Failed calling AquaCrop.start_cropfield()\nAquaCrop error: $(all_ok.msg)" - end - - return new(crop_type, height, soiltype, aquacrop_cropfield) + aquacrop_cropfield = make_aquacrop_cropfield(croptype, model, soiltype) + return new(croptype, height, soiltype, aquacrop_cropfield) end end @@ -168,9 +175,12 @@ cropname(cs::CropState) = cropname(croptype(cs)) cropheight(cs::CropState) = cs.height # TODO: calculate from AquaCrop state info cropcover(cs::CropState) = AquaCrop.canopycover(cs.cropstate) cropyield(cs::CropState) = AquaCrop.dryyield(cs.cropstate) # TODO: there is also freshyield -isharvestable(cs::CropState) = true # TODO: implement this correctly -makecropstate(crop_type::CropType, model::SimulationModel, soiltype::SoilType) = - CropState(crop_type, soiltype, model) +function isharvestable(cs::CropState) + stage = get_aquacrop_stage(cs) + return !ismissing(stage) && stage == 0 +end +makecropstate(croptype::CropType, model::SimulationModel, soiltype::SoilType) = + CropState(croptype, soiltype, model) function setsoiltype!(cs::CropState, soiltype::SoilType) # TODO: this does not affect the actual soil type of the state of # the aquacrop simulation @@ -178,6 +188,11 @@ function setsoiltype!(cs::CropState, soiltype::SoilType) return nothing end +function get_aquacrop_stage(cs::CropState) + stages = cs.cropstate.dayout.Stage + length(stages) > 0 ? last(stages) : missing +end + """ stepagent!(cropstate, model) @@ -196,9 +211,15 @@ function sow!(cs::CropState, model::SimulationModel, cropname::String) if cropname ∉ keys(model.crops) @error "cropname \"$cropname\" not found" end - cs.croptype = model.crops[cropname] + new_croptype = model.crops[cropname] + if typeof(new_croptype) !== CropType + error("Cannot change crop model of AquaCrop crop state.") + end + + cs.croptype = new_croptype cs.height = 0.0cm - # TODO: other things needed for AquaCrop? + # cs.soiltype stays the way it is + cs.cropstate = make_aquacrop_cropfield(cs.croptype, model, soiltype) end """ diff --git a/src/crop/cropmodels.jl b/src/crop/cropmodels.jl index 153eb39..5aafe24 100644 --- a/src/crop/cropmodels.jl +++ b/src/crop/cropmodels.jl @@ -6,24 +6,39 @@ using StatsBase: mode """ - initcropmodel(cropmodel, cropdirectory) + initcropmodels(cropmodels, cropdirectory) -Initialise the crop model. Returns the crop types available in the -simulation, as well as the types `Tcroptype` and `Tcropstate`. +Initialise the crop models given as a comma-delimited string +(e.g. "almass,aquacrop"). The crop model parameters are read from the +`cropdirectories`, also a comma-delimited string. Returns the crop +types available in the simulation. """ -function initcropmodel(cropmodel::AbstractString, cropdirectory::AbstractString) - if cropmodel == "almass" - crops = ALMaSS.readcropparameters(cropdirectory) - elseif cropmodel == "simple" - crops_almass = ALMaSS.readcropparameters(cropdirectory) - crops = Dict(name => SimpleCrop.CropType(ct.name, ct.group, ct.minsowdate, ct.maxsowdate) - for (name, ct) in crops_almass) - elseif cropmodel == "aquacrop" - crops = AquaCropWrapper.readcropparameters(cropdirectory) - else - error("initcropmodel: no implementation for crop model '$cropmodel'") +function initcropmodels(cropmodels::AbstractString, + cropdirectories::AbstractString) + models = string.(split(cropmodels, ",")) + dirs = string.(split(cropdirectories, ",")) + if length(models) != length(dirs) + error("Different number of cropmodels and cropdirectories.\n" * + "cropmodels = $cropmodels\ncropdirectories = $cropdirectories") end - return crops + + allcrops = Dict{String, AbstractCropType}() + for (i, cropmodel) in enumerate(models) + cropdirectory = dirs[i] + if cropmodel == "almass" + crops = ALMaSS.readcropparameters(cropdirectory) + elseif cropmodel == "simple" + crops_almass = ALMaSS.readcropparameters(cropdirectory) + crops = Dict(name => SimpleCrop.CropType(ct.name, ct.group, ct.minsowdate, ct.maxsowdate) + for (name, ct) in crops_almass) + elseif cropmodel == "aquacrop" + crops = AquaCropWrapper.readcropparameters(cropdirectory) + else + error("initcropmodel: no implementation for crop model '$cropmodel'") + end + merge!(allcrops, crops) + end + return allcrops end """ diff --git a/src/crop/farmplot.jl b/src/crop/farmplot.jl index 40a3917..b39e5bd 100644 --- a/src/crop/farmplot.jl +++ b/src/crop/farmplot.jl @@ -44,8 +44,13 @@ end Sow the specified crop on the farmplot. """ function sow!(farmplot::FarmPlot, model::SimulationModel, cropname::String) + new_croptype = model.crops[cropname] createevent!(model, farmplot.pixels, sowing) - sow!(farmplot.cropstate, model, cropname) + if typeof(new_croptype) === typeof(croptype(farmplot)) + sow!(farmplot.cropstate, model, cropname) + else + farmplot.cropstate = makecropstate(new_croptype, model, farmplot.soiltype) + end @debug "Farmer $(farmplot.farmer) sowed $(cropname) on farmplot $(farmplot.id)." end diff --git a/src/crop/simplecrop.jl b/src/crop/simplecrop.jl index 7f74ca4..2fd9c29 100644 --- a/src/crop/simplecrop.jl +++ b/src/crop/simplecrop.jl @@ -44,8 +44,8 @@ cropheight(cs::CropState) = cs.height cropcover(cs::CropState) = 0.0 cropyield(cs::CropState) = 0.0 # TODO: units? isharvestable(cs::CropState) = true -makecropstate(crop_type::CropType, model::SimulationModel, soiltype::SoilType) = - CropState(crop_type, 0.0cm) +makecropstate(croptype::CropType, model::SimulationModel, soiltype::SoilType) = + CropState(croptype, 0.0cm) """ diff --git a/src/parameters.toml b/src/parameters.toml index c3d8aff..1b7d420 100644 --- a/src/parameters.toml +++ b/src/parameters.toml @@ -46,5 +46,5 @@ indoutfreq = "end" # output frequency individual-level data, daily/monthly/yearl insectmodel = ["season", "habitat", "pesticides", "weather"] # factors affecting insect growth [crop] -cropmodel = "aquacrop" # crop growth model to use, "almass", "aquacrop", or "simple" -cropdirectory = "data/crops/aquacrop/" # the directory storing all data files for the selected crop model +cropmodel = "almass,aquacrop" # crop growth model to use, "almass", "aquacrop", or "simple" +cropdirectory = "data/crops/almass/,data/crops/aquacrop/" # the directory storing all data files for the selected crop model diff --git a/test/paramscan.toml b/test/paramscan.toml index 565e685..fecac92 100644 --- a/test/paramscan.toml +++ b/test/paramscan.toml @@ -29,4 +29,4 @@ indoutfreq = "end" # output frequency individual-level data, daily/monthly/yearl [crop] cropmodel = "simple" # crop growth model to use, "simple" or "almass" - +cropdirectory = "../data/crops/almass/" diff --git a/test/test_parameters.toml b/test/test_parameters.toml index fa539d9..a9d6b9b 100644 --- a/test/test_parameters.toml +++ b/test/test_parameters.toml @@ -42,4 +42,4 @@ insectmodel = ["season", "habitat", "pesticides"] # which factors affect insect [crop] cropmodel = "almass" # crop growth model to use, "almass", "aquacrop", or "simple" -cropdirectory = "data/crops/almass/" # the directory storing all data files for the selected crop model +cropdirectory = "../data/crops/almass/" # the directory storing all data files for the selected crop model diff --git a/test/test_parameters_almass.toml b/test/test_parameters_almass.toml index 6d8fa7e..c497679 100644 --- a/test/test_parameters_almass.toml +++ b/test/test_parameters_almass.toml @@ -42,4 +42,4 @@ insectmodel = ["season", "habitat", "pesticides"] # which factors affect insect [crop] cropmodel = "almass" # crop growth model to use, "almass", "aquacrop", or "simple" -cropdirectory = "data/crops/almass/" # the directory storing all data files for the selected crop model +cropdirectory = "../data/crops/almass/" # the directory storing all data files for the selected crop model diff --git a/test/test_parameters_aquacrop.toml b/test/test_parameters_aquacrop.toml index 9f29fdc..13d5935 100644 --- a/test/test_parameters_aquacrop.toml +++ b/test/test_parameters_aquacrop.toml @@ -41,5 +41,5 @@ indoutfreq = "daily" # output frequency individual-level data, daily/monthly/yea insectmodel = ["season", "habitat", "pesticides"] # which factors affect insect growth ("weather" is not yet implemented) [crop] -cropmodel = "aquacrop" # crop growth model to use, "almass", "aquacrop", or "simple" -cropdirectory = "data/crops/aquacrop/" +cropmodel = "almass,aquacrop" # crop growth model to use, "almass", "aquacrop", or "simple" +cropdirectory = "../data/crops/almass/,../data/crops/aquacrop/" diff --git a/test/test_parameters_simplecrop.toml b/test/test_parameters_simplecrop.toml index 6540f38..f2fd15c 100644 --- a/test/test_parameters_simplecrop.toml +++ b/test/test_parameters_simplecrop.toml @@ -42,4 +42,4 @@ insectmodel = ["season", "habitat", "pesticides"] # which factors affect insect [crop] cropmodel = "simple" # crop growth model to use, "almass", "aquacrop", or "simple" -cropdirectory = "data/crops/almass/" # the directory storing all data files for the selected crop model +cropdirectory = "../data/crops/almass/" # the directory storing all data files for the selected crop model -- GitLab