### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe. ### ### This file is responsible for managing the crop growth modules. ### # 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. #FIXME Looking at fields.pdf in the Persefone output, crop maturity is not recognised # properly, so most crops (except maize) are never planted. At the same time, maize never # seems to start growing. #TODO write tests to compare our output to the output of the original ALMaSS algorithm module ALMaSS ## The two input files required by ALMaSS. Must be present in the crop directory. const CROPFILE = "crop_data_general.csv" const GROWTHFILE = "crop_growth_curves.csv" using Persefone: @rand, @u_str, AnnualDate, Management, cm, SimulationModel, fertiliser, maxtemp, mintemp, year, dayofyear import Persefone: stepagent!, croptype, cropname, cropheight, cropcover, cropyield, sow!, harvest!, isharvestable, bounds 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 ALMaSS crop growth curves are split into five phases, triggered by seasonal dates or agricultural events. """ @enum GrowthPhase janfirst sow marchfirst harvest1 harvest2 """ CropCurveParams The values in this struct define one crop growth curve. """ struct CropCurveParams #TODO add Unitful curveID::Int highnutrients::Bool GDD::Dict{GrowthPhase,Vector{Float64}} LAItotal::Dict{GrowthPhase, Vector{Float64}} LAIgreen::Dict{GrowthPhase, Vector{Float64}} height::Dict{GrowthPhase, Vector{typeof(1.0cm)}} end """ CropType The type struct for all crops. Currently follows the crop growth model as implemented in ALMaSS. """ struct CropType #FIXME this needs thinking about. The sowing and harvest dates belong in the farm model, # not here. Also, we need to harmonise crops across the crop growth models. name::String group::String is_c4_plant::Bool # false means it is a C3 plant minsowdate::Union{Missing,AnnualDate} maxsowdate::Union{Missing,AnnualDate} minharvestdate::Union{Missing,AnnualDate} maxharvestdate::Union{Missing,AnnualDate} mingrowthtemp::Union{Missing,Float64} highnutrientgrowth::Union{Missing,CropCurveParams} lownutrientgrowth::Union{Missing,CropCurveParams} biomass_scale::Float64 end cropname(ct::CropType) = ct.name """ CropState The state data for an ALMaSS vegetation point calculation. Usually part of a `FarmPlot`. """ @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 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.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 = [ 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 0, 0, 0, 0, 0, 0.28, 0.56, 0.84, 1.12, 1.4, # 0°C to 9°C 1.4, 1.4, 1.4, 1.4, 1.4, 1.4, 1.4, 1.4, 1.4, 1.4, # 10°C to 19°C 1.4, 1.4, 1.4, 1.4, 1.4, 1.4, 1.26, 1.12, 0.98, 0.84, # 20°C to 29°C 0.7, 0.56, 0.42, 0.28, 0.14, 0, 0, 0, 0, 0, # 30°C to 39°C 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 40°C to 49°C 0 # 50°C ] # TODO: units "Temperature to solar conversion factor for C4 plants." 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, # -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 ] """ solar_conversion_c3(temperature) Solar conversion factor (no units) for C3 plants. """ function solar_conversion_c3(temperature) if temperature < -30 || temperature > 50 return zero(eltype(temperature_to_solar_conversion_c3)) end idx = Int(floor(0.5 + temperature)) + 30 + 1 return temperature_to_solar_conversion_c3[idx] end """ solar_conversion_c3(temperature) Solar conversion factor (no units) for C4 plants. """ function solar_conversion_c4(temperature) if temperature < -30 || temperature > 50 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) Extend `tryparse` to allow parsing GrowthPhase values. (Needed to read in the CSV parameter file.) """ function Base.tryparse(::Type{GrowthPhase}, str::String) str == "janfirst" ? janfirst : str == "sow" ? sow : str == "marchfirst" ? marchfirst : str == "harvest1" ? harvest1 : str == "harvest2" ? harvest2 : nothing end """ buildgrowthcurve(data) Convert a list of rows from the crop growth data into a CropCurveParams object. """ function buildgrowthcurve(data::Vector{CSV.Row}) isempty(data) && return missing GDD = Dict(janfirst=>Vector{Float64}(), sow=>Vector{Float64}(), marchfirst=>Vector{Float64}(), harvest1=>Vector{Float64}(), harvest2=>Vector{Float64}()) LAItotal = Dict(janfirst=>Vector{Float64}(), sow=>Vector{Float64}(), marchfirst=>Vector{Float64}(), harvest1=>Vector{Float64}(), harvest2=>Vector{Float64}()) LAIgreen = Dict(janfirst=>Vector{Float64}(), sow=>Vector{Float64}(), marchfirst=>Vector{Float64}(), harvest1=>Vector{Float64}(), harvest2=>Vector{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) append!(LAIgreen[e.growth_phase], e.LAI_green) append!(height[e.growth_phase], e.height * cm) # assumes `height` is in units of `cm` end CropCurveParams(data[1].curve_id, data[1].nutrient_status=="high", GDD, LAItotal, LAIgreen, height) end """ readcropparameters(cropdirectory) Parse a CSV file containing the required parameter values for each crop (as produced from the original ALMaSS file by `convert_almass_data.py`). """ function readcropparameters(cropdirectory::String) @debug "Reading crop parameters" cropdata = CSV.File(joinpath(cropdirectory, CROPFILE), missingstring="NA", types=[String,AnnualDate,AnnualDate,AnnualDate,AnnualDate,Float64,String,Float64,Bool]) growthdata = CSV.File(joinpath(cropdirectory, GROWTHFILE), missingstring="NA", types=[Int,String,String,GrowthPhase,String, Float64,Float64,Float64,Float64]) croptypes = Dict{String,CropType}() for crop in cropdata cropgrowthdata = growthdata |> filter(x -> !ismissing(x.crop_name) && x.crop_name == crop.name) highnuts = buildgrowthcurve(cropgrowthdata |> filter(x -> x.nutrient_status=="high")) lownuts = buildgrowthcurve(cropgrowthdata |> filter(x -> x.nutrient_status=="low")) croptypes[crop.name] = CropType(crop.name, crop.group, crop.is_c4_plant, crop.minsowdate, crop.maxsowdate, crop.minharvestdate, crop.maxharvestdate, crop.mingrowthtemp, highnuts, lownuts, crop.biomass_scale) end 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... # Note: the code is slightly different to the original ALMaSS # code, which I think is caused by ALMaSS not storing the -1 entry # in the dds array. if first(dds) == 99999.0 return 0.0 * oneunit(T) # zero(T) does not work when slopes has units end function get_index(array::Tarr, threshold::eltype(Tarr)) where {Tarr <: DenseArray} idx = findfirst(x -> x > threshold, array) if isnothing(idx) idx = lastindex(array) + 1 end idx -= 1 if idx < firstindex(array) error("Index too small, idx = $idx") end return idx end newindex = get_index(dds, ddegs) oldindex = get_index(dds, yddegs) dds_newindex = dds[newindex] == -1.0 ? zero(eltype(dds)) : dds[newindex] slopes_oldindex = dds[oldindex] == -1.0 ? zero(eltype(slopes)) : slopes[oldindex] slopes_newindex = dds[newindex] == -1.0 ? zero(eltype(slopes)) : slopes[newindex] 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 function growthcurve(cs::CropState) if ismissing(cs.croptype.lownutrientgrowth) && ismissing(cs.croptype.highnutrientgrowth) error("No growth curves available for cropstate:\n $cs") end if fertiliser in cs.events curve = cs.croptype.highnutrientgrowth if ismissing(curve) @warn "fertiliser used, but highnutrientgrowth curve is missing. Using lownutrientgrowth curve." curve = cs.croptype.lownutrientgrowth end else curve = cs.croptype.lownutrientgrowth if ismissing(curve) # Note: This warning is commented out, as this is quite # common for e.g. "permanent grassland" # @warn "fertiliser not used, but lownutrientgrowth curve is missing. Using highnutrientgrowth curve." curve = cs.croptype.highnutrientgrowth end end return curve end # 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) setphase!(cs, janfirst, model) elseif monthday(model.date) == (3, 1) 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 biomass_scale = cs.croptype.biomass_scale 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 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 """ sow!(cropstate, model, cropname) Change the cropstate to sow the specified crop. """ 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] setphase!(cs, sow, model) cs.mature = false # TODO: is this correct to remove all events? this would # e.g. remove fertilising event? 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 """ harvest!(cropstate, model) Harvest the crop of this cropstate. """ function harvest!(cs::CropState, model::SimulationModel) 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 end #TODO fertilise!() #TODO spray!() #TODO till!() end # module ALMaSS