Skip to content
Snippets Groups Projects
Select Git revision
  • e36dd056c545630e900781cfd9a0d83d65def5b1
  • master default protected
  • development
  • fix-missing-weatherdata
  • fix-eichsfeld-weather
  • marco-development
  • marco/dev-aquacrop
  • precompile-statements
  • precompile-tools
  • tmp-faster-loading
  • skylark
  • testsuite
  • code-review
  • v0.7.0
  • v0.6.1
  • v0.6.0
  • v0.5.5
  • v0.5.4
  • v0.5.3
  • v0.5.2
  • v0.2
  • v0.3.0
  • v0.4.1
  • v0.5
24 results

CONTRIBUTORS.md

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    almass.jl 33.71 KiB
    ### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe.
    ###
    ### This file is responsible for managing the crop growth modules.
    ###
    
    #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
    
    # 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.
    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}
    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])
        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"))
            # TODO: set crop group temporarily until there is a column in
            # the csv file
            crop_group = "CROP_GROUP_NOT_SET"
            # TODO: it would be better to save this in the parameter file
            # (Note that this matches the current ALMaSS code though,
            # which also only hardcodes maize as a C4 crop)
            is_c4_plant = occursin("maize", lowercase(crop.name))
            croptypes[crop.name] = CropType(crop.name, crop_group, is_c4_plant, crop.minsowdate,
                                            crop.maxsowdate, crop.minharvestdate, crop.maxharvestdate,
                                            crop.mingrowthtemp, highnuts, lownuts)
        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...
        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)
            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
    
            # 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
        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
        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!()
    
    
    
    # """
    #     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