From 0a196ddfa161226d37c207d4a0e6e3c516ce10be Mon Sep 17 00:00:00 2001 From: Marco Matthies <71844+marcom@users.noreply.github.com> Date: Thu, 31 Oct 2024 16:25:00 +0100 Subject: [PATCH] AquaCrop: initial implementation --- Project.toml | 3 ++ src/Persefone.jl | 1 + src/core/input.jl | 2 +- src/crop/aquacrop.jl | 70 ++++++++++++++++++++++++++++++ src/crop/cropmodels.jl | 9 ++++ test/crop_tests.jl | 21 ++++++++- test/test_parameters_aquacrop.toml | 46 ++++++++++++++++++++ 7 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 src/crop/aquacrop.jl create mode 100644 test/test_parameters_aquacrop.toml diff --git a/Project.toml b/Project.toml index 5189975..bb09465 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,7 @@ authors = ["Daniel Vedder <daniel.vedder@idiv.de>"] version = "0.6.1" [deps] +AquaCrop = "8f16cebd-c0b4-44a3-857f-c101f083256c" ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63" CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" @@ -14,6 +15,7 @@ Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" GeoArrays = "2fb1d81b-e6a0-5fc5-82e6-8e06903437ab" +ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" LoggingExtras = "e6f89c97-d47a-5376-807f-9c37f3926c36" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" @@ -28,4 +30,5 @@ TiffImages = "731e570b-9d59-4bfa-96dc-6df516fadf69" Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" [compat] +ImageMagick = "1.2.1" julia = ">= 1.10" diff --git a/src/Persefone.jl b/src/Persefone.jl index 2b51908..6a824d1 100644 --- a/src/Persefone.jl +++ b/src/Persefone.jl @@ -147,6 +147,7 @@ include("crop/farmplot.jl") include("crop/cropmodels.jl") include("crop/almass.jl") include("crop/simplecrop.jl") +include("crop/aquacrop.jl") include("farm/farm.jl") include("farm/farmdata.jl") diff --git a/src/core/input.jl b/src/core/input.jl index db2bdef..b149699 100644 --- a/src/core/input.jl +++ b/src/core/input.jl @@ -20,7 +20,7 @@ const PARAMFILE = joinpath(pkgdir(Persefone), "src/parameters.toml") """ The crop models that can be used in the simulation. """ -const AVAILABLE_CROPMODELS = ["almass", "simple"] +const AVAILABLE_CROPMODELS = ["almass", "simple", "aquacrop"] """ @param(domainparam) diff --git a/src/crop/aquacrop.jl b/src/crop/aquacrop.jl new file mode 100644 index 0000000..374827c --- /dev/null +++ b/src/crop/aquacrop.jl @@ -0,0 +1,70 @@ +import AquaCrop + +# We can't use Length{Float64} as that is a Union type +const Tlength = typeof(1.0cm) + +struct AquaCropType + name::String + group::String + minsowdate::Union{Missing,AnnualDate} + maxsowdate::Union{Missing,AnnualDate} +end + +cropname(ct::AquaCropType) = ct.name + +mutable struct AquaCropState + croptype::AquaCropType + height::Length{Float64} # TODO: remove height field, supply from cropstate + cropstate::AquaCrop.AquaCropField + + function AquaCropState(croptype::AquaCropType, height::Length{Float64}=0.0cm) + runtype = AquaCrop.TomlFileRun() + parentdir = AquaCrop.test_toml_dir # TODO: hardcoded croptype + ac_cropfield, all_ok = AquaCrop.start_cropfield(; parentdir, runtype) + if ! all_ok.logi + error("AquaCrop.start_cropfield() failed, status = $all_ok") + end + AquaCrop.setup_cropfield!(ac_cropfield, all_ok; runtype=runtype) + if ! all_ok.logi + error("AquaCrop.setup_cropfield!() failed, status = $all_ok") + end + return new(croptype, height, ac_cropfield) + end +end + +croptype(cs::AquaCropState) = cs.croptype +cropname(cs::AquaCropState) = cropname(croptype(cs)) +cropheight(cs::AquaCropState) = cs.height # TODO: use AquaCrop state info +cropcover(cs::AquaCropState) = AquaCrop.canopycover(cs.cropstate) +cropyield(cs::AquaCropState) = AquaCrop.dryyield(cs.cropstate) # TODO: there is also freshyield +isharvestable(cs::AquaCropState) = true # TODO: implement this correctly + +""" + stepagent!(cropstate, model) + +Update a crop state by one day. +""" +function stepagent!(cs::AquaCropState, model::SimulationModel) + AquaCrop.dailyupdate!(cs.cropstate) +end + +""" + sow!(cropstate, model, cropname) + +Change the cropstate to sow the specified crop. +""" +function sow!(cs::AquaCropState, model::SimulationModel, cropname::String) + cs.croptype = model.crops[cropname] + cs.height = 0.0cm + # TODO: other things needed for AquaCrop? +end + +""" + harvest!(cropstate, model) + +Harvest the crop of this cropstate. +""" +function harvest!(cs::AquaCropState, model::SimulationModel) + # TODO: implement this + return 0.0u"g/m^2" +end diff --git a/src/crop/cropmodels.jl b/src/crop/cropmodels.jl index 92722a1..dac5c8b 100644 --- a/src/crop/cropmodels.jl +++ b/src/crop/cropmodels.jl @@ -20,6 +20,13 @@ function initcropmodel(cropmodel::AbstractString, cropdirectory::AbstractString) 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" + Tcroptype = AquaCropType + Tcropstate = AquaCropState + # TODO: temporarily using ALMaSS crop types + crops_almass = ALMaSS.readcropparameters(cropdirectory) + crops = Dict(name => AquaCropType(ct.name, ct.group, ct.minsowdate, ct.maxsowdate) + for (name, ct) in crops_almass) else error("initcropmodel: no implementation for crop model '$cropmodel'") end @@ -73,6 +80,8 @@ function makecropstate(model::SimulationModel) model.crops["natural grass"], 0.0m ) + elseif @param(crop.cropmodel) == "aquacrop" + cs = AquaCropState(model.crops["natural grass"], 0.0cm) else Base.error("Unhandled crop model '$(@param(crop.cropmodel))' in makecropstate().") end diff --git a/test/crop_tests.jl b/test/crop_tests.jl index b412030..e97ea84 100644 --- a/test/crop_tests.jl +++ b/test/crop_tests.jl @@ -3,10 +3,13 @@ ### These are the tests for the crop growth models. ### +import Unitful + const TESTPARAM_ALMASS = joinpath(pkgdir(Persefone), "test", "test_parameters_almass.toml") const TESTPARAM_SIMPLECROP = joinpath(pkgdir(Persefone), "test", "test_parameters_simplecrop.toml") +const TESTPARAM_AQUACROP = joinpath(pkgdir(Persefone), "test", "test_parameters_aquacrop.toml") -@testset for configfile in (TESTPARAM_ALMASS, TESTPARAM_SIMPLECROP) +@testset for configfile in (TESTPARAM_ALMASS, TESTPARAM_SIMPLECROP, TESTPARAM_AQUACROP) @testset "Model initialisation" begin model = initialise(; configfile) @test model isa AgricultureModel @@ -15,6 +18,7 @@ const TESTPARAM_SIMPLECROP = joinpath(pkgdir(Persefone), "test", "test_parameter model = initialise(; configfile) stepsimulation!(model) @test model isa AgricultureModel + # TODO: test stepagent!(), sow!(), harvest!() end end @@ -51,3 +55,18 @@ end @test cropcover(fp) isa Float64 @test cropyield(fp) isa Float64 end + +@testset "Submodule AquaCrop" begin + ct = Ps.AquaCropType("olive tree", "no_crop_group", AnnualDate(4, 1), AnnualDate(5, 1)) + id = 0 + pixels = [(0, 0)] + farmer = 0 + fp = FarmPlot(id, pixels, farmer, Ps.AquaCropState(ct)) + @test fp isa FarmPlot + @test fp isa FarmPlot{Ps.AquaCropState} + @test croptype(fp) isa Ps.AquaCropType + @test cropname(fp) isa String + @test cropheight(fp) isa Length{Float64} + @test cropcover(fp) isa Float64 + @test Unitful.dimension(cropyield(fp)) == Unitful.ðŒ * Unitful.ð‹^-2 +end diff --git a/test/test_parameters_aquacrop.toml b/test/test_parameters_aquacrop.toml new file mode 100644 index 0000000..b117ed0 --- /dev/null +++ b/test/test_parameters_aquacrop.toml @@ -0,0 +1,46 @@ +### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe. +### +### This configuration file is used for the test suite. +### + +#XXX remember that changes here may break tests! + +[core] +configfile = "test_parameters_aquacrop.toml" # location of the configuration file +outdir = "results_testsuite" # location and name of the output folder +logoutput = "both" +overwrite = true # overwrite the output directory? (true/false/"ask") +csvoutput = true # save collected data in CSV files +visualise = true # generate result graphs +storedata = true # keep collected data in memory +loglevel = "warn" # verbosity level: "debug", "info", "warn" +processors = 6 # number of processors to use on parallel runs +seed = 1 # seed value for the RNG (0 -> random value) +# dates to start and end the simulation +startdate = 2022-02-01 +enddate = 2022-03-31 + +[world] +mapdirectory = "." # the directory in which all geographic data are stored +mapresolution = 10 # map resolution in meters +landcovermap = "landcover_jena.tif" # location of the landcover map +farmfieldsmap = "fields_jena.tif" # location of the field geometry map +weatherfile = "weather_jena.csv" # location of the weather data file + +[farm] +farmmodel = "BasicFarmer" # which version of the farm model to use +setaside = 0.04 # proportion of farm area set aside as fallow +fieldoutfreq = "daily" # output frequency for crop/field data, daily/monthly/yearly/end/never + +[nature] +targetspecies = ["Wolpertinger", "Wyvern"] # list of target species to simulate - example species +#targetspecies = ["Skylark"] # list of target species to simulate +popoutfreq = "daily" # output frequency population-level data, daily/monthly/yearly/end/never +indoutfreq = "daily" # output frequency individual-level data, daily/monthly/yearly/end/never +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" +# TODO: these two files below are ALMaSS parameters +cropfile = "crop_data_general.csv" # file with general crop parameters +growthfile = "almass_crop_growth_curves.csv" # file with crop growth parameters -- GitLab