From 1439b33c473c5c2a4083a6d27f2e24c65cdc5042 Mon Sep 17 00:00:00 2001
From: Marco Matthies <71844+marcom@users.noreply.github.com>
Date: Tue, 25 Feb 2025 14:04:33 +0100
Subject: [PATCH] Prepare for allowing multiple crop models in one simulation

- each crop model must implement types `CropType <: AbstractCropType`
  and `CropState <: AbstractCropState`

- `AgricultureModel` and `FarmPlot` are not parametrised structs
  anymore to allow for different crop models active in one simulation
---
 src/Persefone.jl       | 16 ++++++++++++++++
 src/core/simulation.jl | 12 ++++++------
 src/crop/almass.jl     |  6 ++++--
 src/crop/aquacrop.jl   |  6 ++++--
 src/crop/cropmodels.jl |  8 +-------
 src/crop/farmplot.jl   | 20 ++++++++++----------
 src/crop/simplecrop.jl |  6 ++++--
 test/crop_tests.jl     |  6 +++---
 test/runtests.jl       |  2 +-
 9 files changed, 49 insertions(+), 33 deletions(-)

diff --git a/src/Persefone.jl b/src/Persefone.jl
index 6a824d1..d6b7ccb 100644
--- a/src/Persefone.jl
+++ b/src/Persefone.jl
@@ -132,6 +132,22 @@ The supertype of all agents in the model (animal species, farmer types, farmplot
 """
 abstract type ModelAgent end
 
+"""
+    AbstractCropType
+
+The abstract supertype of all crop types in the model.  Each crop
+model has to define a type `CropType <: AbstractCropType`.
+"""
+abstract type AbstractCropType end
+
+"""
+    AbstractCropState
+
+The abstract supertype of all crop states in the model.  Each crop
+model has to define a type `CropState <: AbstractCropState`.
+"""
+abstract type AbstractCropState end
+
 function stepagent! end
 
 ## include all module files (note that the order matters - if file
diff --git a/src/core/simulation.jl b/src/core/simulation.jl
index 5c3628e..667f766 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.
 """
-@kwdef mutable struct AgricultureModel{Tcroptype, Tcropstate} <: SimulationModel
+@kwdef mutable struct AgricultureModel <: SimulationModel
     settings = Dict{String, Any}()
     rng::AbstractRNG
     logger::AbstractLogger
@@ -20,9 +20,9 @@ as input to most model functions.
     date::Date
     landscape = Matrix{Pixel}()
     weather::Weather
-    crops = Dict{String, Tcroptype}()
+    crops = Dict{String, AbstractCropType}()
     farmers = Vector{Farmer}()
-    farmplots = Vector{FarmPlot{Tcropstate}}()
+    farmplots = Vector{FarmPlot}()
     animals = Vector{Union{Animal, Nothing}}()
     migrants = Vector{Pair{Animal, AnnualDate}}()
     events = Vector{FarmEvent}()
@@ -140,9 +140,9 @@ function initmodel(settings::Dict{String, Any})
                                        settings["world.weatherfile"]),
                               settings["core.startdate"],
                               settings["core.enddate"])
-        crops, Tcroptype, Tcropstate = initcropmodel(settings["crop.cropmodel"],
-                                                     settings["crop.cropdirectory"])
-        model = AgricultureModel{Tcroptype, Tcropstate}(;
+        crops = initcropmodel(settings["crop.cropmodel"],
+                              settings["crop.cropdirectory"])
+        model = AgricultureModel(;
             settings,
             rng = StableRNG(settings["core.seed"]),
             logger,
diff --git a/src/crop/almass.jl b/src/crop/almass.jl
index 8edc318..a2dada4 100644
--- a/src/crop/almass.jl
+++ b/src/crop/almass.jl
@@ -43,6 +43,8 @@ const GROWTHFILE = "crop_growth_curves.csv"
 using Persefone:
     @rand,
     @u_str,
+    AbstractCropState,
+    AbstractCropType,
     AnnualDate,
     Management,
     cm,
@@ -116,7 +118,7 @@ end
 The type struct for all crops. Currently follows the crop growth model as
 implemented in ALMaSS.
 """
-struct CropType
+struct CropType <: AbstractCropType
     #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
@@ -155,7 +157,7 @@ cropname(ct::CropType) = ct.name
 The state data for an ALMaSS vegetation point calculation.  Usually
 part of a `FarmPlot`.
 """
-@kwdef mutable struct CropState
+@kwdef mutable struct CropState <: AbstractCropState
     # Class in original ALMaSS code:
     #     `VegElement` from `Landscape/Elements.h`, line 601
     croptype::CropType
diff --git a/src/crop/aquacrop.jl b/src/crop/aquacrop.jl
index 205078d..975dd5a 100644
--- a/src/crop/aquacrop.jl
+++ b/src/crop/aquacrop.jl
@@ -10,6 +10,8 @@ using Dates: Date
 using DataFrames: DataFrame
 
 using Persefone:
+    AbstractCropState,
+    AbstractCropType,
     AnnualDate,
     cm,
     daynumber,
@@ -68,7 +70,7 @@ const AQUACROP_CROPNAMES = [
     "wheatGDD",
 ]
 
-@kwdef struct CropType
+@kwdef struct CropType <: AbstractCropType
     name::String
     aquacrop_name::String
     group::String
@@ -101,7 +103,7 @@ function readcropparameters(cropdirectory::String)
     return croptypes
 end
 
-mutable struct CropState
+mutable struct CropState <: AbstractCropState
     croptype::CropType
     height::Tlength  # TODO: remove height field, supply from cropstate
     soiltype::SoilType
diff --git a/src/crop/cropmodels.jl b/src/crop/cropmodels.jl
index 715e74b..5571dbd 100644
--- a/src/crop/cropmodels.jl
+++ b/src/crop/cropmodels.jl
@@ -13,23 +13,17 @@ simulation, as well as the types `Tcroptype` and `Tcropstate`.
 """
 function initcropmodel(cropmodel::AbstractString, cropdirectory::AbstractString)
     if cropmodel == "almass"
-        Tcroptype = ALMaSS.CropType
-        Tcropstate = ALMaSS.CropState
         crops = ALMaSS.readcropparameters(cropdirectory)
     elseif cropmodel == "simple"
-        Tcroptype = SimpleCrop.CropType
-        Tcropstate = SimpleCrop.CropState
         crops_almass = ALMaSS.readcropparameters(cropdirectory)
         crops = Dict(name => SimpleCrop.CropType(ct.name, ct.group, ct.minsowdate, ct.maxsowdate)
                      for (name, ct) in crops_almass)
     elseif cropmodel == "aquacrop"
-        Tcroptype = AquaCropWrapper.CropType
-        Tcropstate = AquaCropWrapper.CropState
         crops = AquaCropWrapper.readcropparameters(cropdirectory)
     else
         error("initcropmodel: no implementation for crop model '$cropmodel'")
     end
-    return crops, Tcroptype, Tcropstate
+    return crops
 end
 
 """
diff --git a/src/crop/farmplot.jl b/src/crop/farmplot.jl
index f676276..810bbdf 100644
--- a/src/crop/farmplot.jl
+++ b/src/crop/farmplot.jl
@@ -8,27 +8,27 @@
 
 A struct representing a single field, on which a crop can be grown.
 """
-mutable struct FarmPlot{T} <: ModelAgent
+mutable struct FarmPlot <: ModelAgent
     const id::Int64
     pixels::Vector{Tuple{Int64, Int64}}
     farmer::Int64
     soiltype::SoilType
-    cropstate::T
+    cropstate::AbstractCropState
 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)
-isharvestable(f::FarmPlot{T}) where {T} = isharvestable(f.cropstate)
+croptype(f::FarmPlot) = croptype(f.cropstate)
+cropname(f::FarmPlot) = cropname(croptype(f))
+cropheight(f::FarmPlot) = cropheight(f.cropstate)
+cropcover(f::FarmPlot) = cropcover(f.cropstate)
+cropyield(f::FarmPlot) = cropyield(f.cropstate)
+isharvestable(f::FarmPlot) = isharvestable(f.cropstate)
 
 """
     stepagent!(farmplot, model)
 
 Update a farm plot by one day.
 """
-function stepagent!(farmplot::FarmPlot{T}, model::SimulationModel) where T
+function stepagent!(farmplot::FarmPlot, model::SimulationModel)
     stepagent!(farmplot.cropstate, model)
 end
 
@@ -48,7 +48,7 @@ end
 
 Harvest the crop of this farmplot.
 """
-function harvest!(farmplot::FarmPlot{T}, model::SimulationModel) where T
+function harvest!(farmplot::FarmPlot, model::SimulationModel)
     createevent!(model, farmplot.pixels, harvesting)
     harvest!(farmplot.cropstate, model)  # TODO: multiply with area to return units of `g`
     @debug "Farmer $(farmplot.farmer) harvested $(cropname(farmplot)) from farmplot $(farmplot.id)."
diff --git a/src/crop/simplecrop.jl b/src/crop/simplecrop.jl
index bc4f11d..32dbad8 100644
--- a/src/crop/simplecrop.jl
+++ b/src/crop/simplecrop.jl
@@ -1,6 +1,8 @@
 module SimpleCrop
 
 using Persefone:
+    AbstractCropState,
+    AbstractCropType,
     AnnualDate,
     FarmPlot,
     Length,
@@ -21,7 +23,7 @@ import Persefone:
 using Unitful: @u_str
 
 # TODO: alternatively just use ALMaSS.CropType ?
-struct CropType
+struct CropType <: AbstractCropType
     name::String
     group::String
     minsowdate::Union{Missing,AnnualDate}
@@ -30,7 +32,7 @@ end
 
 cropname(ct::CropType) = ct.name
 
-mutable struct CropState
+mutable struct CropState <: AbstractCropState
     croptype::CropType
     height::Length{Float64}
 end
diff --git a/test/crop_tests.jl b/test/crop_tests.jl
index e80ed59..8b3a0d3 100644
--- a/test/crop_tests.jl
+++ b/test/crop_tests.jl
@@ -33,7 +33,7 @@ end
     force_growth = false
     fp = FarmPlot(id, pixels, farmer, Ps.nosoildata, Ps.ALMaSS.CropState(croptype=ct, phase=Ps.ALMaSS.janfirst))
     @test fp isa FarmPlot
-    @test fp isa FarmPlot{Ps.ALMaSS.CropState}
+    @test fp.cropstate isa Ps.ALMaSS.CropState
     @test croptype(fp) isa Ps.ALMaSS.CropType
     @test cropname(fp) isa String
     @test cropheight(fp) isa Length{Float64}
@@ -48,7 +48,7 @@ end
     farmer = 0
     fp = FarmPlot(id, pixels, farmer, Ps.nosoildata, Ps.SimpleCrop.CropState(ct, 0.0cm))
     @test fp isa FarmPlot
-    @test fp isa FarmPlot{Ps.SimpleCrop.CropState}
+    @test fp.cropstate isa Ps.SimpleCrop.CropState
     @test croptype(fp) isa Ps.SimpleCrop.CropType
     @test cropname(fp) isa String
     @test cropheight(fp) isa Length{Float64}
@@ -67,7 +67,7 @@ end
     soiltype = Ps.sand
     fp = FarmPlot(id, pixels, farmer, soiltype, PsAC.CropState(ct, soiltype, model))
     @test fp isa FarmPlot
-    @test fp isa FarmPlot{PsAC.CropState}
+    @test fp.cropstate isa PsAC.CropState
     @test croptype(fp) isa PsAC.CropType
     @test cropname(fp) isa String
     @test cropheight(fp) isa Length{Float64}
diff --git a/test/runtests.jl b/test/runtests.jl
index 906be63..3ca4364 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -46,7 +46,7 @@ function inittestmodel(smallmap=true)
                              TESTSETTINGS["core.enddate"])
     # TODO: support other crop models besides ALMaSS
     crops = Ps.ALMaSS.readcropparameters(TESTSETTINGS["crop.cropdirectory"])
-    model = AgricultureModel{Ps.ALMaSS.CropType,Ps.ALMaSS.CropState}(;
+    model = AgricultureModel(;
         settings = copy(TESTSETTINGS),
         rng = StableRNG(TESTSETTINGS["core.seed"]),
         logger = global_logger(),
-- 
GitLab