diff --git a/src/Persefone.jl b/src/Persefone.jl
index ec269bfd733887472526f65acaad82a1e779f3e5..ad32aef3a8e66814209fc56b7ab69397b74c40c8 100644
--- a/src/Persefone.jl
+++ b/src/Persefone.jl
@@ -100,7 +100,12 @@ export
     populationtrends,
     visualiseoutput,
     savemodelobject,
-    loadmodelobject
+    loadmodelobject,
+    croptype,
+    cropname,
+    cropheight,
+    cropcover,
+    cropyield
 
 ## Import and define units and dimensions
 import Unitful: cm, m, km, ha, mg, g, kg, Length, Area, Mass
@@ -127,6 +132,8 @@ The supertype of all agents in the model (animal species, farmer types, farmplot
 """
 abstract type ModelAgent end
 
+function stepagent! end
+
 ## include all module files (note that the order matters - if file
 ## b references something from file a, it must be included later)
 include("core/input.jl")
@@ -136,8 +143,11 @@ include("analysis/makieplots.jl")
 include("world/landscape.jl")
 include("world/weather.jl")
 
-include("crop/crops.jl")
 include("crop/farmplot.jl")
+include("crop/cropmodels.jl")
+include("crop/almass.jl")
+include("crop/simplecrop.jl")
+
 include("farm/farm.jl")
 
 include("nature/insects.jl")
diff --git a/src/core/input.jl b/src/core/input.jl
index a8bc2a42d392c2ab220590867d791a53644fd9fe..46460cb92af1701ff1109771a439809722bb44a5 100644
--- a/src/core/input.jl
+++ b/src/core/input.jl
@@ -17,6 +17,11 @@ const PARAMFILE = joinpath(pkgdir(Persefone), "src/parameters.toml")
 #XXX do I need to use absolute paths for all input files in case working dir is changed?
 # (can be done with `joinpath(dirname(@__FILE__), <filename>)` )
 
+"""
+The crop models that can be used in the simulation.
+"""
+const AVAILABLE_CROPMODELS = ["almass", "simple"]
+
 """
     @param(domainparam)
 
@@ -89,6 +94,9 @@ function preprocessparameters(settings::Dict{String,Any}, defaultoutdir::String)
         Base.error("Enddate is earlier than startdate.") #TODO replace with exception
     end
     settings["world.mapresolution"] = settings["world.mapresolution"] * 1m
+    if ! (settings["crop.cropmodel"] in AVAILABLE_CROPMODELS)
+        error("crop.cropmodel = \"$(settings["crop.cropmodel"])\", but has to be one of: $AVAILABLE_CROPMODELS")
+    end
     #FIXME enable parallelisation
     # if !isempty(settings["internal.scanparams"]) && (nprocs() < 2)
     #     @warn "To parallelise multiple simulation runs, use `julia -p <n>`."
diff --git a/src/core/simulation.jl b/src/core/simulation.jl
index 05158aa6590a6e4bcc38941ca6332d45f734b778..ac9a4570dfe97298e33f1cf582162cb4236daccd 100644
--- a/src/core/simulation.jl
+++ b/src/core/simulation.jl
@@ -12,7 +12,7 @@ This is the heart of the model - a struct that holds all data and state
 for one simulation run. It is created by [`initialise`](@ref) and passed
 as input to most model functions.
 """
-mutable struct AgricultureModel <: SimulationModel
+mutable struct AgricultureModel{Tcroptype,Tcropstate} <: SimulationModel
     settings::Dict{String,Any}
     rng::AbstractRNG
     logger::AbstractLogger
@@ -21,9 +21,9 @@ mutable struct AgricultureModel <: SimulationModel
     date::Date
     landscape::Matrix{Pixel}
     weather::Dict{Date,Weather}
-    crops::Dict{String,CropType}
+    crops::Dict{String,Tcroptype}
     farmers::Vector{Farmer}
-    farmplots::Vector{FarmPlot}
+    farmplots::Vector{FarmPlot{Tcropstate}}
     animals::Vector{Union{Animal,Nothing}}
     migrants::Vector{Pair{Animal,Date}}
     events::Vector{FarmEvent}
@@ -117,23 +117,29 @@ function initmodel(settings::Dict{String, Any})
                                        settings["world.weatherfile"]),
                               settings["core.startdate"],
                               settings["core.enddate"])
-        crops = readcropparameters(settings["crop.cropfile"],
-                                   settings["crop.growthfile"])
-        model = AgricultureModel(settings,
-                                 StableRNG(settings["core.seed"]),
-                                 logger,
-                                 Vector{DataOutput}(),
-                                 Dict{String, DataFrame}(),
-                                 settings["core.startdate"],
-                                 landscape,
-                                 weather,
-                                 crops,
-                                 Vector{Farmer}(),
-                                 Vector{FarmPlot}(),
-                                 Vector{Union{Animal,Nothing}}(),
-                                 Vector{Pair{Animal, Date}}(),
-                                 Vector{FarmEvent}())
+        crops, Tcroptype, Tcropstate = initcropmodel(settings["crop.cropmodel"],
+                                                     settings["crop.cropfile"],
+                                                     settings["crop.growthfile"])
+        farmers = Vector{Farmer}()
+        farmplots = Vector{FarmPlot{Tcropstate}}()
+        model = AgricultureModel{Tcroptype,Tcropstate}(
+            settings,
+            StableRNG(settings["core.seed"]),
+            logger,
+            Vector{DataOutput}(),
+            Dict{String, DataFrame}(),
+            settings["core.startdate"],
+            landscape,
+            weather,
+            crops,
+            farmers,
+            farmplots,
+            Vector{Union{Animal,Nothing}}(),
+            Vector{Pair{Animal, Date}}(),
+            Vector{FarmEvent}()
+        )
         saveinputfiles(model)
+
         initfields!(model)
         initfarms!(model)
         initnature!(model)
diff --git a/src/crop/almass.jl b/src/crop/almass.jl
new file mode 100644
index 0000000000000000000000000000000000000000..5ac4c53ed22c13ab66fe9125577659fd3879b838
--- /dev/null
+++ b/src/crop/almass.jl
@@ -0,0 +1,258 @@
+### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe.
+###
+### This file is responsible for managing the crop growth modules.
+###
+
+#TODO write tests for input functions
+
+module ALMaSS
+
+using Persefone:
+    Management,
+    Length,
+    cm,
+    SimulationModel,
+    fertiliser,
+    maxtemp,
+    mintemp
+
+import Persefone:
+    stepagent!,
+    croptype,
+    cropname,
+    cropheight,
+    cropcover,
+    cropyield
+
+using Dates: Date, month, monthday
+using CSV: CSV
+
+"""
+    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{Length{Float64}}}
+end
+
+"""
+    CropType
+
+The type struct for all crops. Currently follows the crop growth model as
+implemented in ALMaSS.
+"""
+struct CropType
+    name::String
+    minsowdate::Union{Missing,Date}
+    maxsowdate::Union{Missing,Date}
+    minharvestdate::Union{Missing,Date}
+    maxharvestdate::Union{Missing,Date}
+    mingrowthtemp::Union{Missing,Float64}
+    highnutrientgrowth::Union{Missing,CropCurveParams}
+    lownutrientgrowth::Union{Missing,CropCurveParams}
+    #issowable::Union{Function,Bool}
+end
+
+cropname(ct::CropType) = ct.name
+
+"""
+    CropState
+
+The state data for an ALMaSS vegetation point calculation.  Usually
+part of a `FarmPlot`.
+"""
+mutable struct CropState
+    #TODO add Unitful
+    croptype::CropType
+    phase::GrowthPhase
+    growingdegreedays::Float64
+    height::Length{Float64}
+    LAItotal::Float64
+    LAIgreen::Float64
+    #biomass::Float64 #XXX I need to figure out how to calculate this
+    events::Vector{Management}
+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?
+
+"""
+    Base.tryparse(type, str)
+
+Extend `tryparse` to allow parsing GrowthPhase values.
+(Needed to read in the CSV parameter file.)
+"""
+function Base.tryparse(type::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{Length{Float64}}(), sow=>Vector{Length{Float64}}(),
+                  marchfirst=>Vector{Length{Float64}}(), harvest1=>Vector{Length{Float64}}(),
+                  harvest2=>Vector{Length{Float64}}())
+    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(generalcropfile, cropgrowthfile)
+
+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(generalcropfile::String, growthfile::String)
+    @debug "Reading crop parameters"
+    cropdata = CSV.File(generalcropfile, missingstring="NA", dateformat="d U",
+                        types=[String,Date,Date,Date,Date,Float64])
+    growthdata = CSV.File(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.minsowdate, crop.maxsowdate,
+                                        crop.minharvestdate, crop.maxharvestdate,
+                                        crop.mingrowthtemp, highnuts, lownuts)
+    end
+    croptypes
+end
+
+"""
+    stepagent!(cropstate, model)
+
+Update a farm plot by one day.
+"""
+function stepagent!(cs::CropState, model::SimulationModel)
+    # update growing degree days
+    # if no crop-specific base temperature is given, default to 5°C
+    # (https://www.eea.europa.eu/publications/europes-changing-climate-hazards-1/heat-and-cold/heat-and-cold-2014-mean)
+    basetemp = cs.croptype.mingrowthtemp
+    ismissing(basetemp) && (basetemp = 5.0)
+    gdd = (maxtemp(model)+mintemp(model))/2 - basetemp
+    gdd > 0 && (cs.growingdegreedays += gdd)
+    # update the phase on key dates
+    monthday(model.date) == (1,1) && (cs.phase = ALMaSS.janfirst)
+    monthday(model.date) == (3,1) && (cs.phase = ALMaSS.marchfirst)
+    # update crop growth
+    growcrop!(cs, model)
+end
+
+
+## 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)
+    #XXX test if the crop is sowable?
+    cs.croptype = model.crops[cropname]
+    cs.phase = ALMaSS.sow
+end
+
+"""
+    harvest!(cropstate, model)
+
+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
+    # height & LAI will be automatically adjusted by the growth function
+    #TODO calculate and return yield
+end
+
+#TODO fertilise!()
+#TODO spray!()
+#TODO till!()
+
+"""
+    growcrop!(cropstate, 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.
+"""
+function growcrop!(cs::CropState, model::SimulationModel)
+    fertiliser in cs.events ?
+        curve = cs.croptype.lownutrientgrowth :
+        curve = cs.croptype.highnutrientgrowth
+    points = curve.GDD[cs.phase]
+    for p in 1:length(points)
+        if points[p] == 99999
+            return # the marker that there is no further growth this phase
+        elseif points[p] == -1 # the marker to set all variables to specified values
+            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]
+                cs.height += curve.height[cs.phase][p]
+                cs.LAItotal += curve.LAItotal[cs.phase][p]
+                cs.LAIgreen += curve.LAIgreen[cs.phase][p]
+                return
+            end
+            #XXX To be precise, we ought to figure out if one or more inflection
+            # points have been passed between yesterday and today, and calculate the
+            # growth exactly up to the inflection point with the old slope, and onward
+            # with the new slope. Not doing so will introduce a small amount of error,
+            # although I think this is acceptable.
+        end
+    end
+end
+
+end # module ALMaSS
diff --git a/src/crop/cropmodels.jl b/src/crop/cropmodels.jl
new file mode 100644
index 0000000000000000000000000000000000000000..bbbd92529424792d327ff7f5e3d55cfd0dfc5b6b
--- /dev/null
+++ b/src/crop/cropmodels.jl
@@ -0,0 +1,83 @@
+### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe.
+###
+### Functions for switchable crop models.
+###
+
+"""
+    initfields!(cropmodel, cropfile, growthfile)
+
+Initialise the crop model.  Returns the crop types available in the
+simulation, as well as the types `Tcroptype` and `Tcropstate`.
+"""
+function initcropmodel(cropmodel::AbstractString, cropfile::AbstractString, growthfile::AbstractString)
+    if cropmodel == "almass"
+        Tcroptype = ALMaSS.CropType
+        Tcropstate = ALMaSS.CropState
+        crops = ALMaSS.readcropparameters(cropfile, growthfile)
+    elseif cropmodel == "simple"
+        Tcroptype = SimpleCrop.CropType
+        Tcropstate = SimpleCrop.CropState
+        crops_almass = ALMaSS.readcropparameters(cropfile, growthfile)
+        crops = Dict(name => SimpleCrop.CropType(ct.name) for (name, ct) in crops_almass)
+    else
+        error("initcropmodel: no implementation for crop model '$cropmodel'")
+    end
+    return crops, Tcroptype, Tcropstate
+end
+
+"""
+    initfields!(model)
+
+Initialise the farm plots in the simulation model.
+"""
+function initfields!(model::SimulationModel)
+    n = 0
+    convertid = Dict{Int64,Int64}()
+    width, height = size(model.landscape)
+    for x in 1:width
+        for y in 1:height
+            # for each pixel, we need to extract the field ID given by the map input
+            # file, and convert it into the internal object ID used by Agents.jl,
+            # creating a new agent object if necessary
+            rawid = model.landscape[x,y].fieldid
+            (ismissing(rawid)) && continue
+            if rawid in keys(convertid)
+                objectid = convertid[rawid]
+                model.landscape[x,y].fieldid = objectid
+                push!(model.farmplots[objectid].pixels, (x,y))
+            else
+                cropstate = make_cropstate(model, @param(crop.cropmodel))
+                fp = FarmPlot(
+                    length(model.farmplots) + 1,
+                    [(x, y)],
+                    cropstate
+                )
+                push!(model.farmplots, fp)
+                model.landscape[x,y].fieldid = fp.id
+                convertid[rawid] = fp.id
+                n += 1
+            end
+        end
+    end
+    @info "Initialised $n farm plots."
+end
+
+# internal utility function
+function make_cropstate(model::SimulationModel, cropmodel::AbstractString)
+    if cropmodel == "almass"
+        phase = (month(model.date) < 3 ? ALMaSS.janfirst : ALMaSS.marchfirst)
+        cs = ALMaSS.CropState(
+                model.crops["natural grass"],
+                phase,
+                0.0, 0.0m, 0.0, 0.0, Vector{Management}()
+        )
+    elseif cropmodel == "simple"
+        cs = SimpleCrop.CropState(
+            model.crops["natural grass"],
+            0.0m
+        )
+    else
+        error("Unhandled crop model '$cropmodel' in make_cropstate")
+    end
+    return cs
+end
diff --git a/src/crop/crops.jl b/src/crop/crops.jl
deleted file mode 100644
index 996c6dabd0187189e6dd701e62669598003f4f63..0000000000000000000000000000000000000000
--- a/src/crop/crops.jl
+++ /dev/null
@@ -1,121 +0,0 @@
-### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe.
-###
-### This file is responsible for managing the crop growth modules.
-###
-
-#TODO write tests for input functions
-
-"""
-    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{Float64}}
-end
-
-"""
-    CropType
-
-The type struct for all crops. Currently follows the crop growth model as
-implemented in ALMaSS.
-"""
-struct CropType
-    #TODO make this into an abstract type and create subtypes for different
-    # crop submodels (#70)
-    name::String
-    minsowdate::Union{Missing,Date}
-    maxsowdate::Union{Missing,Date}
-    minharvestdate::Union{Missing,Date}
-    maxharvestdate::Union{Missing,Date}
-    mingrowthtemp::Union{Missing,Float64}
-    highnutrientgrowth::Union{Missing,CropCurveParams}
-    lownutrientgrowth::Union{Missing,CropCurveParams}
-    #issowable::Union{Function,Bool}
-end
-
-"""
-    Base.tryparse(type, str)
-
-Extend `tryparse` to allow parsing GrowthPhase values.
-(Needed to read in the CSV parameter file.)
-"""
-function Base.tryparse(type::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{Float64}(), sow=>Vector{Float64}(),
-                  marchfirst=>Vector{Float64}(), harvest1=>Vector{Float64}(),
-                  harvest2=>Vector{Float64}())
-    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)
-    end
-    CropCurveParams(data[1].curve_id, data[1].nutrient_status=="high",
-                    GDD, LAItotal, LAIgreen, height)
-end
-
-"""
-    readcropparameters(generalcropfile, cropgrowthfile)
-
-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(generalcropfile::String, growthfile::String)
-    @debug "Reading crop parameters"
-    cropdata = CSV.File(generalcropfile, missingstring="NA", dateformat="d U",
-                        types=[String,Date,Date,Date,Date,Float64])
-    growthdata = CSV.File(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.minsowdate, crop.maxsowdate,
-                                        crop.minharvestdate, crop.maxharvestdate,
-                                        crop.mingrowthtemp, highnuts, lownuts)
-    end
-    croptypes
-end
diff --git a/src/crop/farmplot.jl b/src/crop/farmplot.jl
index 359c0c00d53ac50f5614f8cd71f8f3a7faf5ffad..c03d2c2d97f65265d9cfbcbc2f1fb5ad0bf45f9e 100644
--- a/src/crop/farmplot.jl
+++ b/src/crop/farmplot.jl
@@ -3,155 +3,48 @@
 ### This file contains code for the fields that farmers manage.
 ###
 
-#XXX not sure whether it makes sense to have this as an agent type,
-# or perhaps better a grid property?
-"""
-    FarmPlot
-
-This represents one field, i.e. a collection of pixels with the same management.
-This is the spatial unit with which the crop growth model and the farm model work.
-"""
-mutable struct FarmPlot <: ModelAgent
-    #TODO add Unitful
+mutable struct FarmPlot{T} <: ModelAgent
     const id::Int64
     pixels::Vector{Tuple{Int64, Int64}}
-    croptype::CropType
-    phase::GrowthPhase
-    growingdegreedays::Float64
-    height::Float64
-    LAItotal::Float64
-    LAIgreen::Float64
-    #biomass::Float64 #XXX I need to figure out how to calculate this
-    events::Vector{Management}
+    cropstate :: T
 end
 
-"""
-    initfields!(model)
-
-Initialise the model with its farm plots.
-"""
-function initfields!(model::SimulationModel)
-    n = 0
-    convertid = Dict{Int64,Int64}()
-    width, height = size(model.landscape)
-    for x in 1:width
-        for y in 1:height
-            # for each pixel, we need to extract the field ID given by the map input
-            # file, and convert it into the internal object ID used by Agents.jl,
-            # creating a new agent object if necessary
-            rawid = model.landscape[x,y].fieldid
-            (ismissing(rawid)) && continue
-            if rawid in keys(convertid)
-                objectid = convertid[rawid]
-                model.landscape[x,y].fieldid = objectid
-                push!(model.farmplots[objectid].pixels, (x,y))
-            else
-                #XXX does this phase calculation work out?
-                month(model.date) < 3 ? phase = janfirst : phase = marchfirst
-                fp = FarmPlot(length(model.farmplots)+1, [(x,y)],
-                                model.crops["natural grass"], phase,
-                              0.0, 0.0, 0.0, 0.0, Vector{Management}())
-                push!(model.farmplots, fp)
-                model.landscape[x,y].fieldid = fp.id
-                convertid[rawid] = fp.id
-                n += 1
-            end
-        end
-    end
-    @info "Initialised $n farm plots."
-end
+croptype(f::FarmPlot{T}) where {T} = croptype(f.cropstate)
+cropname(f::FarmPlot{T}) where {T} = cropname(croptype(f))
+cropheight(f::FarmPlot{T}) where {T} = cropheight(f.cropstate)
+cropcover(f::FarmPlot{T}) where {T} = cropcover(f.cropstate)
+cropyield(f::FarmPlot{T}) where {T} = cropyield(f.cropstate)
 
 """
     stepagent!(farmplot, model)
 
 Update a farm plot by one day.
 """
-function stepagent!(farmplot::FarmPlot, model::SimulationModel)
-    # update growing degree days
-    # if no crop-specific base temperature is given, default to 5°C
-    # (https://www.eea.europa.eu/publications/europes-changing-climate-hazards-1/heat-and-cold/heat-and-cold-2014-mean)
-    basetemp = farmplot.croptype.mingrowthtemp
-    ismissing(basetemp) && (basetemp = 5.0)        
-    gdd = (maxtemp(model)+mintemp(model))/2 - basetemp
-    gdd > 0 && (farmplot.growingdegreedays += gdd)
-    # update the phase on key dates
-    monthday(model.date) == (1,1) && (farmplot.phase = janfirst)
-    monthday(model.date) == (3,1) && (farmplot.phase = marchfirst)
-    # update crop growth
-    growcrop!(farmplot, model)
+function stepagent!(farmplot::FarmPlot{T}, model::SimulationModel) where T
+    stepagent!(farmplot.cropstate, model)
 end
 
-
-## CROP MANAGEMENT AND GROWTH FUNCTIONS
-
 """
-    sow!(cropname, farmplot, model)
+    sow!(farmplot, model, cropname)
 
-Sow the specified crop on this farmplot.
+Sow the specified crop on the farmplot.
 """
-function sow!(cropname::String, farmplot::FarmPlot, model::SimulationModel)
-    createevent!(model, farmplot.pixels, sowing)
-    farmplot.croptype = model.crops[cropname]
-    farmplot.phase = sow
+function sow!(farmplot::FarmPlot, model::SimulationModel, cropname::String)
     #XXX test if the crop is sowable?
+    createevent!(model, farmplot.pixels, sowing)
+    sow!(farmplot.cropstate, model, cropname)
 end
 
 """
     harvest!(farmplot, model)
 
-Harvest the crop on this farmplot.
+Harvest the crop of this farmplot.
 """
-function harvest!(farmplot::FarmPlot, model::SimulationModel)
+function harvest!(farmplot::FarmPlot{T}, model::SimulationModel) where T
     createevent!(model, farmplot.pixels, harvesting)
-    farmplot.phase in [harvest1, harvest2] ?
-        farmplot.phase = harvest2 :
-        farmplot.phase = harvest1
-    # height & LAI will be automatically adjusted by the growth function
-    #TODO calculate and return yield
+    harvest!(farmplot.cropstate, model)  # TODO: multiply with area to return units of `g`
 end
 
-#TODO fertilise!()
-#TODO spray!()
-#TODO till!()
-
-"""
-    growcrop!(farmplot, model)
-
-Apply the relevant crop growth model to update the plants on this farm plot.
-Currently only supports the ALMaSS crop growth model by Topping et al.
-"""
-function growcrop!(farmplot::FarmPlot, model::SimulationModel)
-    fertiliser in farmplot.events ?
-        curve = farmplot.croptype.lownutrientgrowth :
-        curve = farmplot.croptype.highnutrientgrowth
-    points = curve.GDD[farmplot.phase]
-    for p in 1:length(points)
-        if points[p] == 99999
-            return # the marker that there is no further growth this phase
-        elseif points[p] == -1 # the marker to set all variables to specified values
-            farmplot.height = curve.height[farmplot.phase][p]
-            farmplot.LAItotal = curve.LAItotal[farmplot.phase][p]
-            farmplot.LAIgreen = curve.LAIgreen[farmplot.phase][p]
-            return
-        else
-            gdd = farmplot.growingdegreedays
-            # figure out which is the correct slope value to use for growth
-            if p == length(points) || gdd < points[p+1]
-                farmplot.height += curve.height[farmplot.phase][p]
-                farmplot.LAItotal += curve.LAItotal[farmplot.phase][p]
-                farmplot.LAIgreen += curve.LAIgreen[farmplot.phase][p]
-                return
-            end
-            #XXX To be precise, we ought to figure out if one or more inflection
-            # points have been passed between yesterday and today, and calculate the
-            # growth exactly up to the inflection point with the old slope, and onward
-            # with the new slope. Not doing so will introduce a small amount of error,
-            # although I think this is acceptable.
-        end
-    end
-end
-                 
-
 ## UTILITY FUNCTIONS
 
 """
@@ -175,7 +68,7 @@ Return the crop at this position, or nothing if there is no crop here (utility w
 """
 function croptype(pos::Tuple{Int64,Int64}, model::SimulationModel)
     ismissing(model.landscape[pos...].fieldid) ? nothing :
-              model.farmplots[model.landscape[pos...].fieldid].croptype
+              croptype(model.farmplots[model.landscape[pos...].fieldid])
 end
 
 """
@@ -186,29 +79,27 @@ Return the name of the crop at this position, or an empty string if there is no
 """
 function cropname(pos::Tuple{Int64,Int64}, model::SimulationModel)
     field = model.landscape[pos...].fieldid
-    ismissing(field) ? "" : model.farmplots[field].croptype.name
+    ismissing(field) ? "" : cropname(model.farmplots[field])
 end
 
 """
     cropheight(model, position)
 
-Return the height of the crop at this position, or 0 if there is no crop here
+Return the height of the crop at this position, or nothing if there is no crop here
 (utility wrapper).
 """
 function cropheight(pos::Tuple{Int64,Int64}, model::SimulationModel)
-    ismissing(model.landscape[pos...].fieldid) ? 0cm : #FIXME can I return something better than 0?
-              model.farmplots[model.landscape[pos...].fieldid].height*1cm #FIXME units
+    ismissing(model.landscape[pos...].fieldid) ? 0.0cm :  # TODO: 0.0cm correct here?
+              cropheight(model.farmplots[model.landscape[pos...].fieldid])
 end
 
 """
     cropcover(model, position)
 
-Return the percentage ground cover of the crop at this position, or 0 if there is no crop
-here (utility wrapper).
+Return the crop cover of the crop at this position, or nothing if
+there is no crop here (utility wrapper).
 """
 function cropcover(pos::Tuple{Int64,Int64}, model::SimulationModel)
-    #FIXME LAItotal != ground cover?
-    ismissing(model.landscape[pos...].fieldid) ? 0 : #FIXME can I return something better than 0?
-              model.farmplots[model.landscape[pos...].fieldid].LAItotal
+    ismissing(model.landscape[pos...].fieldid) ? 0.0 :  # TODO: 0.0 correct here?
+              cropcover(model.farmplots[model.landscape[pos...].fieldid])
 end
-
diff --git a/src/crop/simplecrop.jl b/src/crop/simplecrop.jl
new file mode 100644
index 0000000000000000000000000000000000000000..6bfe858364cf472af1a49b87cf0a8f455dfd5b8c
--- /dev/null
+++ b/src/crop/simplecrop.jl
@@ -0,0 +1,70 @@
+module SimpleCrop
+
+using Persefone:
+    FarmPlot,
+    Length,
+    cm,
+    SimulationModel
+
+import Persefone:
+    stepagent!,
+    croptype,
+    cropname,
+    cropheight,
+    cropcover,
+    cropyield
+
+struct CropType
+    name::String
+end
+
+cropname(ct::CropType) = ct.name
+
+mutable struct CropState
+    croptype::CropType
+    height::Length{Float64}
+end
+
+croptype(cs::CropState) = cs.croptype
+cropname(cs::CropState) = cropname(croptype(cs))
+cropheight(cs::CropState) = cs.height
+cropcover(cs::CropState) = 0.0
+cropyield(cs::CropState) = 0.0  # TODO: units?
+
+
+"""
+    stepagent!(farmplot, model)
+
+Update a crop state by one day.
+"""
+function stepagent!(cs::CropState, model::SimulationModel)
+    # height undergoes random diffusion, bounded by 0
+    delta_height = (2 * rand() - 1) * cm
+    cs.height = max(0.0cm, cs.height + delta_height)
+end
+
+"""
+    sow!(cropstate, model, cropname)
+
+Change the cropstate to sow the specified crop.
+"""
+function sow!(cs::CropState, model::SimulationModel, cropname::String)
+    cs.croptype = model.crops[cropname]
+    cs.height = 0cm
+end
+
+"""
+    harvest!(cropstate, model)
+
+Harvest the crop of this cropstate.
+"""
+function harvest!(cs::CropState, model::SimulationModel)
+    # TODO: set cs.croptype ?
+    # TODO: this 1.0g/cm/m^2 height_to_yield factor should be a param
+    #       for every crop type
+    yield = cs.height * 1.0g/cm/m^2
+    cs.height = 0cm
+    return yield
+end
+
+end # module SimpleCrop
diff --git a/src/farm/farm.jl b/src/farm/farm.jl
index 391b825997502a7fc290c91e4b5b036b753a0adc..6d7b1588f33e54cb67e14a5fe86f4153e4379c15 100644
--- a/src/farm/farm.jl
+++ b/src/farm/farm.jl
@@ -12,8 +12,9 @@ mutable struct Farmer <: ModelAgent
     #XXX make this into an abstract type and create subtypes for different
     # farm submodels? (#69)
     const id::Int64
-    fields::Vector{FarmPlot}
-    croprotation::Vector{CropType}
+    # TODO: hardcoded ALMaSS crop model
+    fields::Vector{FarmPlot{ALMaSS.CropState}}
+    croprotation::Vector{ALMaSS.CropType}
     #TODO add AES
 end
 
diff --git a/src/parameters.toml b/src/parameters.toml
index 2f2215b842291d7d0a5b65f6c7dc223cc7ee3299..659e8870ed158976062c91433ad7e06b69045427 100644
--- a/src/parameters.toml
+++ b/src/parameters.toml
@@ -40,7 +40,7 @@ indoutfreq = "end" # output frequency individual-level data, daily/monthly/yearl
 insectmodel = ["season", "habitat", "pesticides", "weather"] # factors affecting insect growth
 	
 [crop]
-cropmodel = "almass" # crop growth model to use, "almass" or "aquacrop"
+cropmodel = "almass" # crop growth model to use, "almass", "aquacrop", or "simple"
 cropfile = "data/crops/almass/crop_data_general.csv" # file with general crop parameters
 growthfile = "data/crops/almass/almass_crop_growth_curves.csv" # file with crop growth parameters	
 
diff --git a/test/crop_tests.jl b/test/crop_tests.jl
index 2ab0affb0eaad04c1a71e141c06c49e497d21185..91469250561ef844d1fd99969b6442701c8133e2 100644
--- a/test/crop_tests.jl
+++ b/test/crop_tests.jl
@@ -1,6 +1,45 @@
 ### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe.
 ###
-### These are the tests for the crop growth model.
+### These are the tests for the crop growth models.
 ###
 
-#TODO
+const TESTPARAM_ALMASS = joinpath(pkgdir(Persefone), "test", "test_parameters_almass.toml")
+const TESTPARAM_SIMPLECROP = joinpath(pkgdir(Persefone), "test", "test_parameters_simplecrop.toml")
+
+@testset for paramfile in (TESTPARAM_ALMASS, TESTPARAM_SIMPLECROP)
+    @testset "Model initialisation" begin
+        model = initialise(paramfile)
+        @test model isa AgricultureModel
+    end
+    @testset "Time step" begin
+        model = initialise(paramfile)
+        stepsimulation!(model)
+        @test model isa AgricultureModel
+    end
+end
+
+@testset "Submodule ALMaSS" begin
+    ct = Ps.ALMaSS.CropType("olive tree", missing, missing, missing, missing, missing,
+                            missing, missing)
+    fp = FarmPlot(0, [(0,0)],
+                  Ps.ALMaSS.CropState(ct, Ps.ALMaSS.janfirst, 0.0, 0.0m, 0.0, 0.0, Ps.Management[]))
+    @test fp isa FarmPlot
+    @test fp isa FarmPlot{Ps.ALMaSS.CropState}
+    @test croptype(fp) isa Ps.ALMaSS.CropType
+    @test cropname(fp) isa String
+    @test cropheight(fp) isa Length{Float64}
+    @test cropcover(fp) isa Float64
+    @test cropyield(fp) isa Float64
+end
+
+@testset "Submodule SimpleCrop" begin
+    ct = Ps.SimpleCrop.CropType("olive tree")
+    fp = FarmPlot(0, [(0,0)], Ps.SimpleCrop.CropState(ct, 0.0cm))
+    @test fp isa FarmPlot
+    @test fp isa FarmPlot{Ps.SimpleCrop.CropState}
+    @test croptype(fp) isa Ps.SimpleCrop.CropType
+    @test cropname(fp) isa String
+    @test cropheight(fp) isa Length{Float64}
+    @test cropcover(fp) isa Float64
+    @test cropyield(fp) isa Float64
+end
diff --git a/test/nature_tests.jl b/test/nature_tests.jl
index afeab5ea92b0168c134ade932923350d7ec47bef..a5ef3f91df4d2c9dea80527dc129fda8615a9f09 100644
--- a/test/nature_tests.jl
+++ b/test/nature_tests.jl
@@ -47,17 +47,22 @@ end) # end eval
 @testset "Habitat macros" begin
     # set up the testing landscape
     model = inittestmodel()
-    model.landscape[6,6] = Pixel(Ps.agriculture, 1)
-    push!(model.farmplots,
-          FarmPlot(1, [(6,6)], model.crops["winter wheat"], Ps.janfirst,
-                   0.0, 0.0, 0.0, 0.0, Vector{Ps.Management}()))
+    model.landscape[6,6] = Pixel(Ps.agriculture, 1, [], [], [])
+    fp = Ps.FarmPlot(
+        1, [(6,6)],
+        Ps.ALMaSS.CropState(
+            model.crops["winter wheat"], Ps.ALMaSS.janfirst,
+            0.0, 0.0m, 0.0, 0.0, Vector{Ps.Management}()
+        )
+    )
+    push!(model.farmplots, fp)
     push!(model.animals,
           Ps.Mermaid(1, Ps.male, (-1,-1), (3,3), Ps.life),
           Ps.Mermaid(2, Ps.female, (-1,-1), (4,4), Ps.life))
     # create a set of habitat descriptors
     h1 = @habitat(@landcover() == Ps.water)
     h2 = @habitat(@cropname() == "winter wheat" &&
-                  @cropheight() < 2) #XXX randomly chosen value
+                  @cropheight() < 2m) #XXX randomly chosen value
     h3 = @habitat(@distanceto(Ps.water) > 20m &&
                   @distancetoedge() <= 20m)
     #FIXME nested macros don't work properly, counting seems wrong
diff --git a/test/paramscan.toml b/test/paramscan.toml
index d2db42fb9add8f360c2247adb5cc3e65f8223054..565e6855f876dbc6474137cd22b0353c384485f8 100644
--- a/test/paramscan.toml
+++ b/test/paramscan.toml
@@ -28,5 +28,5 @@ popoutfreq = "daily" # output frequency population-level data, daily/monthly/yea
 indoutfreq = "end" # output frequency individual-level data, daily/monthly/yearly/end/never
 	
 [crop]
-cropmodel = "linear" # crop growth model to use, "linear" or "aquacrop" (not yet implemented)
+cropmodel = "simple" # crop growth model to use, "simple" or "almass"
 
diff --git a/test/runtests.jl b/test/runtests.jl
index b36a792bd36b7795f1efbe3a8603a2f5c41c63e0..4aaf78eb80ee893ba409eed6b396547f056721ab 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -43,22 +43,27 @@ function inittestmodel(smallmap=true)
                                       TESTSETTINGS["world.weatherfile"]),
                              TESTSETTINGS["core.startdate"],
                              TESTSETTINGS["core.enddate"])
-    crops = Ps.readcropparameters(TESTSETTINGS["crop.cropfile"],
-                                  TESTSETTINGS["crop.growthfile"])
-    model = AgricultureModel(TESTSETTINGS,
-                             StableRNG(TESTSETTINGS["core.seed"]),
-                             global_logger(),
-                             Vector{DataOutput}(),
-                             Dict{String, DataFrame}(),
-                             TESTSETTINGS["core.startdate"],
-                             landscape,
-                             weather,
-                             crops,
-                             Vector{Farmer}(),
-                             Vector{FarmPlot}(),
-                             Vector{Union{Animal,Nothing}}(),
-                             Vector{Pair{Animal, Date}}(),
-                             Vector{FarmEvent}())
+    # TODO: support other crop models besides ALMaSS
+    crops = Ps.ALMaSS.readcropparameters(TESTSETTINGS["crop.cropfile"],
+                                         TESTSETTINGS["crop.growthfile"])
+    farmers = Vector{Farmer}()
+    farmplots = Vector{Ps.FarmPlot{Ps.ALMaSS.CropState}}()
+    model = AgricultureModel{Ps.ALMaSS.CropType,Ps.ALMaSS.CropState}(
+        TESTSETTINGS,
+        StableRNG(TESTSETTINGS["core.seed"]),
+        global_logger(),
+        Vector{DataOutput}(),
+        Dict{String, DataFrame}(),
+        TESTSETTINGS["core.startdate"],
+        landscape,
+        weather,
+        crops,
+        farmers,
+        farmplots,
+        Vector{Union{Animal,Nothing}}(),
+        Vector{Pair{Animal, Date}}(),
+        Vector{FarmEvent}()
+    )
     model
 end    
 
diff --git a/test/test_parameters.toml b/test/test_parameters.toml
index 4d88eb3bf0deeee889c106ce4ff90329e1706a89..94d6a9a8b25be61910d3a7d690079d2405256295 100644
--- a/test/test_parameters.toml
+++ b/test/test_parameters.toml
@@ -38,7 +38,7 @@ indoutfreq = "daily" # output frequency individual-level data, daily/monthly/yea
 insectmodel = ["season", "habitat", "pesticides"] # which factors affect insect growth ("weather" is not yet implemented)
 	
 [crop]
-cropmodel = "almass" # crop growth model to use, "almass" or "aquacrop"
+cropmodel = "almass" # crop growth model to use, "almass", "aquacrop", or "simple"
 cropfile = "crop_data_general.csv" # file with general crop parameters
 growthfile = "almass_crop_growth_curves.csv" # file with crop growth parameters
 
diff --git a/test/test_parameters_almass.toml b/test/test_parameters_almass.toml
new file mode 100644
index 0000000000000000000000000000000000000000..1308029ee9b7b387064752b0c0e3a70174798c77
--- /dev/null
+++ b/test/test_parameters_almass.toml
@@ -0,0 +1,44 @@
+### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe.
+###
+### This configuration file is used for the test suite.
+###
+
+#XXX remember that changes here may break tests!
+
+[core]
+configfile = "test_parameters.toml" # location of the configuration file
+outdir = "results_testsuite" # location and name of the output folder
+logoutput = "both"
+overwrite = true # overwrite the output directory? (true/false/"ask")
+csvoutput = true # save collected data in CSV files
+visualise = true # generate result graphs
+storedata = true # keep collected data in memory
+loglevel = "warn" # verbosity level: "debug", "info", "warn"
+processors = 6 # number of processors to use on parallel runs
+seed = 1 # seed value for the RNG (0 -> random value)
+# dates to start and end the simulation
+startdate = 2022-02-01
+enddate = 2022-03-31
+
+[world]
+mapdirectory = "." # the directory in which all geographic data are stored
+mapresolution = 10 # map resolution in meters
+landcovermap = "landcover_jena.tif" # location of the landcover map
+farmfieldsmap = "fields_jena.tif" # location of the field geometry map
+weatherfile = "weather_jena.csv" # location of the weather data file
+
+[farm]
+farmmodel = "FieldManager" # which version of the farm model to use (not yet implemented)
+
+[nature]
+targetspecies = ["Wolpertinger", "Wyvern"] # list of target species to simulate - example species
+#targetspecies = ["Skylark"] # list of target species to simulate
+popoutfreq = "daily" # output frequency population-level data, daily/monthly/yearly/end/never
+indoutfreq = "daily" # output frequency individual-level data, daily/monthly/yearly/end/never
+insectmodel = ["season", "habitat", "pesticides"] # which factors affect insect growth ("weather" is not yet implemented)
+
+[crop]
+cropmodel = "almass" # crop growth model to use, "almass", "aquacrop", or "simple"
+cropfile = "crop_data_general.csv" # file with general crop parameters
+growthfile = "almass_crop_growth_curves.csv" # file with crop growth parameters
+
diff --git a/test/test_parameters_simplecrop.toml b/test/test_parameters_simplecrop.toml
new file mode 100644
index 0000000000000000000000000000000000000000..fc199a8c8664b0d900e131d9b5b159dc006ee502
--- /dev/null
+++ b/test/test_parameters_simplecrop.toml
@@ -0,0 +1,45 @@
+### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe.
+###
+### This configuration file is used for the test suite.
+###
+
+#XXX remember that changes here may break tests!
+
+[core]
+configfile = "test_parameters.toml" # location of the configuration file
+outdir = "results_testsuite" # location and name of the output folder
+logoutput = "both"
+overwrite = true # overwrite the output directory? (true/false/"ask")
+csvoutput = true # save collected data in CSV files
+visualise = true # generate result graphs
+storedata = true # keep collected data in memory
+loglevel = "warn" # verbosity level: "debug", "info", "warn"
+processors = 6 # number of processors to use on parallel runs
+seed = 1 # seed value for the RNG (0 -> random value)
+# dates to start and end the simulation
+startdate = 2022-02-01
+enddate = 2022-03-31
+
+[world]
+mapdirectory = "." # the directory in which all geographic data are stored
+mapresolution = 10 # map resolution in meters
+landcovermap = "landcover_jena.tif" # location of the landcover map
+farmfieldsmap = "fields_jena.tif" # location of the field geometry map
+weatherfile = "weather_jena.csv" # location of the weather data file
+
+[farm]
+farmmodel = "FieldManager" # which version of the farm model to use (not yet implemented)
+
+[nature]
+targetspecies = ["Wolpertinger", "Wyvern"] # list of target species to simulate - example species
+#targetspecies = ["Skylark"] # list of target species to simulate
+popoutfreq = "daily" # output frequency population-level data, daily/monthly/yearly/end/never
+indoutfreq = "daily" # output frequency individual-level data, daily/monthly/yearly/end/never
+insectmodel = ["season", "habitat", "pesticides"] # which factors affect insect growth ("weather" is not yet implemented)
+
+[crop]
+cropmodel = "simple" # crop growth model to use, "almass", "aquacrop", or "simple"
+cropfile = "crop_data_general.csv" # file with general crop parameters
+# TODO: this parameter should be unnecessary, but removing it
+# currently gives an error
+growthfile = "almass_crop_growth_curves.csv" # file with crop growth parameters