From 580b6ae73e9905b4dddbdc2aa63f1ddbc06a9241 Mon Sep 17 00:00:00 2001 From: Marco Matthies <71844+marcom@users.noreply.github.com> Date: Mon, 15 Jul 2024 10:10:38 +0200 Subject: [PATCH] Fix bugs from moving ALMaSS crop model into submodule --- src/Persefone.jl | 4 +- src/core/simulation.jl | 6 +- src/crop/crops.jl | 203 +++++++++++++++++++++++++++++++++++++++- src/farm/farm.jl | 2 +- src/nature/macros.jl | 4 +- test/landscape_tests.jl | 4 +- test/runtests.jl | 2 +- 7 files changed, 214 insertions(+), 11 deletions(-) diff --git a/src/Persefone.jl b/src/Persefone.jl index ec269bf..8b55870 100644 --- a/src/Persefone.jl +++ b/src/Persefone.jl @@ -46,7 +46,7 @@ export Weather, FarmEvent, ModelAgent, - FarmPlot, + # FarmPlot, Animal, Farmer, DataOutput, @@ -127,6 +127,8 @@ The supertype of all agents in the model (animal species, farmer types, farmplot """ abstract type ModelAgent end +function stepagent! end + ## include all module files (note that the order matters - if file ## b references something from file a, it must be included later) include("core/input.jl") diff --git a/src/core/simulation.jl b/src/core/simulation.jl index b94b521..f745b24 100644 --- a/src/core/simulation.jl +++ b/src/core/simulation.jl @@ -23,7 +23,7 @@ mutable struct AgricultureModel <: SimulationModel weather::Dict{Date,Weather} crops::Dict{String,ALMaSS.CropType} farmers::Vector{Farmer} - farmplots::Vector{FarmPlot} + farmplots::Vector{ALMaSS.FarmPlot} animals::Vector{Union{Animal,Nothing}} migrants::Vector{Pair{Animal,Date}} events::Vector{FarmEvent} @@ -129,12 +129,12 @@ function initmodel(settings::Dict{String, Any}) weather, crops, Vector{Farmer}(), - Vector{FarmPlot}(), + Vector{ALMaSS.FarmPlot}(), Vector{Union{Animal,Nothing}}(), Vector{Pair{Animal, Date}}(), Vector{FarmEvent}()) saveinputfiles(model) - initfields!(model) + ALMaSS.initfields!(model) initfarms!(model) initnature!(model) model diff --git a/src/crop/crops.jl b/src/crop/crops.jl index 3b4ce0c..e66ce77 100644 --- a/src/crop/crops.jl +++ b/src/crop/crops.jl @@ -6,7 +6,10 @@ #TODO write tests for input functions module ALMaSS -using Dates: Date + +using Persefone: ModelAgent, EventType, SimulationModel, maxtemp, mintemp, fertiliser +import Persefone: stepagent! +using Dates: Date, month, monthday using CSV: CSV """ @@ -124,4 +127,202 @@ function readcropparameters(generalcropfile::String, growthfile::String) croptypes end + +#XXX not sure whether it makes sense to have this as an agent type, +# or perhaps better a grid property? +""" + FarmPlot + +This represents one field, i.e. a collection of pixels with the same management. +This is the spatial unit with which the crop growth model and the farm model work. +""" +mutable struct FarmPlot <: ModelAgent + #TODO add Unitful + const id::Int64 + pixels::Vector{Tuple{Int64, Int64}} + croptype::ALMaSS.CropType + phase::ALMaSS.GrowthPhase + growingdegreedays::Float64 + height::Float64 + LAItotal::Float64 + LAIgreen::Float64 + #biomass::Float64 #XXX I need to figure out how to calculate this + events::Vector{EventType} +end + +""" + initfields!(model) + +Initialise the model with its farm plots. +""" +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 + rawid = model.landscape[x,y].fieldid + (ismissing(rawid)) && continue + if rawid in keys(convertid) + objectid = convertid[rawid] + model.landscape[x,y].fieldid = objectid + push!(model.farmplots[objectid].pixels, (x,y)) + else + #XXX does this phase calculation work out? + month(model.date) < 3 ? phase = ALMaSS.janfirst : phase = ALMaSS.marchfirst + fp = FarmPlot(length(model.farmplots)+1, [(x,y)], + model.crops["natural grass"], phase, + 0.0, 0.0, 0.0, 0.0, Vector{EventType}()) + 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." +end + +""" + stepagent!(farmplot, model) + +Update a farm plot by one day. +""" +function stepagent!(farmplot::FarmPlot, model::SimulationModel) + # update growing degree days + # if no crop-specific base temperature is given, default to 5°C + # (https://www.eea.europa.eu/publications/europes-changing-climate-hazards-1/heat-and-cold/heat-and-cold-2014-mean) + basetemp = farmplot.croptype.mingrowthtemp + ismissing(basetemp) && (basetemp = 5.0) + gdd = (maxtemp(model)+mintemp(model))/2 - basetemp + gdd > 0 && (farmplot.growingdegreedays += gdd) + # update the phase on key dates + monthday(model.date) == (1,1) && (farmplot.phase = ALMaSS.janfirst) + monthday(model.date) == (3,1) && (farmplot.phase = ALMaSS.marchfirst) + # update crop growth + growcrop!(farmplot, model) +end + + +## CROP MANAGEMENT AND GROWTH FUNCTIONS + +""" + sow!(cropname, farmplot, model) + +Sow the specified crop on this farmplot. +""" +function sow!(cropname::String, farmplot::FarmPlot, model::SimulationModel) + createevent!(model, farmplot.pixels, sowing) + farmplot.croptype = model.crops[cropname] + farmplot.phase = ALMaSS.sow + #XXX test if the crop is sowable? +end + +""" + harvest!(farmplot, model) + +Harvest the crop on this farmplot. +""" +function harvest!(farmplot::FarmPlot, model::SimulationModel) + createevent!(model, farmplot.pixels, harvesting) + farmplot.phase in [ALMaSS.harvest1, ALMaSS.harvest2] ? + farmplot.phase = ALMaSS.harvest2 : + farmplot.phase = ALMaSS.harvest1 + # height & LAI will be automatically adjusted by the growth function + #TODO calculate and return yield +end + +#TODO fertilise!() +#TODO spray!() +#TODO till!() + +""" + growcrop!(farmplot, model) + +Apply the relevant crop growth model to update the plants on this farm plot. +Currently only supports the ALMaSS crop growth model by Topping et al. +""" +function growcrop!(farmplot::FarmPlot, model::SimulationModel) + fertiliser in farmplot.events ? + curve = farmplot.croptype.lownutrientgrowth : + curve = farmplot.croptype.highnutrientgrowth + points = curve.GDD[farmplot.phase] + for p in 1:length(points) + if points[p] == 99999 + return # the marker that there is no further growth this phase + elseif points[p] == -1 # the marker to set all variables to specified values + farmplot.height = curve.height[farmplot.phase][p] + farmplot.LAItotal = curve.LAItotal[farmplot.phase][p] + farmplot.LAIgreen = curve.LAIgreen[farmplot.phase][p] + return + else + gdd = farmplot.growingdegreedays + # figure out which is the correct slope value to use for growth + if p == length(points) || gdd < points[p+1] + farmplot.height += curve.height[farmplot.phase][p] + farmplot.LAItotal += curve.LAItotal[farmplot.phase][p] + farmplot.LAIgreen += curve.LAIgreen[farmplot.phase][p] + return + end + #XXX To be precise, we ought to figure out if one or more inflection + # points have been passed between yesterday and today, and calculate the + # growth exactly up to the inflection point with the old slope, and onward + # with the new slope. Not doing so will introduce a small amount of error, + # although I think this is acceptable. + end + end +end + + +## UTILITY FUNCTIONS + +""" + 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) + end + round(sum(sizes)/size(sizes)[1], digits=2) +end + +""" + croptype(model, position) + +Return the crop at this position, or nothing if there is no crop here (utility wrapper). +""" +function croptype(pos::Tuple{Int64,Int64}, model::SimulationModel) + ismissing(model.landscape[pos...].fieldid) ? nothing : + model.farmplots[model.landscape[pos...].fieldid].croptype +end + +""" + cropname(model, position) + +Return the name of the crop at this position, or an empty string if there is no crop here +(utility wrapper). +""" +function cropname(pos::Tuple{Int64,Int64}, model::SimulationModel) + field = model.landscape[pos...].fieldid + ismissing(field) ? "" : model.farmplots[field].croptype.name +end + +""" + cropheight(model, position) + +Return the height of the crop at this position, or nothing if there is no crop here +(utility wrapper). +""" +function cropheight(pos::Tuple{Int64,Int64}, model::SimulationModel) + ismissing(model.landscape[pos...].fieldid) ? nothing : + model.farmplots[model.landscape[pos...].fieldid].height +end + end # module ALMaSS diff --git a/src/farm/farm.jl b/src/farm/farm.jl index ef3d34c..e9e70c3 100644 --- a/src/farm/farm.jl +++ b/src/farm/farm.jl @@ -12,7 +12,7 @@ mutable struct Farmer <: ModelAgent #XXX make this into an abstract type and create subtypes for different # farm submodels? (#69) const id::Int64 - fields::Vector{FarmPlot} + fields::Vector{ALMaSS.FarmPlot} croprotation::Vector{ALMaSS.CropType} #TODO add AES end diff --git a/src/nature/macros.jl b/src/nature/macros.jl index add747c..b9345b8 100644 --- a/src/nature/macros.jl +++ b/src/nature/macros.jl @@ -342,7 +342,7 @@ This is a utility wrapper that can only be used nested within [`@phase`](@ref) or [`@habitat`](@ref). """ macro cropname() - :(cropname($(esc(:pos)), $(esc(:model)))) + :(ALMaSS.cropname($(esc(:pos)), $(esc(:model)))) end """ @@ -353,7 +353,7 @@ This is a utility wrapper that can only be used nested within [`@phase`](@ref) or [`@habitat`](@ref). """ macro cropheight() - :(cropheight($(esc(:pos)), $(esc(:model)))) + :(ALMaSS.cropheight($(esc(:pos)), $(esc(:model)))) end """ diff --git a/test/landscape_tests.jl b/test/landscape_tests.jl index b825765..9fcc4ec 100644 --- a/test/landscape_tests.jl +++ b/test/landscape_tests.jl @@ -6,7 +6,7 @@ @testset "Landscape initialisation" begin model = inittestmodel(false) # these tests are specific to the Jena maps - @test_logs (:info, "Initialised 2092 farm plots.") match_mode=:any Ps.initfields!(model) + @test_logs (:info, "Initialised 2092 farm plots.") match_mode=:any Ps.ALMaSS.initfields!(model) @test size(model.landscape) == (1754, 1602) @test Ps.landcover((100,100), model) == Ps.forest @test Ps.landcover((300,1), model) == Ps.soil @@ -14,7 +14,7 @@ @test Ps.landcover((400,400), model) == Ps.grass @test Ps.landcover((800,800), model) == Ps.agriculture @test Ps.landcover((1100,1100), model) == Ps.builtup - @test Ps.averagefieldsize(model) == 5.37 + @test Ps.ALMaSS.averagefieldsize(model) == 5.37 @test count(f -> ismissing(f.fieldid), model.landscape) == 1685573 @test length(Ps.farmplot((800,800), model).pixels) == 4049 end diff --git a/test/runtests.jl b/test/runtests.jl index 39aef77..b318010 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -55,7 +55,7 @@ function inittestmodel(smallmap=true) weather, crops, Vector{Farmer}(), - Vector{FarmPlot}(), + Vector{Ps.ALMaSS.FarmPlot}(), Vector{Union{Animal,Nothing}}(), Vector{Pair{Animal, Date}}(), Vector{FarmEvent}()) -- GitLab