From d7996cb04c485f6cce46c98294759ac88da53dfe Mon Sep 17 00:00:00 2001
From: Daniel Vedder <daniel.vedder@idiv.de>
Date: Mon, 17 Jul 2023 19:12:34 +0200
Subject: [PATCH] Started working on the insect biomass submodel.

---
 src/Persefone.jl       |  2 ++
 src/core/simulation.jl |  1 +
 src/nature/insects.jl  | 75 ++++++++++++++++++++++++++++++++++++++++++
 src/parameters.toml    |  1 +
 src/world/landscape.jl |  3 +-
 test/nature_tests.jl   |  2 +-
 test/runtests.jl       |  4 +--
 7 files changed, 84 insertions(+), 4 deletions(-)
 create mode 100644 src/nature/insects.jl

diff --git a/src/Persefone.jl b/src/Persefone.jl
index 65c3b29..509f728 100644
--- a/src/Persefone.jl
+++ b/src/Persefone.jl
@@ -31,6 +31,7 @@ using
 # PackageCompiler, https://julialang.github.io/PackageCompiler.jl/stable/
 # SpatialEcology, https://github.com/EcoJulia/SpatialEcology.jl
 # OmniScape, https://circuitscape.org/
+# PlantSimEngine, https://virtualplantlab.github.io/PlantSimEngine.jl/stable/
 
 ## define exported functions and variables
 export
@@ -82,6 +83,7 @@ include("farm/farm.jl")
 include("crop/crops.jl")
 
 include("nature/nature.jl")
+include("nature/insects.jl")
 include("nature/populations.jl")
 include("nature/ecologicaldata.jl")
 #TODO loop over files in nature/species directory
diff --git a/src/core/simulation.jl b/src/core/simulation.jl
index 18c0dd0..96a9f92 100644
--- a/src/core/simulation.jl
+++ b/src/core/simulation.jl
@@ -118,6 +118,7 @@ Execute one update of the model.
 function stepsimulation!(model::AgentBasedModel)
     with_logger(model.logger) do
         @info "Simulating day $(model.date)."
+        updateinsectbiomass!(model)
         for a in Schedulers.ByType((Farmer,FarmPlot,Animal), true)(model)
             try #The animal may have been killed
                 stepagent!(model[a], model)
diff --git a/src/nature/insects.jl b/src/nature/insects.jl
new file mode 100644
index 0000000..7838492
--- /dev/null
+++ b/src/nature/insects.jl
@@ -0,0 +1,75 @@
+### Persefone - a socio-economic-ecological model of European agricultural landscapes.
+###
+### This file contains the functions for calculating insect biomass.
+###
+
+##FIXME Updating every pixel every day is really expensive! I should seriously
+## think about switching this to a "calculate-on-demand" design.
+## (The problem is that that works for all models except the pesticide one, which
+## is a Markov Chain model that cannot be calculated without knowing the previous
+## value at the location.)
+
+##TODO write tests for this
+
+"""
+    updateinsectbiomass!(model)
+
+Calculate a new insect biomass value for each pixel in the landscape.
+"""
+function updateinsectbiomass!(model::AgentBasedModel)
+    @debug "Updating insect biomass"
+    if @param(nature.insectmodel) == "seasonal"
+        updatefunction = seasonalmodel
+    elseif @param(nature.insectmodel) == "seasonalhabitat"
+        updatefunction = seasonalhabitatmodel
+    else
+        Base.error("Unknown insect model $(@param(nature.insectmodel)).")
+        #TODO replace with exception
+    end
+    for p in model.landscape
+        p.insectbiomass = updatefunction(p, model)
+    end
+end
+
+
+"""
+    seasonalmodel(pixel)
+
+Calculate an insect biomass value based purely on the calendar day, following
+a parabolic function as per Paquette et al. (2013). Assumes a maximum insect
+density of 300 mg/m² (cf. Odderskaer et al. 1997, Püttmanns et al. 2022).
+
+*[Note that this is a very approximate calculation!]*
+"""
+function seasonalmodel(pixel::Pixel, model::AgentBasedModel)
+    calendarday = dayofyear(model.date)
+    biomass = -0.085(calendarday-187)^2+300
+    biomass > 0 ? biomass : 0
+end
+
+"""
+    seasonalhabitatmodel(pixel)
+
+Calculate an insect biomass value based on the calendar day and habitat type,
+following a parabolic function.
+
+See Odderskaer et al. (1997), Grübler et al. (2008), Paquette et al. (2013),
+and Püttmanns et al. (2022).
+
+*[Note that this is a very approximate calculation!]*
+"""
+function seasonalhabitatmodel(pixel::Pixel, model::AgentBasedModel)
+    calendarday = dayofyear(model.date)
+    habitatfactor = 0.0
+    if pixel.landcover == soil || pixel.landcover == forest
+        habitatfactor = 2.0
+    elseif pixel.landcover == grass
+        habitatfactor = 1.5
+    elseif pixel.landcover == agriculture
+        habitatfactor = 1.0
+    end
+    biomass = -0.085(calendarday-187)^2+300*habitatfactor
+    biomass > 0 ? biomass : 0
+end
+
+#TODO add models that include weather and pesticide application
diff --git a/src/parameters.toml b/src/parameters.toml
index 4e888a4..0838a3f 100644
--- a/src/parameters.toml
+++ b/src/parameters.toml
@@ -29,6 +29,7 @@ farmmodel = "FieldManager" # which version of the farm model to use (not yet imp
 targetspecies = ["Wolpertinger", "Wyvern"] # list of target species to simulate
 popoutfreq = "daily" # output frequency population-level data, daily/monthly/yearly/end/never
 indoutfreq = "end" # output frequency individual-level data, daily/monthly/yearly/end/never
+insectmodel = "seasonalhabitat" # how to calculate insect biomass, seasonal/seasonalhabitat/full
 	
 [crop]
 cropmodel = "linear" # crop growth model to use, "linear" or "aquacrop" (not yet implemented)
diff --git a/src/world/landscape.jl b/src/world/landscape.jl
index d6b16f6..e3dcd15 100644
--- a/src/world/landscape.jl
+++ b/src/world/landscape.jl
@@ -21,6 +21,7 @@ mutable struct Pixel
     landcover::LandCover
     fieldid::Union{Missing,Int64}
     events::Vector{EventType}
+    insectbiomass::Float64 # in mg/m²
 end
 
 """
@@ -57,7 +58,7 @@ function initlandscape(landcovermap::String, farmfieldsmap::String)
             lcv = LandCover(Int(lc/10))
             ff = farmfields[x,y][1]
             !(ismissing(ff)) && (ff = Int64(ff))
-            landscape[x,y] = Pixel(lcv, ff, Vector{Symbol}())
+            landscape[x,y] = Pixel(lcv, ff, Vector{Symbol}(), 0.0)
         end
     end
     return landscape
diff --git a/test/nature_tests.jl b/test/nature_tests.jl
index b357d7b..2f483f9 100644
--- a/test/nature_tests.jl
+++ b/test/nature_tests.jl
@@ -6,7 +6,7 @@
 @testset "Habitat macros" begin
     # set up the testing landscape
     model = smalltestlandscape()
-    model.landscape[6,6] = Pixel(Ps.agriculture, 1, [])
+    model.landscape[6,6] = Pixel(Ps.agriculture, 1, [], 0.0)
     species::Dict{String,Any} = Dict("name"=>"test_animal")
     add_agent!((6,6), FarmPlot, model, [(6,6)], Ps.wheat, 1.2, 3.4)
     add_agent!((3,3), Animal, model, species, Ps.male, 1)
diff --git a/test/runtests.jl b/test/runtests.jl
index a1de751..db3a518 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -37,10 +37,10 @@ function smalltestlandscape()
     for x in 1:6
         for y in 1:6
             (x in (1:2) || y in (1:4)) ? lc = Ps.forest : lc = Ps.grass
-            landscape[x,y] = Pixel(lc, missing, [])
+            landscape[x,y] = Pixel(lc, missing, [], 0.0)
         end
     end
-    landscape[6,4] = Pixel(Ps.water, 0, [])
+    landscape[6,4] = Pixel(Ps.water, 0, [], 0.0)
     space = GridSpace(size(landscape), periodic=false)
     properties = Dict{Symbol,Any}(:date=>TESTSETTINGS["core.startdate"],
                                   :landscape=>landscape,
-- 
GitLab