diff --git a/src/Persefone.jl b/src/Persefone.jl index 509f728a8ca4304df5575f171becfa0dff3e77ee..0c76d61562e5f527422e3aa0d61649175edee976 100644 --- a/src/Persefone.jl +++ b/src/Persefone.jl @@ -82,8 +82,9 @@ include("world/weather.jl") include("farm/farm.jl") include("crop/crops.jl") -include("nature/nature.jl") include("nature/insects.jl") +include("nature/energy.jl") +include("nature/nature.jl") include("nature/populations.jl") include("nature/ecologicaldata.jl") #TODO loop over files in nature/species directory diff --git a/src/nature/energy.jl b/src/nature/energy.jl new file mode 100644 index 0000000000000000000000000000000000000000..74937b9bf8ab6dd1f56576e236205743be3b6066 --- /dev/null +++ b/src/nature/energy.jl @@ -0,0 +1,122 @@ +### Persefone - a socio-economic-ecological model of European agricultural landscapes. +### +### This file contains structs and functions for implementing Dynamic Energy Budgets. +### + +""" + DEBparameters + +An immutable struct to save the parameter list for a species' Dynamic Energy Budget +model. (See Sousa et al., 2010.) +""" +struct DEBparameters + assimilation::Float64 # percentage of food biomass assimilated (y_EX) + kappa::Float64 # percentage allocated to somatic maintenance and growth (k) + juvenilesize::Float64 # structure size at which an embryo becomes a juvenile (MHb) + adultsize::Float64 # structure size at which a juvenile becomes an adult (MHp) + maintenance::Float64 # daily percentage of body size needed per update for somatic and maturity maintenance + reproduction::Float64 # energy needed to produce an egg (= initial amount of reserve) + v::Float64 # energy conductance (XXX ???) +end + +""" + EnergyBudget + +This struct represents an individual's energy balance, as conceptualised by the +Dynamic Energy Budget theory. Upon assimilation, energy is first stored as biomass +in a reserve buffer, before being used for maintenance, growth, and reproduction. +(Note that this is a simplified model form which ignores maturity as a separate buffer.) + +**Sources:** + +- Malishev & Kramer-Schadt (2021). Movement, models, and metabolism: +Individual-based energy budget models as next-generation extensions +for predicting animal movement outcomes across scales. Ecological +Modelling, 441, 109413. https://doi.org/10.1016/j.ecolmodel.2020.109413 +- Marques et al. (2018). The AmP project: Comparing species on the basis +of dynamic energy budget parameters. PLOS Computational Biology, +14(5), e1006100. https://doi.org/10.1371/journal.pcbi.1006100 +- Sibly et al. (2013). Representing the acquisition and use of energy +by individuals in agent-based models of animal populations. Methods in +Ecology and Evolution, 4(2), 151–161. https://doi.org/10.1111/2041-210x.12002 +- Sousa et al. (2010). Dynamic energy budget theory restores coherence +in biology. Philosophical Transactions of the Royal Society B: Biological +Sciences, 365(1557), 3413–3428. https://doi.org/10.1098/rstb.2010.0166 +- Kooijman, S. A. L. M. (2009). Dynamic energy and mass budgets +in biological systems (3rd ed). Cambridge University Press. +https://www.researchgate.net/profile/Edgar-Meza-3/post/Is_there_a_toxicokinetic_model_for_daphnia_magna_or_other_zooplankton/attachment/59d62cf579197b807798b396/AS%3A348547653357569%401460111644286/download/Dynamic+Energy+Budget+theory+-+Kooijman.pdf +- *see also:* Brown et al. (2004). Toward a metabolic theory of ecology. +Ecology, 85(7), 1771–1789. https://doi.org/10.1890/03-9000 +""" +mutable struct EnergyBudget + params::DEBparameters + reserve::Float64 + structure::Float64 + offspring::Float64 +end + +""" + biomass(energybudget) + +Calculate the total biomass of an individual based on its energy budget. +(Sum of the reserve, structure, and offspring buffers; Sibly et al. 2013.) +""" +function biomass(deb::EnergyBudget) + deb.reserve + deb.structure + deb.offspring +end + +""" + feed!(biomass, energybudget) + +Consume a given quantity of food. Expands the energy reserve by an +amount determined by the assimilation rate. +""" +function feed!(biomass::Float64, deb::EnergyBudget) + deb.reserve += biomass * deb.params.assimilation + #TODO reserve doesn't grow infinitely +end + +""" + update!(energybudget) + +Carry out a daily update of the energy budget. Mobilises reserves and allocates +these to maintenance (prioritised), growth, and reproduction. + +Return `true` if the individual has enough energy to survive, or `false` if the +reserve is empty and it starves. +""" +function update!(deb::EnergyBudget) + #TODO + mobilisation = 0 + somaticmaintenance = deb.params.maintenance # J_ES + maturitymaintenance = deb.params.maintenance # J_EJ + # juveniles grow until they reach adult size + if deb.structure < adultsize + growth = (mobilisation*deb.params.kappa) - somaticmaintenance + deb.structure += growth + end + # juveniles increase in maturity, adults invest into reproduction + maturityreproduction = (mobilisation*(1-deb.params.kappa)) - maturitymaintenance + if deb.structure >= adultsize + deb.offspring += maturityreproduction + end + + deb.reserve -= mobilisation + return deb.reserve > 0 +end + +""" + reproduce!(energybudget) + +If there is sufficient energy in the `offspring` buffer of an adult, produce an +embryo/egg, reducing the parent energy in the process. Returns the embryo's energy +budget, or `nothing` if the conditions are not met. +""" +function reproduce!(deb::EnergyBudget) + if deb.structure >= deb.params.adultsize && deb.offspring >= deb.params.reproduction + deb.offspring -= deb.params.reproduction + return EnergyBudget(deb.params, deb.params.reproduction, 0.0, 0.0) + else + return nothing + end +end diff --git a/src/nature/insects.jl b/src/nature/insects.jl index c0fd291b6ab1c10f0421751bef6d31d5f080506e..743bbf2c722546e1e5d3feae0a089c96fc97c35b 100644 --- a/src/nature/insects.jl +++ b/src/nature/insects.jl @@ -1,21 +1,19 @@ ### Persefone - a socio-economic-ecological model of European agricultural landscapes. ### -### This file contains submodel that calculates insect biomass +### This file contains the submodel that calculates insect biomass ### -##TODO write tests for this - """ insectbiomass(pixel, model) Calculate the insect biomass in this location, using the factors configured -in the `nature.insectmodel` settings (any of: "season", "habitat", "weather", -"pesticides"). Returns a float value in mg/m². +in the `nature.insectmodel` settings (any combination of: "season", "habitat", +"weather", "pesticides"). Returns a float value in g/m². **Biological note:** this is a very approximate calculation! Insect biomass varies wildly in time and space and is hard to measure. This calculation is based on the idea of a parabolic seasonal development of insect abundance, -modified habitat suitability, weather, and pesticide application. Although it +modified by habitat suitability, weather, and pesticide application. Although it is based on empirical studies, it can only deliver a rough, order-of-magnitude estimation of likely insect biomass in a given location. @@ -35,9 +33,9 @@ farmland. Ecology and Evolution, 12(1), e8461. https://doi.org/10.1002/ece3.8461 """ function insectbiomass(pixel::Pixel, model::AgentBasedModel)::Float64 - ## if no factors are configure, insect abundance defaults to 300 mg/m², + ## if no factors are configured, insect abundance defaults to 300 mg/m², ## a value in the upper range of insect biomass density in agricultural landscapes - baseline = 300.0 + baseline = 300 seasonfactor = 0.0 habitatfactor = 1.0 weatherfactor = 1.0 @@ -68,6 +66,7 @@ function insectbiomass(pixel::Pixel, model::AgentBasedModel)::Float64 ## based on fig. 3b in Paquette et al. (2013) ##XXX (and possibly table 3 in Grübler et al. (2008)) if "weather" in @param(nature.insectmodel) + @warn "Weather effects on insect biomass are not yet implemented." #TODO add this once we have a working weather module end @@ -81,5 +80,5 @@ function insectbiomass(pixel::Pixel, model::AgentBasedModel)::Float64 ## calculate biomass using a parabolic equation in the vertex form biomass = seasonfactor+baseline*habitatfactor*pesticidefactor*weatherfactor - biomass > 0 ? biomass : 0.0 + biomass > 0 ? biomass/1000 : 0.0 # convert mg to g end diff --git a/src/nature/nature.jl b/src/nature/nature.jl index 3d270bbb3d5bd3278b6bd3724bd962ccc7443673..ae58eb1ba5caf39e2fc8c8008cf797a781ef5b67 100644 --- a/src/nature/nature.jl +++ b/src/nature/nature.jl @@ -24,10 +24,15 @@ i.e. the trait `phase` can be accessed and modified with `animal.phase`.) # to deepcopy the speciesdict. #XXX add `phase` and `species` (rather than `name`) directly to the struct? traits::Dict{String,Any} + energy::Union{EnergyBudget,Nothing} # DEB is optional sex::Sex age::Int end +# DEB is optional +Animal(id::Int64, pos::Tuple{Int64,Int64}, traits::Dict{String,Any}, + sex::Sex, age::Int) = Animal(id, pos, traits, nothing, sex, age) + # Overloading the `getproperty` and `setproperty!` methods for `Animal` allows # us to write `animal.property` instead of `animal.traits["property"]`. # (This is inspired by Agents.jl in `model.jl`.) diff --git a/test/nature_tests.jl b/test/nature_tests.jl index 7495004731dab9a5838facceaddd29d112ff3b9c..cf56846428ec336d4138d29b103477753177bf15 100644 --- a/test/nature_tests.jl +++ b/test/nature_tests.jl @@ -151,24 +151,24 @@ end p6 = Pixel(Ps.water, 1, []) # check whether the model calculates the same numbers I did by hand model.date = date1 - @test Ps.insectbiomass(p1, model) ≈ 4.11 atol=0.01 + @test Ps.insectbiomass(p1, model) ≈ 0.00411 atol=0.0001 @test Ps.insectbiomass(p2, model) == 0.0 - @test Ps.insectbiomass(p3, model) ≈ 154.11 atol=0.01 - @test Ps.insectbiomass(p4, model) ≈ 4.11 atol=0.01 - @test Ps.insectbiomass(p5, model) ≈ 304.11 atol=0.01 + @test Ps.insectbiomass(p3, model) ≈ 0.15411 atol=0.0001 + @test Ps.insectbiomass(p4, model) ≈ 0.00411 atol=0.0001 + @test Ps.insectbiomass(p5, model) ≈ 0.30411 atol=0.0001 @test Ps.insectbiomass(p6, model) == 0.0 model.date = date2 - @test Ps.insectbiomass(p1, model) == 300.0 - @test Ps.insectbiomass(p2, model) == 150.0 - @test Ps.insectbiomass(p3, model) == 450.0 - @test Ps.insectbiomass(p4, model) == 300.0 - @test Ps.insectbiomass(p5, model) == 600.0 + @test Ps.insectbiomass(p1, model) == 0.3 + @test Ps.insectbiomass(p2, model) == 0.15 + @test Ps.insectbiomass(p3, model) == 0.45 + @test Ps.insectbiomass(p4, model) == 0.3 + @test Ps.insectbiomass(p5, model) == 0.6 @test Ps.insectbiomass(p6, model) == 0.0 model.date = date3 @test Ps.insectbiomass(p1, model) == 0.0 @test Ps.insectbiomass(p2, model) == 0.0 @test Ps.insectbiomass(p3, model) == 0.0 @test Ps.insectbiomass(p4, model) == 0.0 - @test Ps.insectbiomass(p5, model) ≈ 14.43 atol=0.01 + @test Ps.insectbiomass(p5, model) ≈ 0.01443 atol=0.0001 @test Ps.insectbiomass(p6, model) == 0.0 end