From 53b9b8619ba7911ac4a02b0e50f072368622c752 Mon Sep 17 00:00:00 2001
From: Daniel Vedder <daniel.vedder@idiv.de>
Date: Thu, 8 Aug 2024 16:01:33 +0200
Subject: [PATCH] Started working on farm model

---
 src/Persefone.jl       |  2 ++
 src/crop/almass.jl     |  6 ++++--
 src/crop/cropmodels.jl | 29 +++++++++++++----------------
 src/crop/farmplot.jl   | 19 +++++++++++++++----
 src/crop/simplecrop.jl |  2 +-
 src/farm/farm.jl       | 18 +++++++++++-------
 6 files changed, 46 insertions(+), 30 deletions(-)

diff --git a/src/Persefone.jl b/src/Persefone.jl
index 9660688..bff3e22 100644
--- a/src/Persefone.jl
+++ b/src/Persefone.jl
@@ -95,6 +95,8 @@ export
     @walk,
     @follow,
     @destroynest,
+    @sow,
+    @harvest,
     #functions
     simulate,
     simulate!,
diff --git a/src/crop/almass.jl b/src/crop/almass.jl
index 5ac4c53..4e5bfe7 100644
--- a/src/crop/almass.jl
+++ b/src/crop/almass.jl
@@ -58,6 +58,7 @@ implemented in ALMaSS.
 """
 struct CropType
     name::String
+    group::String
     minsowdate::Union{Missing,Date}
     maxsowdate::Union{Missing,Date}
     minharvestdate::Union{Missing,Date}
@@ -93,6 +94,7 @@ 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?
+isharvestable(cs::CropState) = false #FIXME how do we do this?
 
 """
     Base.tryparse(type, str)
@@ -147,7 +149,7 @@ Parse a CSV file containing the required parameter values for each crop
 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])
+                        types=[String,Date,Date,Date,Date,Float64,String])
     growthdata = CSV.File(growthfile, missingstring="NA",
                           types=[Int,String,String,GrowthPhase,String,
                                  Float64,Float64,Float64,Float64])
@@ -159,7 +161,7 @@ function readcropparameters(generalcropfile::String, growthfile::String)
                                     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,
+        croptypes[crop.name] = CropType(crop.name, crop.group, crop.minsowdate, crop.maxsowdate,
                                         crop.minharvestdate, crop.maxharvestdate,
                                         crop.mingrowthtemp, highnuts, lownuts)
     end
diff --git a/src/crop/cropmodels.jl b/src/crop/cropmodels.jl
index bbbd925..d947254 100644
--- a/src/crop/cropmodels.jl
+++ b/src/crop/cropmodels.jl
@@ -31,14 +31,12 @@ end
 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
+            # file, and convert it into an internal object ID for `model.farmplots`
             rawid = model.landscape[x,y].fieldid
             (ismissing(rawid)) && continue
             if rawid in keys(convertid)
@@ -46,38 +44,37 @@ function initfields!(model::SimulationModel)
                 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
-                )
+                cropstate = makecropstate(model)
+                fp = FarmPlot(length(model.farmplots) + 1, [(x, y)], -1, 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."
+    @info "Initialised $(length(model.farmplots)) farm plots."
 end
 
-# internal utility function
-function make_cropstate(model::SimulationModel, cropmodel::AbstractString)
-    if cropmodel == "almass"
+"""
+    makecropstate(model, cropmodel)
+
+An internal utility function to initialise one instance of the configured crop growth model.
+"""
+function makecropstate(model::SimulationModel)
+    if @param(crop.cropmodel) == "almass"
         phase = (month(model.date) < 3 ? ALMaSS.janfirst : ALMaSS.marchfirst)
         cs = ALMaSS.CropState(
                 model.crops["natural grass"],
                 phase,
                 0.0, 0.0m, 0.0, 0.0, Vector{Management}()
         )
-    elseif cropmodel == "simple"
+    elseif @param(crop.cropmodel) == "simple"
         cs = SimpleCrop.CropState(
             model.crops["natural grass"],
             0.0m
         )
     else
-        error("Unhandled crop model '$cropmodel' in make_cropstate")
+        Base.error("Unhandled crop model '$(@param(crop.cropmodel))' in makecropstate().")
     end
     return cs
 end
diff --git a/src/crop/farmplot.jl b/src/crop/farmplot.jl
index ebe7c84..35061b3 100644
--- a/src/crop/farmplot.jl
+++ b/src/crop/farmplot.jl
@@ -3,10 +3,17 @@
 ### This file contains code for the fields that farmers manage.
 ###
 
+"""
+    FarmPlot
+
+A struct representing a single field, on which a crop can be grown.
+"""
 mutable struct FarmPlot{T} <: ModelAgent
     const id::Int64
     pixels::Vector{Tuple{Int64, Int64}}
-    cropstate :: T
+    farmer::Int64
+    croprotation::Vector{String}
+    cropstate::T
 end
 
 croptype(f::FarmPlot{T}) where {T} = croptype(f.cropstate)
@@ -14,6 +21,7 @@ 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)
 
 """
     stepagent!(farmplot, model)
@@ -47,18 +55,21 @@ end
 
 ## UTILITY FUNCTIONS
 
+function isgrassland(farmplot::FarmPlot, model::SimulationModel)
+    #TODO
+end
+
 """
     averagefieldsize(model)
 
 Calculate the average field size in hectares for the model landscape.
 """
 function averagefieldsize(model::SimulationModel)
-    conversionfactor = 100 #our pixels are currently 10x10m, so 100 pixels per hectare
     sizes::Vector{Float64} = []
     for fp in model.farmplots
-        push!(sizes, size(fp.pixels)[1]/conversionfactor)
+        push!(sizes, length(fp.pixels)*@param(world.mapresolution)^2)
     end
-    round(sum(sizes)/size(sizes)[1], digits=2)
+    return sum(sizes)/length(sizes) |> ha
 end
 
 """
diff --git a/src/crop/simplecrop.jl b/src/crop/simplecrop.jl
index 6bfe858..a8aac2d 100644
--- a/src/crop/simplecrop.jl
+++ b/src/crop/simplecrop.jl
@@ -30,7 +30,7 @@ cropname(cs::CropState) = cropname(croptype(cs))
 cropheight(cs::CropState) = cs.height
 cropcover(cs::CropState) = 0.0
 cropyield(cs::CropState) = 0.0  # TODO: units?
-
+isharvestable(cs::CropState) = true
 
 """
     stepagent!(farmplot, model)
diff --git a/src/farm/farm.jl b/src/farm/farm.jl
index 6d7b158..29390ed 100644
--- a/src/farm/farm.jl
+++ b/src/farm/farm.jl
@@ -1,7 +1,10 @@
 ### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe.
 ###
 ### This file is responsible for managing the farm module(s).
-###
+###    
+
+###XXXXXXX In future, I want to expand Persefone into a proper ABM with multiple farm actors.
+###XXXXXXX However, at the moment, all fields are controlled centrally -> see farm/farm.jl
 
 """
     Farmer
@@ -9,13 +12,10 @@
 This is the agent type for the farm ABM. (Not yet implemented.)
 """
 mutable struct Farmer <: ModelAgent
-    #XXX make this into an abstract type and create subtypes for different
-    # farm submodels? (#69)
+    #XXX make this into an abstract type and create subtypes for different farm submodels? (#69)
     const id::Int64
-    # TODO: hardcoded ALMaSS crop model
-    fields::Vector{FarmPlot{ALMaSS.CropState}}
-    croprotation::Vector{ALMaSS.CropType}
-    #TODO add AES
+    fields::Vector{Int64} # IDs of the farmplots this farmer owns
+    totalincome::Float64
 end
 
 """
@@ -25,6 +25,10 @@ Update a farmer by one day.
 """
 function stepagent!(farmer::Farmer, model::SimulationModel)
     #TODO
+    # - check each field, whether it can be harvested
+    # - if so, harvest it and set its crop to "no growth"
+    # - [later: calculate income based on yield and annual price]
+    # - if a field has been harvested, check if the next crop can be sown
 end
 
 """
-- 
GitLab