From 262d27bd9c5a57838573d9e70d8e13bf5f88ec55 Mon Sep 17 00:00:00 2001 From: Marco Matthies <71844+marcom@users.noreply.github.com> Date: Thu, 17 Oct 2024 01:07:53 +0200 Subject: [PATCH] Merge port of ALMaSS vegetation code The code seems to work for crop plants but currently doesn't yet work for non-crop plants such as "permanent set-aside", which grow to enourmous height and do not get reset at the end of the year. I think this is due to different treatment of the growth phase transitions for these plants. --- src/crop/almass.jl | 709 +++++++++++++++++++++++++++++++++++------ src/crop/cropmodels.jl | 5 +- test/crop_tests.jl | 3 +- test/nature_tests.jl | 4 +- 4 files changed, 616 insertions(+), 105 deletions(-) diff --git a/src/crop/almass.jl b/src/crop/almass.jl index c400682..1903cbb 100644 --- a/src/crop/almass.jl +++ b/src/crop/almass.jl @@ -9,6 +9,30 @@ #TODO write tests to compare our output to the output of the original ALMaSS algorithm +# Some functions translated from ALMaSS for use in Persefone.jl +# +# ALMaSS is licensed as follows: +# +# Copyright (c) 2011, Christopher John Topping, Aarhus University +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are permitted provided +# that the following conditions are met: +# +# Redistributions of source code must retain the above copyright notice, this list of conditions and the +# following disclaimer. +# Redistributions in binary form must reproduce the above copyright notice, this list of conditions and +# the following disclaimer in the documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + module ALMaSS ## The two input files required by ALMaSS. Must be present in the crop directory. @@ -16,14 +40,17 @@ const CROPFILE = "crop_data_general.csv" const GROWTHFILE = "crop_growth_curves.csv" using Persefone: + @rand, + @u_str, AnnualDate, Management, - Length, cm, SimulationModel, fertiliser, maxtemp, - mintemp + mintemp, + year, + dayofyear import Persefone: stepagent!, @@ -39,6 +66,10 @@ import Persefone: using Dates: Date, month, monthday using CSV: CSV +using Unitful: unit + +# We can't use Length{Float64} as that is a Union type +const Tlength = typeof(1.0cm) """ GrowthPhase @@ -58,9 +89,9 @@ struct CropCurveParams curveID::Int highnutrients::Bool GDD::Dict{GrowthPhase,Vector{Float64}} - LAItotal::Dict{GrowthPhase,Vector{Float64}} - LAIgreen::Dict{GrowthPhase,Vector{Float64}} - height::Dict{GrowthPhase,Vector{Length{Float64}}} + LAItotal::Dict{GrowthPhase, Vector{Float64}} + LAIgreen::Dict{GrowthPhase, Vector{Float64}} + height::Dict{GrowthPhase, Vector{typeof(1.0cm)}} end """ @@ -92,29 +123,64 @@ cropname(ct::CropType) = ct.name The state data for an ALMaSS vegetation point calculation. Usually part of a `FarmPlot`. """ -mutable struct CropState - #TODO add Unitful +@kwdef mutable struct CropState + # Class in original ALMaSS code: + # `VegElement` from `Landscape/Elements.h`, line 601 croptype::CropType + events::Vector{Management} = Management[] + mature::Bool = false #TODO how do we determine this? + + # biomass::Float64 #XXX I need to figure out how to calculate this + # height::Length{Float64} + # growingdegreedays::Float64 + phase::GrowthPhase - growingdegreedays::Float64 - height::Length{Float64} - LAItotal::Float64 - LAIgreen::Float64 - #biomass::Float64 #XXX I need to figure out how to calculate this - mature::Bool #TODO how do we determine this? - events::Vector{Management} #FIXME does this do anything? - force_growth::Bool # true means the crop is not sowed but grows by itself, e.g. grass + vegddegs::Float64 = 0 # Vegetation growing degree days since sowing + ddegs::Float64 = 0 # Growing degree days in current phase + yddegs::Float64 = 0 # Growing degree days in current phase for previous day + LAgreen::Float64 = 0 + LAtotal::Float64 = 0 + oldLAtotal::Float64 = 0 + veg_height::Tlength = 0.0cm + newgrowth::Float64 = 0 + veg_cover::Float64 = 0 + veg_biomass::Float64 = 0 + green_biomass::Float64 = 0 + dead_biomass::Float64 = 0 + + growth_scaler::Float64 = 1.0 # TODO: where is this set? + forced_phase_shift::Bool = false # TODO: what does this do? + + # TODO: meaning of force_* fields ? + force_growth::Bool = false + force_LAgreen::Float64 = 0 + force_LAtotal::Float64 = 0 + force_veg_height::Tlength = 0.0cm end croptype(cs::CropState) = cs.croptype 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? +cropheight(cs::CropState) = cs.veg_height +cropcover(cs::CropState) = cs.veg_cover +cropyield(cs::CropState) = cs.veg_biomass # TODO: correct? units? dry or wet? isharvestable(cs::CropState) = cs.mature +# Constant in original ALMaSS code: +# `EL_VEG_START_LAIT` from `Landscape/Elements.cpp`, line 238-239 +const VEG_START_LAIT::Float64 = 1.08 + +# Constant in original ALMaSS code: +# `EL_VEG_HEIGHTSCALE` from `Landscape/Elements.cpp`, line 242-243 +const VEG_HEIGHTSCALE::Tlength = 16.0cm + +# Constant in original ALMaSS code: +# `cfg_beer_law_extinction_coef` from `Landscape/Elements.cpp`, line 139 +# TODO: units +const BEER_LAW_EXTINCTION_COEF::Float64 = 0.6 + +# TODO: units "Temperature to solar conversion factor for C3 plants." -const temperature_to_solar_conversion_c3::Vector{Float64} = [ +const temperature_to_solar_conversion_c3 = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # -30°C to -21°C 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # -20°C to -11°C 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # -10°C to -1°C @@ -126,17 +192,18 @@ const temperature_to_solar_conversion_c3::Vector{Float64} = [ 0 # 50°C ] +# TODO: units "Temperature to solar conversion factor for C4 plants." -const temperature_to_solar_conversion_c4::Vector{Float64} = [ +const temperature_to_solar_conversion_c4 = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # -30°C to -21°C 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # -20°C to -11°C - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 0°C to 9°C - 0, 0, 0, 0, 0, 0, 0, 0, 0.242857, 0.485714, # 10°C to 19°C - 0.728571, 0.971429, 1.214286, 1.457143, 1.7, 1.7, 1.7, 1.7, 1.7, 1.7, # 20°C to 29°C - 1.7, 1.7, 1.7, 1.7, 1.7, 1.7, 1.7, 1.7, 1.7, 1.7, # 30°C to 39°C - 1.7, 1.7, 1.7, 1.7, 1.7, 1.7, 1.53, 1.36, 1.19, 1.02, # 40°C to 49°C - 0.85, 0.68, 0.51, 0.34, 0.17, 0, 0, 0, 0, 0, # 50°C - 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # -10°C to -1°C + 0, 0, 0, 0, 0, 0, 0, 0, 0.242857, 0.485714, # 0°C to 9°C + 0.728571, 0.971429, 1.214286, 1.457143, 1.7, 1.7, 1.7, 1.7, 1.7, 1.7, # 10°C to 19°C + 1.7, 1.7, 1.7, 1.7, 1.7, 1.7, 1.7, 1.7, 1.7, 1.7, # 20°C to 29°C + 1.7, 1.7, 1.7, 1.7, 1.7, 1.7, 1.53, 1.36, 1.19, 1.02, # 30°C to 39°C + 0.85, 0.68, 0.51, 0.34, 0.17, 0, 0, 0, 0, 0, # 40°C to 49°C + 0 # 50°C ] """ @@ -146,7 +213,7 @@ Solar conversion factor (no units) for C3 plants. """ function solar_conversion_c3(temperature) if temperature < -30 || temperature > 50 - return 0.0 + return zero(eltype(temperature_to_solar_conversion_c3)) end idx = Int(floor(0.5 + temperature)) + 30 + 1 return temperature_to_solar_conversion_c3[idx] @@ -159,12 +226,92 @@ Solar conversion factor (no units) for C4 plants. """ function solar_conversion_c4(temperature) if temperature < -30 || temperature > 50 - return 0.0 + return zero(eltype(temperature_to_solar_conversion_c4)) end idx = Int(floor(0.5 + temperature)) + 30 + 1 return temperature_to_solar_conversion_c4[idx] end +# Below is the data for insolation in MJ per m2 for Ødum fitted to 1961-1990 +# by Olesen (1991) +# TODO: unit is u"MJ/m^2" +const global_insolation = [ + 0.91, 0.921585, 0.935138, 0.950669, 0.968188, + 0.987706, 1.009231, 1.032774, 1.058346, 1.085956, + 1.115614, 1.14733, 1.181113, 1.216974, 1.254921, + 1.294964, 1.337112, 1.381373, 1.427757, 1.476271, + 1.526923, 1.57972, 1.634671, 1.691781, 1.751057, + 1.812504, 1.876128, 1.941934, 2.009926, 2.080107, + 2.15248, 2.227048, 2.303812, 2.382774, 2.463933, + 2.547289, 2.63284, 2.720585, 2.81052, 2.902642, + 2.996945, 3.093425, 3.192073, 3.292884, 3.395847, + 3.500954, 3.608194, 3.717555, 3.829024, 3.942587, + 4.05823, 4.175935, 4.295687, 4.417465, 4.541252, + 4.667024, 4.794762, 4.924441, 5.056036, 5.189522, + 5.324873, 5.462059, 5.601051, 5.741819, 5.884329, + 6.02855, 6.174446, 6.321981, 6.471118, 6.621819, + 6.774044, 6.927753, 7.082902, 7.239449, 7.397349, + 7.556555, 7.717022, 7.8787, 8.041541, 8.205493, + 8.370505, 8.536524, 8.703496, 8.871365, 9.040077, + 9.209573, 9.379796, 9.550686, 9.722183, 9.894227, + 10.06675, 10.2397, 10.41301, 10.58661, 10.76044, + 10.93443, 11.10852, 11.28264, 11.45671, 11.63068, + 11.80448, 11.97802, 12.15125, 12.3241, 12.49648, + 12.66834, 12.8396, 13.01019, 13.18004, 13.34907, + 13.51721, 13.6844, 13.85056, 14.01561, 14.17949, + 14.34212, 14.50344, 14.66337, 14.82184, 14.97877, + 15.13411, 15.28778, 15.43971, 15.58983, 15.73807, + 15.88437, 16.02866, 16.17087, 16.31093, 16.44879, + 16.58439, 16.71764, 16.8485, 16.97691, 17.1028, + 17.22612, 17.3468, 17.46479, 17.58005, 17.6925, + 17.80211, 17.90881, 18.01256, 18.11332, 18.21102, + 18.30564, 18.39712, 18.48542, 18.5705, 18.65233, + 18.73086, 18.80605, 18.87788, 18.94632, 19.01132, + 19.07286, 19.13092, 19.18546, 19.23647, 19.28393, + 19.3278, 19.36809, 19.40475, 19.4378, 19.4672, + 19.49296, 19.51506, 19.53349, 19.54825, 19.55934, + 19.56676, 19.5705, 19.57058, 19.56698, 19.55973, + 19.54883, 19.53428, 19.51611, 19.49432, 19.46893, + 19.43996, 19.40743, 19.37136, 19.33177, 19.28868, + 19.24213, 19.19214, 19.13873, 19.08195, 19.02183, + 18.95839, 18.89168, 18.82173, 18.74858, 18.67228, + 18.59286, 18.51036, 18.42484, 18.33634, 18.2449, + 18.15058, 18.05342, 17.95348, 17.8508, 17.74545, + 17.63746, 17.52691, 17.41384, 17.29832, 17.18039, + 17.06012, 16.93757, 16.8128, 16.68586, 16.55683, + 16.42576, 16.29271, 16.15775, 16.02094, 15.88235, + 15.74203, 15.60006, 15.4565, 15.3114, 15.16485, + 15.0169, 14.86762, 14.71707, 14.56532, 14.41243, + 14.25848, 14.10352, 13.94762, 13.79084, 13.63325, + 13.47491, 13.31588, 13.15624, 12.99604, 12.83534, + 12.6742, 12.5127, 12.35088, 12.18881, 12.02654, + 11.86415, 11.70167, 11.53918, 11.37673, 11.21437, + 11.05216, 10.89016, 10.72841, 10.56697, 10.40589, + 10.24522, 10.08502, 9.925323, 9.766186, 9.607653, + 9.449772, 9.292586, 9.13614, 8.980476, 8.825635, + 8.67166, 8.518588, 8.366459, 8.215311, 8.06518, + 7.916101, 7.768108, 7.621236, 7.475515, 7.330978, + 7.187655, 7.045573, 6.904763, 6.765249, 6.62706, + 6.490218, 6.35475, 6.220676, 6.088021, 5.956804, + 5.827045, 5.698765, 5.571981, 5.44671, 5.32297, + 5.200776, 5.080142, 4.961083, 4.843613, 4.727743, + 4.613485, 4.50085, 4.38985, 4.280492, 4.172788, + 4.066743, 3.962368, 3.859668, 3.758651, 3.659322, + 3.561687, 3.465753, 3.371522, 3.279, 3.18819, + 3.099097, 3.011723, 2.926071, 2.842145, 2.759946, + 2.679477, 2.600739, 2.523735, 2.448466, 2.374933, + 2.303139, 2.233084, 2.16477, 2.098198, 2.033369, + 1.970285, 1.908946, 1.849355, 1.791512, 1.735419, + 1.681077, 1.628489, 1.577656, 1.528581, 1.481264, + 1.43571, 1.391919, 1.349896, 1.309643, 1.271164, + 1.234461, 1.199539, 1.166401, 1.135053, 1.105497, + 1.07774, 1.051786, 1.02764, 1.005309, 0.984798, + 0.966113, 0.94926, 0.934248, 0.921081, 0.909769, + 0.900317, 0.892735, 0.88703, 0.88321, 0.881284, + 0.881261, 0.883149, 0.886958, 0.892696, 0.900374, + 0.900374, # duplicated last datapoint in case the year has 366 days +] + """ Base.tryparse(type, str) @@ -196,9 +343,9 @@ function buildgrowthcurve(data::Vector{CSV.Row}) LAIgreen = Dict(janfirst=>Vector{Float64}(), sow=>Vector{Float64}(), marchfirst=>Vector{Float64}(), harvest1=>Vector{Float64}(), harvest2=>Vector{Float64}()) - height = Dict(janfirst=>Vector{Length{Float64}}(), sow=>Vector{Length{Float64}}(), - marchfirst=>Vector{Length{Float64}}(), harvest1=>Vector{Length{Float64}}(), - harvest2=>Vector{Length{Float64}}()) + height = Dict(janfirst=>Vector{Tlength}(), sow=>Vector{Tlength}(), + marchfirst=>Vector{Tlength}(), harvest1=>Vector{Tlength}(), + harvest2=>Vector{Tlength}()) for e in data append!(GDD[e.growth_phase], e.GDD) append!(LAItotal[e.growth_phase], e.LAI_total) @@ -244,31 +391,394 @@ function readcropparameters(cropdirectory::String) croptypes end +""" + setphase!(cropstate, phase) + +Set the growth `phase` of an ALMaSS `cropstate`. +""" +function setphase!(cs::CropState, phase::GrowthPhase, model::SimulationModel) + # Function in original ALMaSS code: + # `void VegElement::SetGrowthPhase(int a_phase)` from `Landscape/Elements.cpp`, line 2098 + if phase == sow + cs.vegddegs = 0 + elseif phase == harvest1 + cs.vegddegs = -1 + end + if phase == janfirst + cs.forced_phase_shift = false + # TODO: Original comments from ALMaSS below, but do they make sense? + # + # If it is the first growth phase of the year then we might + # cause some unnecessary hops if e.g. our biomass is 0 and we + # suddenly jump up to 20 cm. To stop this from happening we + # check here and if our settings are lower than the targets we + # do nothing. + if isvalidstart(cs, phase) + if startvalue_veg_height(cs, phase) < cs.veg_height + # we are better off with the numbers we have to start with + cs.LAgreen = startvalue_LAgreen(cs, phase) + cs.LAtotal = startvalue_LAtotal(cs, phase) + cs.veg_height = startvalue_veg_height(cs, phase) + end + end + elseif isvalidstart(cs, phase) + cs.LAgreen = startvalue_LAgreen(cs, phase) + cs.LAtotal = startvalue_LAtotal(cs, phase) + cs.veg_height = startvalue_veg_height(cs, phase) + end + + cs.phase = phase + cs.yddegs = 0 + cs.ddegs = get_dddegs(model) + cs.force_growth = false + + if cs.phase == janfirst + # Original comment from ALMaSS: + # + # For some growth curves there is no growth in the first two + # months of the year. This will more likely than not cause a + # discontinuous jump in the growth curves come March + # first. `force_growth_spring_test()` tries to avoid that by + # checking for positive growth values for the January growth + # phase. If none are found, then it initializes a forced + # growth/transition to the March 1st starting values. + # + # Removal of this causes continuous increase in vegetation + # growth year on year for any curve that does not have a hard + # reset (e.g. harvest). + force_growth_spring_test!(cs, model) + end +end + +# Function in original ALMaSS code: +# `void VegElement::ForceGrowthSpringTest(void)` in `Landscape/Elements.cpp`, line 2162 +function force_growth_spring_test!(cs::CropState, model::SimulationModel) + # Original comments from ALMaSS: + # + # Check if there are any positive growth differentials in the + # curve for the first two months of the year. Do nothing if there + # is. If we have any positive growth then no need to force either + if (get_LAgreen_diff(cs, janfirst, 90000.0, 0.0) > 0.001 || + get_LAtotal_diff(cs, janfirst, 90000.0, 0.0) > 0.001 || + get_height_diff(cs, janfirst, 90000.0, 0.0) > 0.001cm) + return + end + # No growth, force it. + force_growth_initialize!(cs, model) +end + +# Function in original ALMaSS code: +# `void VegElement::ForceGrowthInitialize(void)` in `Landscape/Elements.cpp`, line 2177 +function force_growth_initialize!(cs::CropState, model::SimulationModel) + # Figure out what our target phase is. + if monthday(model.date) < (1, 3) + daysleft = (Date(year(model.date), 3, 1) - model.date).value + next_phase = marchfirst + elseif monthday(model.date) >= (11, 1) + # Adjusted from 365 to prevent occaisional negative values + daysleft = 366 - dayofyear(model.date) + next_phase = janfirst + else + return + end + + if daysleft <= 0 + @warn "Uh! Oh! This really shouldn't happen." + return + end + + if ! isvalidstart(cs, next_phase) + # If no valid starting values for next phase, then + # preinitialize the random starting values! Ie. make the + # choice here and then do not choose another set come next + # phase transition, but use the values we already got at that + # point in time. + LAtotal_target, LAgreen_target, veg_height_target = random_veg_start_values(model) + # TODO: ignoring Weed_target + else + # add +/- 20% variation + vari = (@rand() * 0.4) + 0.8 + # TODO: ignoring Weed_target + LAgreen_target = startvalue_LAgreen(cs, next_phase) * vari + LAtotal_target = startvalue_LAtotal(cs, next_phase) * vari + veg_height_target = startvalue_veg_height(cs, next_phase) * vari + end + + cs.force_growth = true + # TODO ignoring: cs.force_Weed = (Weed_target - cs.weed_biomass) / daysleft + cs.force_LAgreen = (LAgreen_target - cs.LAgreen) / daysleft + cs.force_LAtotal = (LAtotal_target - cs.LAtotal) / daysleft + cs.force_veg_height = (veg_height_target - cs.veg_height) / daysleft +end + +# Function in original ALMaSS code: +# `void VegElement::RandomVegStartValues(...)` in `Landscape/Elements.cpp`, line 2090 +function random_veg_start_values(model) + LAtotal = VEG_START_LAIT * ((@rand(-10:10) / 100.0) + 1.0) + LAgreen = LAtotal / 4 + veg_height = LAgreen * VEG_HEIGHTSCALE + # TODO ignoring `weed_biomass` + return LAtotal, LAgreen, veg_height +end + +function get_LAgreen_diff(cs::CropState, phase::GrowthPhase, ddegs::Float64, yddegs::Float64) + curve = growthcurve(cs) + dds = curve.GDD[phase] + slopes = curve.LAIgreen[phase] + return find_diff(dds, slopes, ddegs, yddegs) +end +get_LAgreen_diff(cs::CropState) = + get_LAgreen_diff(cs, cs.phase, cs.ddegs, cs.yddegs) + +function get_LAtotal_diff(cs::CropState, phase::GrowthPhase, ddegs::Float64, yddegs::Float64) + curve = growthcurve(cs) + dds = curve.GDD[phase] + slopes = curve.LAItotal[phase] + return find_diff(dds, slopes, ddegs, yddegs) +end +get_LAtotal_diff(cs::CropState) = + get_LAtotal_diff(cs, cs.phase, cs.ddegs, cs.yddegs) + +function get_height_diff(cs::CropState, phase::GrowthPhase, ddegs::Float64, yddegs::Float64) + curve = growthcurve(cs) + dds = curve.GDD[phase] + slopes = curve.height[phase] + return find_diff(dds, slopes, ddegs, yddegs) +end +get_height_diff(cs::CropState) = + get_height_diff(cs, cs.phase, cs.ddegs, cs.yddegs) + +# Function in original ALMaSS code: +# `double PlantGrowthData::FindDiff(...)` in `Landscape/Plants.cpp`, line 45 +function find_diff(dds::Vector{Float64}, slopes::Vector{T}, ddegs::Float64, yddegs::Float64) where {T} + # Comment in ALMaSS code: + # + # This is broken for growth curves where one can risk passing + # more than a single inflection point in the growth curve in a + # single day... + n = length(dds) + oldindex = 1 + newindex = 1 + + if first(dds) == 99999.0 + return 0.0 * oneunit(T) # zero(T) does not work when slopes has units + end + + for i = 1:n-1 + if dds[i + 1] > ddegs + newindex = i + break + end + end + for i = 1:n-1 + if dds[i + 1] > yddegs + oldindex = i + break + end + end + if newindex > oldindex + # We have passed an inflection point between today and yesterday. + # First add the increment from yesterdays day degree sum up to + # the inflection point. + dddif = dds[newindex] - yddegs + diff = slopes[oldindex] * dddif + + # Then from the inflection point up to today. + dddif = ddegs - dds[newindex] + diff += slopes[newindex] * dddif + else + # No inflection point passed + diff = slopes[newindex] * (ddegs - yddegs) + end + return diff +end + +# Function in original ALMaSS code: +# `double Weather::GetDDDegs(long a_date)` in `Landscape/Weather.cpp`, line 205 +# TODO: original ALMaSS code does not use a minimum growth temperature? +get_dddegs(model::SimulationModel) = bounds(supply_temperature(model)) + +# TODO efficiency: the fertiliser check is done in many places +growthcurve(cs::CropState) = + fertiliser in cs.events ? cs.croptype.lownutrientgrowth : cs.croptype.highnutrientgrowth + +# Function in original ALMaSS code: +# `double PlantGrowthData::GetStartValue(int a_veg_type, int a_phase, int a_type)` in `Landscape/Plants.h`, line 142 +startvalue_veg_height(cs::CropState, phase::GrowthPhase) = + first(growthcurve(cs).height[phase]) +startvalue_LAgreen(cs::CropState, phase::GrowthPhase) = + first(growthcurve(cs).LAIgreen[phase]) +startvalue_LAtotal(cs::CropState, phase::GrowthPhase) = + first(growthcurve(cs).LAItotal[phase]) + +# Function in original ALMaSS code: +# `bool PlantGrowthData::StartValid(int a_veg_type, int a_phase)` in `Landscape/Plants.cpp`, line 835 +# The original logic for this is in `Landscape/Plants.cpp`, line 213 +# where `m_start_valid` is set. +isvalidstart(cs::CropState, phase::GrowthPhase) = + growthcurve(cs).height[phase] == -1.0cm + + """ stepagent!(cropstate, model) Update a farm plot by one day. """ function stepagent!(cs::CropState, model::SimulationModel) + # Function in original ALMaSS code: + # `void VegElement::DoDevelopment(void)` from `Landscape/Elements.cpp`, line 2254 + + # This part happens in the ALMaSS function `void Landscape::Tick(void)` + # in `Landscape/Landscape.cpp`, line 2272 + # # update the phase on key dates if monthday(model.date) == (1, 1) - cs.phase = ALMaSS.janfirst - cs.growingdegreedays = 0.0 + setphase!(cs, janfirst, model) elseif monthday(model.date) == (3, 1) - cs.phase = ALMaSS.marchfirst - cs.growingdegreedays = 0.0 + setphase!(cs, marchfirst, model) + end + + # TODO ignoring: cs.new_weed_growth = 0.0 + if ! cs.force_growth + cs.yddegs = cs.ddegs + pos_temp_today = get_dddegs(model) + if cs.vegddegs != -1 + # Sum up the vegetation day degrees since sowing + cs.vegddegs += pos_temp_today + end + # And sum up the phase ddegs + cs.ddegs = pos_temp_today + cs.yddegs + + dLAG = get_LAgreen_diff(cs) * cs.growth_scaler + dLAT = get_LAtotal_diff(cs) * cs.growth_scaler + dHgt = get_height_diff(cs) * cs.growth_scaler + + cs.LAgreen += dLAG + cs.LAtotal += dLAT + cs.veg_height += dHgt + if cs.LAgreen < 0.0 + cs.LAgreen = 0.0 + end + if cs.LAtotal < 0.0 + cs.LAtotal = 0.0 + end + if cs.veg_height < 0.0cm + cs.veg_height = 0.0cm + end + + # TODO ignored: calculation of cs.weedddegs, cs.new_weed_growth, cs.weed_biomass + else + force_growth_development!(cs) + end + + if cs.LAtotal < cs.LAgreen + @warn "Leaf Area Total smaller than Leaf Area Green (Veg Growth Model inconsistent). Performing hack correction." + cs.LAtotal = 1.1 * cs.LAgreen + end + + recalculate_bugs_n_stuff!(cs, model) + + # TODO ignored: ResetGeese() + # TODO ignored: Deal with any possible unsprayed margin, transferring info as necessary + # TODO ignored: cs.interested_green_biomass = cs.green_biomass * cs.interested_biomass_fraction +end + +# Function in original ALMaSS code: +# `void VegElement::ForceGrowthDevelopment(void)` from `Landscape/Elements.cpp`, line 2223 +function force_growth_development!(cs::CropState) + # TODO ignored: cs.weed_biomass += cs.force_Weed + # TODO ignored: cs.new_weed_growth = cs.force_Weed + cs.LAgreen += cs.force_LAgreen + cs.LAtotal += cs.force_LAtotal + cs.veg_height += cs.force_veg_height + if cs.LAgreen < 0 + cs.LAgreen = 0 + end + if cs.LAtotal < 0 + cs.LAtotal = 0 + end + if cs.veg_height < 0.0cm + cs.veg_height = 0.0cm + end +end + +# Function in original ALMaSS code: +# `void VegElement::RecalculateBugsNStuff(void)` from `Landscape/Elements.cpp`, line 1704 +function recalculate_bugs_n_stuff!(cs::CropState, model::SimulationModel) + # This is the heart of the dynamics of vegetation elements. It + # calculates vegetation cover and uses this to determine + # vegetation biomass. It also calculates spilled grain and goose + # forage, as well a calculating insect biomass, vegetation density + # and dead biomass. + + cs.newgrowth = 0 + + # Beer's Law to give vegetation cover + cs.veg_cover = 1.0 - exp(- BEER_LAW_EXTINCTION_COEF * cs.LAtotal) + + # This is used to calc growth rate + # TODO: hardcoded extinction coefficient of 0.4 + useful_veg_cover = 1.0 - exp(cs.LAgreen * -0.4) + + # Need global radiation today + glrad = supply_global_radiation(model) + + temperature = supply_temperature(model) + if cs.croptype.is_c4_plant + radconv = solar_conversion_c4(temperature) + else + radconv = solar_conversion_c3(temperature) + end + + if cs.LAtotal >= cs.oldLAtotal + # we are in positive growth so grow depending on our equation + + # TODO: biomass_scale + biomass_scale = 1.0 + cs.newgrowth = useful_veg_cover * glrad * radconv * biomass_scale + + # TODO: ignoring farm intensity + cs.veg_biomass += cs.newgrowth + else + # Negative growth - so shrink proportional to the loss in LAI Total + if cs.oldLAtotal > 0 + temp_proportion = cs.LAtotal / cs.oldLAtotal + cs.veg_biomass *= temp_proportion + end + end + + # Here we also want to know how much biomass we have on the field + # in total. So we multiply the current biomass by area + # cs.total_biomass = cs.veg_biomass * cs.area + # cs.total_biomass_old = cs.total_biomass + + # NB The cs.weed_biomass is calculated directly from the curve in Curves.pre + # rather than going through the rigmarole of converting leaf-area index + # cs.veg_density = Int(floor(0.5 + (cs.veg_biomass / (1 + cs.veg_height)))) + + # if cs.veg_density > 100 + # cs.veg_density = 100 # to stop array bounds problems + # end + + if cs.LAtotal == 0.0 + cs.green_biomass = 0.0 + else + cs.green_biomass = cs.veg_biomass * (cs.LAgreen / (cs.LAtotal)) end - # 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 = cs.croptype.mingrowthtemp - ismissing(basetemp) && (basetemp = 5.0) - gdd = (maxtemp(model)+mintemp(model))/2 - basetemp - gdd > 0 && (cs.growingdegreedays += gdd) - # update crop growth - growcrop!(cs, gdd, model) + cs.dead_biomass = cs.veg_biomass - cs.green_biomass + + # TODO: species-specific calculations + # Here we use our member function pointer to call the correct piece of code for our current species + #(this->*(SpeciesSpecificCalculations))(); end +# m_Landscape->SupplyTemp() +supply_temperature(model::SimulationModel) = (mintemp(model) + maxtemp(model) / 2) + +# m_Landscape->SupplyGlobalRadiation() +supply_global_radiation(model::SimulationModel) = + global_insolation[dayofyear(model.date)] + ## CROP MANAGEMENT AND GROWTH FUNCTIONS @@ -281,13 +791,16 @@ function sow!(cs::CropState, model::SimulationModel, cropname::String) !ismissing(cs.croptype.minsowdate) && model.date < cs.croptype.minsowdate && @warn "$(model.date) is earlier than the minimum sowing date for $(cropname)." cs.croptype = model.crops[cropname] - cs.phase = ALMaSS.sow - cs.growingdegreedays = 0.0 - cs.height = 0.0cm - cs.LAItotal = 0.0 - cs.LAIgreen = 0.0 + setphase!(cs, sow, model) cs.mature = false - cs.events = Vector{Management}() + empty!(cs.events) + + # cs.vegddegs = 0.0 + # cs.ddegs = 0.0 + # cs.yddegs = 0.0 + # cs.veg_height = 0.0cm + # cs.LAItotal = 0.0 + # cs.LAIgreen = 0.0 end """ @@ -296,10 +809,8 @@ end Harvest the crop of this cropstate. """ function harvest!(cs::CropState, model::SimulationModel) - cs.phase in [ALMaSS.harvest1, ALMaSS.harvest2] ? - cs.phase = ALMaSS.harvest2 : - cs.phase = ALMaSS.harvest1 - cs.growingdegreedays = 0.0 + phase = (cs.phase in (ALMaSS.harvest1, ALMaSS.harvest2) ? ALMaSS.harvest2 : ALMaSS.harvest1) + setphase!(cs, phase, model) cs.mature = false # height & LAI will be automatically adjusted by the growth function #TODO calculate and return yield @@ -309,49 +820,51 @@ end #TODO spray!() #TODO till!() -""" - growcrop!(cropstate, gdd, model) -Apply the relevant crop growth model to update the plants crop state -on this farm plot. Implements the ALMaSS crop growth model by Topping -et al. (see `ALMaSS/Landscape/plant.cpp:PlantGrowthData::FindDiff()` and -`ALMaSS/Landscape/elements.cpp:VegElement::DoDevelopment()`). -""" -function growcrop!(cs::CropState, gdd::Float64, model::SimulationModel) - fertiliser in cs.events ? - curve = cs.croptype.lownutrientgrowth : - curve = cs.croptype.highnutrientgrowth - points = curve.GDD[cs.phase] #FIXME what if the curve is empty? - for p in 1:length(points) - if points[p] == 99999 - !(cs.phase in (janfirst, sow)) && (cs.mature = true) #FIXME only in the last phase? - return # the marker that there is no further growth this phase - elseif points[p] == -1 # the marker to set all variables to specified values - cs.height = curve.height[cs.phase][p] - cs.LAItotal = curve.LAItotal[cs.phase][p] - cs.LAIgreen = curve.LAIgreen[cs.phase][p] - return - else - gdd = cs.growingdegreedays - # figure out which is the correct slope value to use for growth - if p == length(points) || gdd < points[p+1] - #FIXME it appears to me from the ALMaSS source code that the curve value - # for the day is multiplied with that day's growingdegreedays to give the - # diff, which is then added to the previous values. However, this results - # in values that are staggeringly too large. I'm not quite sure what the - # issue is here... - cs.height += bounds(curve.height[cs.phase][p]*gdd) - cs.LAItotal += bounds(curve.LAItotal[cs.phase][p]*gdd) - cs.LAIgreen += bounds(curve.LAIgreen[cs.phase][p]*gdd) - 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 + +# """ +# growcrop!(cropstate, gdd, model) + +# Apply the relevant crop growth model to update the plants crop state +# on this farm plot. Implements the ALMaSS crop growth model by Topping +# et al. (see `ALMaSS/Landscape/plant.cpp:PlantGrowthData::FindDiff()` and +# `ALMaSS/Landscape/elements.cpp:VegElement::DoDevelopment()`). +# """ +# function growcrop!(cs::CropState, gdd::Float64, model::SimulationModel) +# fertiliser in cs.events ? +# curve = cs.croptype.lownutrientgrowth : +# curve = cs.croptype.highnutrientgrowth +# points = curve.GDD[cs.phase] #FIXME what if the curve is empty? +# for p in 1:length(points) +# if points[p] == 99999 +# !(cs.phase in (janfirst, sow)) && (cs.mature = true) #FIXME only in the last phase? +# return # the marker that there is no further growth this phase +# elseif points[p] == -1 # the marker to set all variables to specified values +# cs.height = curve.height[cs.phase][p] +# cs.LAItotal = curve.LAItotal[cs.phase][p] +# cs.LAIgreen = curve.LAIgreen[cs.phase][p] +# return +# else +# gdd = cs.growingdegreedays +# # figure out which is the correct slope value to use for growth +# if p == length(points) || gdd < points[p+1] +# #FIXME it appears to me from the ALMaSS source code that the curve value +# # for the day is multiplied with that day's growingdegreedays to give the +# # diff, which is then added to the previous values. However, this results +# # in values that are staggeringly too large. I'm not quite sure what the +# # issue is here... +# cs.height += bounds(curve.height[cs.phase][p]*gdd) +# cs.LAItotal += bounds(curve.LAItotal[cs.phase][p]*gdd) +# cs.LAIgreen += bounds(curve.LAIgreen[cs.phase][p]*gdd) +# 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 end # module ALMaSS diff --git a/src/crop/cropmodels.jl b/src/crop/cropmodels.jl index 3480da2..92722a1 100644 --- a/src/crop/cropmodels.jl +++ b/src/crop/cropmodels.jl @@ -65,9 +65,8 @@ 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.0cm, 0.0, 0.0, false, Vector{Management}(), false + croptype = model.crops["natural grass"], + phase = phase, ) elseif @param(crop.cropmodel) == "simple" cs = SimpleCrop.CropState( diff --git a/test/crop_tests.jl b/test/crop_tests.jl index 7182ce0..38ae55a 100644 --- a/test/crop_tests.jl +++ b/test/crop_tests.jl @@ -27,8 +27,7 @@ end mature = false events = Ps.Management[] force_growth = false - fp = FarmPlot(id, pixels, farmer, - Ps.ALMaSS.CropState(ct, Ps.ALMaSS.janfirst, 0.0, 0.0m, 0.0, 0.0, mature, events, force_growth)) + fp = FarmPlot(id, pixels, farmer, Ps.ALMaSS.CropState(croptype=ct, phase=Ps.ALMaSS.janfirst)) @test fp isa FarmPlot @test fp isa FarmPlot{Ps.ALMaSS.CropState} @test croptype(fp) isa Ps.ALMaSS.CropType diff --git a/test/nature_tests.jl b/test/nature_tests.jl index a392287..0a786ca 100644 --- a/test/nature_tests.jl +++ b/test/nature_tests.jl @@ -51,8 +51,8 @@ end) # end eval fp = Ps.FarmPlot( 1, [(6,6)], 1, Ps.ALMaSS.CropState( - model.crops["winter wheat"], Ps.ALMaSS.janfirst, - 0.0, 0.0m, 0.0, 0.0, false, Vector{Ps.Management}(), false + croptype = model.crops["winter wheat"], + phase = Ps.ALMaSS.janfirst, ) ) push!(model.farmplots, fp) -- GitLab