From a48e45b575d36782bd632c8c71d6028d5e3c07be Mon Sep 17 00:00:00 2001 From: Daniel <xo30xoqa@idivnb533.usr.idiv.de> Date: Tue, 16 Apr 2024 15:21:25 +0200 Subject: [PATCH] Trying to excise Agents.jl (not yet successful) --- src/Persefone.jl | 23 ++++- src/analysis/makieplots.jl | 4 +- src/core/input.jl | 2 +- src/core/output.jl | 12 +-- src/core/simulation.jl | 48 ++++++---- src/crop/crops.jl | 2 + src/crop/farmplot.jl | 46 +++++----- src/farm/farm.jl | 10 +- src/nature/ecologicaldata.jl | 14 +-- src/nature/insects.jl | 2 +- src/nature/macros.jl | 141 ++++++++++++++--------------- src/nature/nature.jl | 84 +++++++---------- src/nature/populations.jl | 117 ++++++++++++------------ src/nature/species/skylark.jl | 106 ++++++++++------------ src/nature/species/wolpertinger.jl | 33 +++---- src/nature/species/wyvern.jl | 101 ++++++++++----------- src/parameters.toml | 2 +- src/world/landscape.jl | 18 ++-- src/world/weather.jl | 14 +-- 19 files changed, 391 insertions(+), 388 deletions(-) diff --git a/src/Persefone.jl b/src/Persefone.jl index cc6fef7..e256782 100644 --- a/src/Persefone.jl +++ b/src/Persefone.jl @@ -14,7 +14,6 @@ module Persefone ## define dependencies using - Agents, ArgParse, CairoMakie, #XXX this is a very big dependency :-( CSV, @@ -47,10 +46,12 @@ export Pixel, Weather, FarmEvent, + ModelAgent, FarmPlot, Animal, Farmer, DataOutput, + SimulationModel, #macros @param, @species, @@ -86,6 +87,22 @@ export savemodelobject, loadmodelobject +""" + SimulationModel + +The supertype of [AgricultureModel](@ref). This is needed to avoid circular +dependencies (most types and functions depend on `SimulationModel`, but the +definition of the model struct depends on these types). +""" +abstract type SimulationModel end + +""" + ModelAgent + +The supertype of all agents in the model (animal species, farmer types, farmplots). +""" +abstract type ModelAgent 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") @@ -110,7 +127,7 @@ include("nature/ecologicaldata.jl") # for f in readdir("nature/species", join=true) # endswith(f, ".jl") && include(f) # end -include("nature/species/skylark.jl") +#include("nature/species/skylark.jl") include("nature/species/wolpertinger.jl") include("nature/species/wyvern.jl") @@ -118,6 +135,6 @@ include("core/simulation.jl") #this must be last # precompile important functions precompile(initialise, (String,Int)) -precompile(stepsimulation!, (AgentBasedModel,)) +precompile(stepsimulation!, (SimulationModel,)) end diff --git a/src/analysis/makieplots.jl b/src/analysis/makieplots.jl index 97b599c..e6523d7 100644 --- a/src/analysis/makieplots.jl +++ b/src/analysis/makieplots.jl @@ -12,7 +12,7 @@ are available. Optionally, you can pass a landcover map image (this is needed to reduce the frequency of disk I/O for Persefone Desktop). Returns a Makie figure object. """ -function visualisemap(model::AgentBasedModel,date=nothing,landcover=nothing) +function visualisemap(model::SimulationModel,date=nothing,landcover=nothing) # load and plot the map # Note: if the landcover map is supplied, it needs to be rotr90'ed isnothing(landcover) && (landcover = rotr90(load(@param(world.landcovermap)))) @@ -50,7 +50,7 @@ end Plot a line graph of population sizes of each species over time. Returns a Makie figure object. """ -function populationtrends(model::AgentBasedModel) +function populationtrends(model::SimulationModel) pops = model.datatables["populations"] ncolors = max(2, length(@param(nature.targetspecies))) update_theme!(palette=(color=cgrad(:seaborn_bright, ncolors),), cycle=[:color]) diff --git a/src/core/input.jl b/src/core/input.jl index 6b8987a..d7011fc 100644 --- a/src/core/input.jl +++ b/src/core/input.jl @@ -197,7 +197,7 @@ function loadmodelobject(fullfilename::String) end object = deserialize(fullfilename) # Do basic integrity checks - if !(typeof(object) <: Dict && typeof(object["model"]) <: AgentBasedModel) + if !(typeof(object) <: Dict && typeof(object["model"]) <: SimulationModel) @warn "This file does not contain a model object. Loading failed." return end diff --git a/src/core/output.jl b/src/core/output.jl index 513b140..e4bdf00 100644 --- a/src/core/output.jl +++ b/src/core/output.jl @@ -56,7 +56,7 @@ Replace the model logger with the currently active logger. This is intended to b in the testsuite to circumvent a [Julia issue](https://github.com/JuliaLang/julia/issues/48456), where `@test_logs` doesn't work with local loggers. """ -function withtestlogger(model::AgentBasedModel) +function withtestlogger(model::SimulationModel) # copied together from https://github.com/JuliaLang/julia/blob/master/base/logging.jl logstate = current_task().logstate logstate == nothing ? model.logger = global_logger() : model.logger = logstate.logger @@ -69,7 +69,7 @@ end Copy all input files into the output directory, including the actual parameter settings used. This allows replicating a run in future. """ -function saveinputfiles(model::AgentBasedModel) +function saveinputfiles(model::SimulationModel) #XXX If this is a parallel run, we should save the global config to the top-level # output directory @debug "Setting up output directory $(@param(core.outdir))." @@ -147,7 +147,7 @@ end Create and register a new data output. This function must be called by all submodels that want to have their output functions called regularly. """ -function newdataoutput!(model::AgentBasedModel, name::String, header::Vector{String}, +function newdataoutput!(model::SimulationModel, name::String, header::Vector{String}, outputfunction::Function, frequency::String, plotfunction::Union{Function,Nothing}=nothing) if !(frequency in ("daily", "monthly", "yearly", "end", "never")) @@ -178,7 +178,7 @@ Cycle through all registered data outputs and activate them according to their configured frequency. If `force` is `true`, activate all outputs regardless of their configuration. """ -function outputdata(model::AgentBasedModel, force=false) +function outputdata(model::SimulationModel, force=false) #XXX enable output every X days, or weekly? #XXX all output functions except for "end" are run on the first update # -> should they all be run on the last update, too? @@ -215,7 +215,7 @@ end Cycle through all data outputs and call their respective plot functions, saving each figure to file. """ -function visualiseoutput(model::AgentBasedModel) +function visualiseoutput(model::SimulationModel) @debug "Visualising output." CairoMakie.activate!() # make sure we're using Cairo for output in model.dataoutputs @@ -234,7 +234,7 @@ Includes the current model and Julia versions for compatibility checking. WARNING: produces large files (>100 MB) and takes a while to execute. """ -function savemodelobject(model::AgentBasedModel, filename::String) +function savemodelobject(model::SimulationModel, filename::String) object = Dict("model"=>model, "modelversion"=>pkgversion(Persefone), "juliaversion"=>VERSION) diff --git a/src/core/simulation.jl b/src/core/simulation.jl index cbd76be..d139fdf 100644 --- a/src/core/simulation.jl +++ b/src/core/simulation.jl @@ -6,13 +6,13 @@ #XXX How can I make the model output during a parallel run clearer? """ - SimulationModel + AgricultureModel 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 SimulationModel +mutable struct AgricultureModel <: SimulationModel settings::Dict{String,Any} rng::AbstractRNG logger::AbstractLogger @@ -29,6 +29,14 @@ mutable struct SimulationModel events::Vector{FarmEvent} end +""" + stepagent!(agent, model) + +All agent types must define a stepagent!() method that will be called daily. +""" +function stepagent!(agent::ModelAgent, model::SimulationModel) + @error "Agent type $(typeof(agent)) has not defined a stepagent!() method." +end """ simulate(config=PARAMFILE, seed=nothing) @@ -50,7 +58,7 @@ end Carry out a complete simulation run using a pre-initialised model object. """ -function simulate!(model::AgentBasedModel) +function simulate!(model::SimulationModel) @info "Simulation run started at $(Dates.now())." runtime = Dates.value(@param(core.enddate)-@param(core.startdate))+1 #TODO remove Agents.jl-related code, reimplement this more cleanly (#72) @@ -62,7 +70,7 @@ end initialise(config=PARAMFILE, seed=nothing) Initialise the model: read in parameters, create the output data directory, -and instantiate the AgentBasedModel object(s). Optionally allows specifying the +and instantiate the SimulationModel object(s). Optionally allows specifying the configuration file and overriding the `seed` parameter. This returns a single model object, unless the config file contains multiple values for one or more parameters, in which case it creates a full-factorial simulation experiment @@ -96,20 +104,20 @@ function initmodel(settings::Dict{String, Any}) settings["core.enddate"]) crops = readcropparameters(settings["crop.cropfile"], settings["crop.growthfile"]) - model = SimulationModel(settings, - StableRNG(settings["core.seed"]), - logger, - Vector{DataOutput}(), - Dict{String, DataFrame}(), - settings["core.startdate"], - landscape, - weather, - crops, - Vector{Farmer}(), - Vector{FarmPlot}(), - Vector{Union{Animals,Nothing}}(), - Vector{Pair{Animal, Date}}(), - Vector{FarmEvent}()) + 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{Animals,Nothing}}(), + Vector{Pair{Animal, Date}}(), + Vector{FarmEvent}()) saveinputfiles(model) initfields!(model) initfarms!(model) @@ -151,7 +159,7 @@ end Execute one update of the model. """ -function stepsimulation!(model::AgentBasedModel) +function stepsimulation!(model::SimulationModel) #TODO catch exceptions and print them to the log file with_logger(model.logger) do @info "Simulating day $(model.date)." @@ -175,7 +183,7 @@ end Wrap up the simulation. Currently doesn't do anything except print some information. """ -function finalise!(model::AgentBasedModel) +function finalise!(model::SimulationModel) with_logger(model.logger) do @info "Simulated $(model.date-@param(core.startdate))." @info "Simulation run completed at $(Dates.now()),\nwrote output to $(@param(core.outdir))." diff --git a/src/crop/crops.jl b/src/crop/crops.jl index d0495d7..927b237 100644 --- a/src/crop/crops.jl +++ b/src/crop/crops.jl @@ -34,6 +34,8 @@ 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} diff --git a/src/crop/farmplot.jl b/src/crop/farmplot.jl index 200a106..1160596 100644 --- a/src/crop/farmplot.jl +++ b/src/crop/farmplot.jl @@ -11,8 +11,8 @@ 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. """ -@agent FarmPlot GridAgent{2} begin - #TODO remove Agents.jl-related code, reimplement this more cleanly (#72) +mutable struct FarmPlot + const id::Int64 pixels::Vector{Tuple{Int64, Int64}} croptype::CropType phase::GrowthPhase @@ -29,27 +29,28 @@ end Initialise the model with its farm plots. """ -function initfields!(model::AgentBasedModel) +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 + # 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[objectid].pixels, (x,y)) + push!(model.farmplots[objectid].pixels, (x,y)) else #XXX does this phase calculation work out? month(model.date) < 3 ? phase = janfirst : phase = marchfirst - fp = add_agent!((x,y), FarmPlot, model, [(x,y)], + fp = FarmPlot(length(model.farmplots)+1, [(x,y)], model.crops["natural grass"], phase, - 0.0, 0.0, 0.0, 0.0, Vector{EventType}()) + 0.0, 0.0, 0.0, 0.0, Vector{EventType}()) + push!(model.farmplots, fp) model.landscape[x,y].fieldid = fp.id convertid[rawid] = fp.id n += 1 @@ -64,7 +65,7 @@ end Update a farm plot by one day. """ -function stepagent!(farmplot::FarmPlot, model::AgentBasedModel) +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) @@ -87,7 +88,7 @@ end Sow the specified crop on this farmplot. """ -function sow!(cropname::String, farmplot::FarmPlot, model::AgentBasedModel) +function sow!(cropname::String, farmplot::FarmPlot, model::SimulationModel) createevent!(model, farmplot.pixels, sowing) farmplot.croptype = model.crops[cropname] farmplot.phase = sow @@ -99,7 +100,7 @@ end Harvest the crop on this farmplot. """ -function harvest!(farmplot::FarmPlot, model::AgentBasedModel) +function harvest!(farmplot::FarmPlot, model::SimulationModel) createevent!(model, farmplot.pixels, harvesting) farmplot.phase in [harvest1, harvest2] ? farmplot.phase = harvest2 : @@ -118,7 +119,7 @@ end 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::AgentBasedModel) +function growcrop!(farmplot::FarmPlot, model::SimulationModel) fertiliser in farmplot.events ? curve = farmplot.croptype.lownutrientgrowth : curve = farmplot.croptype.highnutrientgrowth @@ -157,12 +158,11 @@ end Calculate the average field size in hectares for the model landscape. """ -function averagefieldsize(model::AgentBasedModel) +function averagefieldsize(model::SimulationModel) conversionfactor = 100 #our pixels are currently 10x10m, so 100 pixels per hectare sizes::Vector{Float64} = [] - for a in allagents(model) - (typeof(a) != FarmPlot) && continue - push!(sizes, size(a.pixels)[1]/conversionfactor) + for fp in model.farmplots + push!(sizes, size(fp.pixels)[1]/conversionfactor) end round(sum(sizes)/size(sizes)[1], digits=2) end @@ -172,9 +172,9 @@ end Return the crop at this position, or nothing if there is no crop here (utility wrapper). """ -function croptype(pos::Tuple{Int64,Int64}, model::AgentBasedModel) +function croptype(pos::Tuple{Int64,Int64}, model::SimulationModel) ismissing(model.landscape[pos...].fieldid) ? nothing : - model[model.landscape[pos...].fieldid].croptype + model.farmplots[model.landscape[pos...].fieldid].croptype end """ @@ -183,9 +183,9 @@ end Return the name of the crop at this position, or nothing if there is no crop here (utility wrapper). """ -function cropname(pos::Tuple{Int64,Int64}, model::AgentBasedModel) +function cropname(pos::Tuple{Int64,Int64}, model::SimulationModel) ismissing(model.landscape[pos...].fieldid) ? nothing : - model[model.landscape[pos...].fieldid].croptype.name + model.farmplots[model.landscape[pos...].fieldid].croptype.name end """ @@ -194,7 +194,7 @@ end 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::AgentBasedModel) +function cropheight(pos::Tuple{Int64,Int64}, model::SimulationModel) ismissing(model.landscape[pos...].fieldid) ? nothing : - model[model.landscape[pos...].fieldid].height + model.farmplots[model.landscape[pos...].fieldid].height end diff --git a/src/farm/farm.jl b/src/farm/farm.jl index 42c5a42..391b825 100644 --- a/src/farm/farm.jl +++ b/src/farm/farm.jl @@ -8,8 +8,10 @@ This is the agent type for the farm ABM. (Not yet implemented.) """ -@agent Farmer GridAgent{2} begin - #TODO remove Agents.jl-related code, reimplement this more cleanly (#72) +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 add AES @@ -20,7 +22,7 @@ end Update a farmer by one day. """ -function stepagent!(farmer::Farmer, model::AgentBasedModel) +function stepagent!(farmer::Farmer, model::SimulationModel) #TODO end @@ -29,6 +31,6 @@ end Initialise the model with a set of farm agents. """ -function initfarms!(model::AgentBasedModel) +function initfarms!(model::SimulationModel) #TODO end diff --git a/src/nature/ecologicaldata.jl b/src/nature/ecologicaldata.jl index 02c646a..554f8ae 100644 --- a/src/nature/ecologicaldata.jl +++ b/src/nature/ecologicaldata.jl @@ -8,7 +8,7 @@ Create output files for each data group collected by the nature model. """ -function initecologicaldata(model::AgentBasedModel) +function initecologicaldata(model::SimulationModel) newdataoutput!(model, "populations", ["Date", "Species", "Abundance"], savepopulationdata, @param(nature.popoutfreq), populationtrends) newdataoutput!(model, "individuals", ["Date","ID","X","Y","Species","Sex","Age"], @@ -23,10 +23,10 @@ the current date and population size for each animal species. May be called neve daily, monthly, yearly, or at the end of a simulation, depending on the parameter `nature.popoutfreq`. """ -function savepopulationdata(model::AgentBasedModel) +function savepopulationdata(model::SimulationModel) pops = Dict{String,Int}(s=>0 for s = @param(nature.targetspecies)) - for a in allagents(model) - (typeof(a) != Animal) && continue + for a in model.animals + isnothing(a) && continue pops[a.name] += 1 end for a in model.migrants @@ -47,11 +47,11 @@ all properties of all animal individuals in the model. May be called never, dail monthly, yearly, or at the end of a simulation, depending on the parameter `nature.indoutfreq`. WARNING: Produces very big files! """ -function saveindividualdata(model::AgentBasedModel) +function saveindividualdata(model::SimulationModel) #XXX doesn't include migrants! data = [] - for a in allagents(model) - (typeof(a) != Animal) && continue + for a in model.animals + isnothing(a) && continue push!(data, [model.date,a.id,a.pos[1],a.pos[2],a.traits["name"],a.sex,a.age]) end data diff --git a/src/nature/insects.jl b/src/nature/insects.jl index c2b81b7..d9c156d 100644 --- a/src/nature/insects.jl +++ b/src/nature/insects.jl @@ -24,7 +24,7 @@ estimation of likely insect biomass in a given location. - Paquette et al. (2013). Seasonal patterns in Tree Swallow prey (Diptera) abundance are affected by agricultural intensification. Ecological Applications, 23(1), 122–133. https://doi.org/10.1890/12-0068.1 - Püttmanns et al. (2022). Habitat use and foraging parameters of breeding Skylarks indicate no seasonal decrease in food availability in heterogeneous farmland. Ecology and Evolution, 12(1), e8461. https://doi.org/10.1002/ece3.8461 """ -function insectbiomass(pixel::Pixel, model::AgentBasedModel)::Float64 +function insectbiomass(pixel::Pixel, model::SimulationModel)::Float64 ## 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 diff --git a/src/nature/macros.jl b/src/nature/macros.jl index ccf9386..e507290 100644 --- a/src/nature/macros.jl +++ b/src/nature/macros.jl @@ -4,15 +4,22 @@ ### for defining species. ### -## Note that this DSL consists of lots of deeply nested macro calls, which is a -## known tricky issue in Julia (https://github.com/JuliaLang/julia/issues/23221, +##XXX does it make sense to have all of the macros in one place, +## or shouldn't they rather be next to the functions they wrap? + +## Note that this DSL consists of lots of deeply nested macro calls, which +## is a known tricky issue in Julia (https://github.com/JuliaLang/julia/issues/23221, ## https://github.com/p-i-/MetaGuideJulia/wiki#example-swap-macro-to-illustrate-esc). -## Hence all the `esc`apes in the following code - be careful when modifying! +## Hence all the `esc`apes in the following code - take care when modifying! + +##TODO update documentation """ @species(name, body) A macro used to create new species definitions for the nature model. +All species include the standard fields described for the [Animal](@ref) type. + This is effectively a simple domain-specific language, establishing a custom syntax to describe species' biology: @@ -37,27 +44,25 @@ of the species during various parts of its life cycle. (See the documentation to [`@initialise`](@ref) and [`@phase`](@ref) for details). Code in a species definition block can access the rest of the model using -the `model` variable (an object of type `AgentBasedModel`). +the `model` variable (an object of type `SimulationModel`). """ macro species(name, body) quote - Core.@__doc__ function $(esc(name))($(esc(:model))::AgentBasedModel) - # create internal variables and execute the definition body - $(esc(:name)) = string($(QuoteNode(name))) - $(esc(:phase)) = "" + Core.@__doc__ @kwdef mutable struct $(esc(name)) <: Animal + const id::Int64 + const sex::Sex + const parents::Tuple{Int64,Int64} #XXX assumes sexual reprod. + pos::Tuple{Int64,Int64} + phase::Function + age::Int = 0 + energy::Union{EnergyBudget,Nothing} = nothing # DEB is optional + offspring::Vector{Int64} = Vector{Int64}() $(esc(body)) - # extract and process the local variables into a species dict - vardict = Base.@locals - speciesdict = Dict{String,Any}() - for k in keys(vardict) - speciesdict[string(k)] = vardict[k] - end - delete!(speciesdict, "model") - delete!(speciesdict, $(string(name))) - return speciesdict end - # allow species to be defined outside of the Persefone module, but still available - # inside it (needed by `initnature!()` and `reproduce!()`) + # define a zero-argument constructor to access default field values + $(esc(name))() = $(esc(name))(-1, hermaphrodite, (-1, -1), (-1, -1), ()->0) + # allow species to be defined outside of the Persefone module, but still + # available inside it (needed by `initnature!()` and `reproduce!()`) (@__MODULE__() != $(esc(:Persefone))) && ($(esc(:Persefone)).$name = $(esc(name))) end end @@ -72,12 +77,10 @@ up the simulation. Note: if this macro is not used, the variable `initialise!` must be set manually in the species definition. """ -macro initialise(habitatdescriptor, kwargs...) - :($(esc(:initialise!)) = initpopulation($(esc(habitatdescriptor)); $(map(esc, kwargs)...))) +macro initialise(phase, kwargs...) + :($(esc(:initialise!)) = initpopulation($(esc(phase)); $(map(esc, kwargs)...))) end -#TODO add an individual-level initialisation function! - """ @phase(name, body) @@ -95,57 +98,42 @@ variables: `animal.sex`, and `animal.<trait>` (where <trait> is a variable that was defined in the top part of the species definition body). - `pos` gives the animal's current position as a coordinate tuple. -- `model` a reference to the model world (an object of type `AgentBasedModel`). +- `model` a reference to the model world (an object of type `SimulationModel`). This allows access to `model.date` (the current simulation date) and `model.landscape` (a two-dimensional array of pixels containing geographic information). -Several utility macros can be used within the body of `@phase` as a short-hand for -common expressions: [`@trait`](@ref), [`@setphase`](@ref), [`@respond`](@ref), -[`@kill`](@ref), [`@reproduce`](@ref), [`@neighbours`](@ref), [`@rand`](@ref), -[`@shuffle!`](@ref), [`@move`](@ref), [`@walk`](@ref), [`@follow`](@ref). +Several utility macros can be used within the body of `@phase` as a +short-hand for common expressions: [`@setphase`](@ref), [`@respond`](@ref), +[`@kill`](@ref), [`@reproduce`](@ref), [`@neighbours`](@ref), +[`@rand`](@ref),[`@shuffle!`](@ref), [`@move`](@ref), [`@walk`](@ref), +[`@follow`](@ref). -Note that the first phase that is defined in a species definition block will be -the phase that animals are assigned at birth, unless the variable `phase` is -explicitly defined by the user in the species definition block. +Note that the first phase that is defined in a species definition block will +be the phase that animals are assigned at birth, unless the variable `phase` +is explicitly defined by the user in the species definition block. """ -macro phase(name, body) - #TODO the docstrings give a lot of warnings in the log - can I fix that? +macro phase(phaseid, body) + species = String(phaseid.args[1]) + phase = String(phaseid.args[2].value) quote - Core.@__doc__ function $(esc(name))($(esc(:animal))::Animal, $(esc(:model))::AgentBasedModel) - #TODO add `self` as a synonym for `animal` - $(esc(:pos)) = $(esc(:animal)).pos #XXX does this make sense? - #$(esc(:date)) = $(esc(:model)).date #XXX does this make sense? + Core.@__doc__ function $(esc(phase))(self::$(esc(species)), + model::SimulationModel) + $(esc(:pos)) = $(esc(:self)).pos # needed for landscape macros $(esc(body)) end - ($(esc(:phase)) == "") && ($(esc(:phase)) = $(String(name))) end end -""" - @trait(traitname) - -A utility macro to quickly access an animal's trait value. -This can only be used nested within [`@phase`](@ref). -""" -macro trait(traitname) - #FIXME actually, we can get rid of this altogether if we add a Python-style `self` - #XXX This would error if called in the first part of a species definition block - # (i.e. outside of a @phase block). Although this is specified in the documentation, - # it is unexpected and liable to be overlooked. Can we add a third clause to - # compensate for that? - :($(esc(:animal)).$(traitname)) -end - """ @animal(id) -Return the animal/agent object associated with this ID number. +Return the animal object associated with this ID number. This can only be used in a context where the `model` object is available (e.g. nested within [`@phase`](@ref)). """ macro animal(id) - :($(esc(:model))[$(esc(id))]) + :($(esc(:model)).animals[$(esc(id))]) end """ @@ -165,8 +153,7 @@ end Switch this animal over to a different phase. This can only be used nested within [`@phase`](@ref). """ macro setphase(newphase) - #XXX make this usable in the top part of a species definition? - :($(esc(:animal)).phase = $(String(newphase))) + :($(esc(:self)).phase = $(newphase)) end """ @@ -177,7 +164,7 @@ This can only be used nested within [`@phase`](@ref). """ macro respond(eventname, body) quote - if $(esc(eventname)) in $(esc(:model)).landscape[$(esc(:animal)).pos...].events + if $(esc(eventname)) in $(esc(:model)).landscape[$(esc(:self)).pos...].events $(esc(body)) end end @@ -191,7 +178,7 @@ thin wrapper around [`kill!`](@ref), and passes on any arguments. This can only used nested within [`@phase`](@ref). """ macro kill(args...) - :(kill!($(esc(:animal)), $(esc(:model)), $(map(esc, args)...)) && return) + :(kill!($(esc(:self)), $(esc(:model)), $(map(esc, args)...)) && return) end """ @@ -201,7 +188,7 @@ Let this animal reproduce. This is a thin wrapper around [`reproduce!`](@ref), a passes on any arguments. This can only be used nested within [`@phase`](@ref). """ macro reproduce(args...) - :(reproduce!($(esc(:animal)), $(esc(:model)), $(map(esc, args)...))) + :(reproduce!($(esc(:self)), $(esc(:model)), $(map(esc, args)...))) end """ @@ -212,7 +199,7 @@ It will be returned to its current location at the specified `arrival` date. This can only be used nested within [`@phase`](@ref). """ macro migrate(arrival) - :(migrate!($(esc(:animal)), $(esc(:model)), $(esc(arrival)))) + :(migrate!($(esc(:self)), $(esc(:model)), $(esc(arrival)))) end """ @@ -222,8 +209,9 @@ Return an iterator over all animals in the given radius around this animal, excl This can only be used nested within [`@phase`](@ref). """ macro neighbours(radius) + #FIXME remove Agents.jl code #TODO enable filtering by species - :(nearby_animals($(esc(:animal)), $(esc(:model)), $(esc(radius)))) + :(nearby_animals($(esc(:self)), $(esc(:model)), $(esc(radius)))) end """ @@ -255,8 +243,9 @@ For more complex habitat suitability checks, the use of this macro can be circumvented by directly creating an equivalent function. """ macro habitat(body) - #XXX I suspect that I may have less problems with macro expansion and module - # scoping if @habitat did not create a new function. But is there a different way? + #XXX I suspect that I may have less problems with macro expansion and + # module scoping if @habitat did not create a new function. But is + # there a different way? quote function($(esc(:pos)), $(esc(:model))) if $(esc(body)) @@ -328,9 +317,10 @@ end """ @randompixel(range, habitatdescriptor) -Find a random pixel within a given `range` of the animal's location that matches the -habitatdescriptor (create this using [`@habitat`](@ref)). This is a utility wrapper -that can only be used nested within [`@phase`](@ref). +Find a random pixel within a given `range` of the animal's location that +matches the habitatdescriptor (create this using [`@habitat`](@ref)). +This is a utility wrapper that can only be used nested within +[`@phase`](@ref). """ macro randompixel(args...) :(randompixel($(esc(:pos)), $(esc(:model)), $(map(esc, args)...))) @@ -340,11 +330,11 @@ end @countanimals(species="", radius=0) Count the number of animals of the given species in this location. -This is a utility wrapper that can only be used nested within [`@phase`](@ref) -or [`@habitat`](@ref). +This is a utility wrapper that can only be used nested within +[`@phase`](@ref) or [`@habitat`](@ref). """ macro countanimals(args...) - :(countanimals($(esc(:pos)), $(esc(:model)))) + :(countanimals($(esc(:pos)), $(esc(:model)), $(map(esc, args)...))) end ##TODO test movement macros @@ -356,7 +346,8 @@ Move the current individual to a new position. This is a utility wrapper that can only be used nested within [`@phase`](@ref). """ macro move(position) - :(move_agent!($(esc(:animal)), $(esc(position)), $(esc(:model)))) + #FIXME remove Agents.jl code + :(move_agent!($(esc(:self)), $(esc(position)), $(esc(:model)))) end """ @@ -368,8 +359,9 @@ by 2 and decrements the Y coordinate by 3.) This is a utility wrapper that can o used nested within [`@phase`](@ref). """ macro walk(direction) + #FIXME remove Agents.jl code #XXX add `ifempty` keyword? - :(walk!($(esc(:animal)), $(esc(direction)), $(esc(:model)))) + :(walk!($(esc(:self)), $(esc(direction)), $(esc(:model)))) end """ @@ -379,8 +371,9 @@ Walk in a random direction for a specified number of steps. This is a utility wrapper that can only be used nested within [`@phase`](@ref). """ macro randomwalk(distance) + #FIXME remove Agents.jl code #XXX add `ifempty` keyword? - :(randomwalk!($(esc(:animal)), $(esc(:model)), $(esc(distance)))) + :(randomwalk!($(esc(:self)), $(esc(:model)), $(esc(distance)))) end #TODO add own walking functions that respect habitat descriptors @@ -391,6 +384,6 @@ Move to a location within the given distance of the leading animal. This is a utility wrapper that can only be used nested within [`@phase`](@ref). """ macro follow(leader, distance) - :(followanimal!($(esc(:animal)), $(esc(leader)), $(esc(:model)), $(esc(distance)))) + :(followanimal!($(esc(:self)), $(esc(leader)), $(esc(:model)), $(esc(distance)))) end diff --git a/src/nature/nature.jl b/src/nature/nature.jl index 2edc9fa..b50dd8f 100644 --- a/src/nature/nature.jl +++ b/src/nature/nature.jl @@ -12,50 +12,28 @@ """ Animal -This is the generic agent type for all animals. Species are differentiated -by trait dictionaries passed by them during initialisation. (Note that each -trait variable can still be accessed as if it were a normal field name, -i.e. the trait `phase` can be accessed and modified with `animal.phase`.) +This is the generic agent type for all animals. Individual species are created +using the [`@species`](@ref) macro. All species contain the following fields: + +- `id` An integer unique identifier for this individual. +- `sex` male, female, or hermaphrodite. +- `parents` The IDs of the individual's parents. +- `pos` An (x, y) coordinate tuple. +- `age` The age of the individual in days. +- `phase` The update function to be called during the individual's current life phase. +- `energy` A [DEBparameters](@ref) struct for calculating energy budgets. +- `offspring` A vector containing the IDs of an individual's children. """ -@agent Animal GridAgent{2} begin - #TODO remove Agents.jl-related code, reimplement this more cleanly (#72) - #XXX is it (performance-)wise to use a dict for the traits? - # Doesn't that rather obviate the point of having an agent struct? - # If I could move the mutable traits to the struct, I wouldn't need - # 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 - parents::Tuple{Int64,Int64} #XXX two parents for hermaphrodites? - sex::Sex - age::Int -end - -# DEB is optional -Animal(id::Int64, pos::Tuple{Int64,Int64}, traits::Dict{String,Any}, - parents::Tuple{Int64,Int64}, sex::Sex, age::Int) = - Animal(id, pos, traits, nothing, parents, 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`.) +abstract type Animal <: ModelAgent end -function Base.getproperty(animal::Animal, s::Symbol) - if s in fieldnames(Animal) - return getfield(animal, s) - else - return animal.traits[String(s)] - end -end +""" + speciesof(animal) -function Base.setproperty!(animal::Animal, s::Symbol, x) - if s in fieldnames(Animal) - setfield!(animal, s, x) - else - animal.traits[String(s)] = x - end +Return the species name of this animal as a string. +""" +function speciesof(a::Animal) + string(typeof(a)) end - """ animalid(animal) @@ -63,7 +41,7 @@ end A small utility function to return a string with the species name and ID of an animal. """ function animalid(a::Animal) - return "$(a.name) $(a.id)" + return "$(speciesof(a)) $(a.id)" end """ @@ -71,9 +49,9 @@ end Update an animal by one day, executing it's currently active phase function. """ -function stepagent!(animal::Animal, model::AgentBasedModel) +function stepagent!(animal::Animal, model::SimulationModel) animal.age += 1 - animal.traits[animal.phase](animal,model) #FIXME -> note to self: why, what's wrong? + animal.phase(animal, model) end """ @@ -81,11 +59,11 @@ end Initialise the model with all simulated animal populations. """ -function initnature!(model::AgentBasedModel) +function initnature!(model::SimulationModel) # The config file determines which species are simulated in this run for speciesname in @param(nature.targetspecies) - species = @eval $(Symbol(speciesname))($model) - species["initialise!"](species, model) + # Call each species' initialisation function + @eval $(Symbol(speciesname))().initialise!($(Symbol(speciesname)), model) end # Initialise the data output initecologicaldata(model) @@ -96,13 +74,19 @@ end Run processes that affect all animals. """ -function updatenature!(model::AgentBasedModel) +function updatenature!(model::SimulationModel) + # Update all animals. Dead animals are replaced by `nothing`, we can skip these. + #XXX I assume that keeping all those `nothing`s in the animal vector won't lead + # to memory bloat, but I will have to check that. + for a in model.animals + !isnothing(a) && stepagent!(a, model) + end # The migrant pool is sorted by date of return, so we can simply look at the top # of the stack to check whether any animals are returning today. while !isempty(model.migrants) && model.migrants[1].second <= model.date - add_agent_pos!(model.migrants[1].first, model) - @debug "$(animalid(model.migrants[1].first)) has returned." + returnee = model.migrants[1].first + model.animals[returnee.id] = returnee + @debug "$(animalid(returnee)) has returned." deleteat!(model.migrants, 1) end - #XXX what else needs to go here? end diff --git a/src/nature/populations.jl b/src/nature/populations.jl index 2288ecd..d218973 100644 --- a/src/nature/populations.jl +++ b/src/nature/populations.jl @@ -31,88 +31,93 @@ This can be used to create the `initialise!` variable in a species definition bl - If `asexual` is true, all created individuals are assigned the sex `hermaphrodite`, otherwise, they are randomly assigned male of female. (If `pairs` is true, `asexual` is ignored.) - -- `initfunction` is an optional function that takes an animal object and the model, - and performs individual-specific initialisation tasks. It is called after the - individual has been created and placed. """ -function initpopulation(habitatdescriptor::Function; phase::Union{String,Nothing}=nothing, +function initpopulation(phase::Function; habitat::Function=@habitat(true), popsize::Int64=-1, popdensity::Int64=-1, pairs::Bool=false, - asexual::Bool=false, initfunction::Function=(a,m)->nothing) - #TODO add a constructor function/macro for the individual - function(species::Dict{String,Any}, model::AgentBasedModel) + asexual::Bool=false) + (popsize <= 0 && popdensity <= 0) && #XXX not sure what this would do + @warn("initpopulation() called with popsize and popdensity both <= 0") + function(species::Type, model::SimulationModel) n = 0 lastn = 0 - specname = species["name"] - (!isnothing(phase)) && (species["phase"] = phase) width, height = size(model.landscape) while n == 0 || n < popsize for x in @shuffle!(Vector(1:width)) for y in @shuffle!(Vector(1:height)) - if habitatdescriptor((x,y), model) && + if habitat((x,y), model) && (popdensity <= 0 || @chance(1/popdensity)) #XXX what if pd==0? if pairs - a1 = add_agent!((x,y), Animal, model, deepcopy(species), - (-1,-1), female, 0) - a2 = add_agent!((x,y), Animal, model, deepcopy(species), - (-1, -1), male, 0) - initfunction(a1, model) - initfunction(a2, model) + a1 = @eval($(species))(length(model.animals)+1, male, + (-1, -1), (x,y), phase) + a2 = @eval($(species))(length(model.animals)+1, female, + (-1, -1), (x,y), phase) + initindividual(a1, model) + initindividual(a2, model) + push!(model.animals, a1) + push!(model.animals, a2) n += 2 else sex = asexual ? hermaphrodite : @rand([male, female]) - a = add_agent!((x,y), Animal, model, deepcopy(species), - (-1, -1), sex,0) - initfunction(a, model) + a = @eval($(species))(length(model.animals)+1, sex, + (-1, -1), (x,y), phase) + initindividual(a, model) + push!(model.animals, a) n += 1 end end + #XXX break randomly to avoid initialising all individuals in a single column? (popsize > 0 && n >= popsize) && break end (popsize > 0 && n >= popsize) && break end if lastn == n # prevent an infinite loop - we don't have a Cray... - @warn "There are not enough suitable locations for $(specname) in the landscape." + @warn "There are not enough suitable locations for $(string(species)) in the landscape." break end lastn = n end - @info "Initialised $(n) $(specname)s." + @info "Initialised $(n) $(string(species))s." end end +#XXX initpopulation with dispersal from an original source? +#XXX initpopulation based on known occurences in real-life? + """ - initrandompopulation(popsize; kwargs...) + initindividual(animal, model) + +The function that is called when a new animal object is created. +Has access to the `model` object and carries out any necessary +initialisation routines. -A simplified version of [`initpopulation`](@ref). Creates a function that initialises -`popsize` individuals, spread at random across the landscape. +Note that this is a stump method. Each species should implement its +own version by defining a [`@phase`](@ref) called `initindividual`. """ -function initrandompopulation(popsize::Int64; kwargs...) - #XXX How should this be called if users are supposed to use @initialise? - initpopulation(@habitat(true); popsize=popsize, kwargs...) +function initindividual(animal::Animal, model::SimulationModel) + @debug "No initindividual() method for species $(typeof(animal))." end -#XXX initpopulation with dispersal from an original source? -#XXX initpopulation based on known occurences in real-life? - """ reproduce!(animal, model, mate, n=1) Produce one or more offspring for the given animal at its current location. The `mate` argument gives the ID of the reproductive partner. """ -function reproduce!(animal::Animal, model::AgentBasedModel, mate::Int64, n::Int64=1) +function reproduce!(animal::Animal, model::SimulationModel, + n::Int64=1, mate::Int64=-1) (animal.sex == male) && @warn "Male $(animalid(animal)) is reproducing." - offspring = [] for i in 1:n - sex = (animal.sex == hermaphrodite) ? hermaphrodite : @rand([male, female]) - # We need to generate a fresh species dict here - species = @eval $(Symbol(animal.name))($model) - a = add_agent!(animal.pos, Animal, model, species, (animal.id, mate), sex, 0) - push!(offspring, a.id) + sex = (animal.sex == hermaphrodite) ? + hermaphrodite : + @rand([male, female]) + child = @eval($(typeof(animal)))(length(model.animals)+1, sex, + (animal.id, mate), animal.pos) + initindividual(child, model) + push!(model.animals, child) + push!(animal.offspring, child.id) + mate > 0 && push!(models.animals[mate].offspring, child.id) end @debug "$(animalid(animal)) has reproduced." - offspring end """ @@ -121,11 +126,12 @@ end Kill this animal, optionally with a given percentage probability. Returns true if the animal dies, false if not. """ -function kill!(animal::Animal, model::AgentBasedModel, probability::Float64=1.0, cause::String="") +function kill!(animal::Animal, model::SimulationModel, + probability::Float64=1.0, cause::String="") if @rand() < probability postfix = isempty(cause) ? "." : " from $cause." @debug "$(animalid(animal)) has died$(postfix)" - kill_agent!(animal, model) + model.animals[animal.id] = nothing return true end return false @@ -137,15 +143,14 @@ end Remove this animal from the map and add it to the migrant species pool. It will be returned to its current location at the specified `arrival` date. """ -function migrate!(animal::Animal, model::AgentBasedModel, arrival::Date) - i = 1 - while i <= length(model.migrants) && model.migrants[i].second < arrival - i += 1 - end - i <= length(model.migrants) ? - insert!(model.migrants, i, Pair(animal, arrival)) : - push!(model.migrants, Pair(animal, arrival)) - remove_agent!(animal, model) +function migrate!(animal::Animal, model::SimulationModel, arrival::Date) + # keep model.migrants sorted by inserting the new migrant after migrants + # that will return earlier than it + i = findfirst(m -> m.second >= arrival, model.migrants) + isnothing(i) ? + push!(model.migrants, Pair(animal, arrival)) : + insert!(model.migrants, i, Pair(animal, arrival)) + model.animals[animal.id] = nothing @debug "$(animalid(animal)) has migrated." end @@ -154,8 +159,8 @@ end Test whether the animal with the given ID is still alive. """ -function isalive(animalid::Int64, model::AgentBasedModel) - animalid in allids(model) +function isalive(animalid::Int64, model::SimulationModel) + !isnothing(model.animals[animalid]) || any(m->m.first.id==animalid, model.migrants) end """ @@ -163,7 +168,7 @@ end Return an iterator over all animals in the given radius around this position. """ -function nearby_animals(pos::Tuple{Int64,Int64}, model::AgentBasedModel, radius::Int64) +function nearby_animals(pos::Tuple{Int64,Int64}, model::SimulationModel, radius::Int64) #TODO enable filtering by species neighbours = (model[id] for id in nearby_ids(pos, model, radius)) Iterators.filter(a -> typeof(a) == Animal, neighbours) @@ -174,7 +179,7 @@ end Return an iterator over all animals in the given radius around this animal, excluding itself. """ -function nearby_animals(animal::Animal, model::AgentBasedModel, radius::Int64) +function nearby_animals(animal::Animal, model::SimulationModel, radius::Int64) #TODO enable filtering by species neighbours = (model[id] for id in nearby_ids(animal.pos, model, radius)) Iterators.filter(a -> typeof(a) == Animal && a.id != animal.id, neighbours) @@ -185,7 +190,7 @@ end Count the number of animals in this location (optionally supplying a species name and radius). """ -function countanimals(pos::Tuple{Int64,Int64}, model::AgentBasedModel; +function countanimals(pos::Tuple{Int64,Int64}, model::SimulationModel; species::String="", radius::Int64=0) n = 0 #XXX can we ignore capitalisation in the spelling of `species`? @@ -200,7 +205,7 @@ end Move the follower animal to a location near the leading animal. """ -function followanimal!(follower::Animal, leader::Animal, model::AgentBasedModel, +function followanimal!(follower::Animal, leader::Animal, model::SimulationModel, distance::Int64=0) #TODO test function spacing = Tuple(@rand(-distance:distance, 2)) diff --git a/src/nature/species/skylark.jl b/src/nature/species/skylark.jl index a7c7191..592b526 100644 --- a/src/nature/species/skylark.jl +++ b/src/nature/species/skylark.jl @@ -62,10 +62,10 @@ At the moment, this implementation is still in development. As an egg, simply check for mortality and hatching. """ @phase egg begin - @kill(@trait(eggpredationmortality), "predation") - @respond(harvesting, @kill(@trait(nestharvestmortality), "harvest")) + @kill(self.eggpredationmortality, "predation") + @respond(harvesting, @kill(self.nestharvestmortality, "harvest")) - if @trait(age) == @trait(eggtime) + if self.age == self.eggtime @setphase(nestling) end end @@ -75,9 +75,9 @@ At the moment, this implementation is still in development. """ @phase nestling begin #TODO add feeding & growth - @kill(@trait(nestlingpredationmortality), "predation") - @respond(harvesting, @kill(@trait(nestharvestmortality), "harvest")) - if @trait(age) == @trait(nestlingtime)+@trait(eggtime) + @kill(self.nestlingpredationmortality, "predation") + @respond(harvesting, @kill(self.nestharvestmortality, "harvest")) + if self.age == self.nestlingtime+self.eggtime) @setphase(fledgling) end end @@ -88,10 +88,10 @@ At the moment, this implementation is still in development. """ @phase fledgling begin #TODO add feeding & growth - @kill(@trait(fledglingpredationmortality), "predation") + @kill(self.fledglingpredationmortality, "predation") @randomwalk(1) #TODO add movement following the parents - if @trait(age) == @trait(fledglingtime)+@trait(eggtime) - @kill(@trait(firstyearmortality), "first year mortality") #XXX mechanistic? + if self.age == self.fledglingtime+self.eggtime) + @kill(self.firstyearmortality, "first year mortality") #XXX mechanistic? @setphase(nonbreeding) end end @@ -103,19 +103,16 @@ At the moment, this implementation is still in development. # flocking behaviour - follow a random neighbour or move randomly #TODO add feeding and mortality, respect habitat when moving neighbours = map(a->a.id, @neighbours(10)) #FIXME - #isempty(neighbours) ? @randomwalk(5) : @follow(@rand(neighbours), 2) - if isempty(neighbours) - @randomwalk(5) - else - @follow(model[@rand(neighbours)], 2) - end + isempty(neighbours) ? + @randomwalk(5) : + @follow(@animal(@rand(neighbours)), 2) # check if the bird migrates leave, arrive = animal.migrationdates m, d = monthday(model.date) migrate = (((m < arrive[1]) || (m == arrive[1] && d < arrive[2])) || ((m > leave[1]) || (m == leave[1] && d >= leave[2]))) if migrate #FIXME not all migrate? - @kill(@trait(migrationmortality), "migration") + @kill(self.migrationmortality, "migration") returndate = Date(year(model.date), arrive[1], arrive[2]) model.date != @param(core.startdate) && (returndate += Year(1)) @setphase(mating) @@ -128,22 +125,22 @@ At the moment, this implementation is still in development. """ @phase mating begin # if we've found a mate, wait for nesting begin and then go to the next phase - if @trait(mate) != -1 - if !@isalive(@trait(mate)) - @trait(mate) = -1 + if self.mate != -1 + if !@isalive(self.mate) + self.mate = -1 return end m, d = monthday(model.date) - nest = ((m == @trait(nestingbegin)[1] && d >= @trait(nestingbegin)[2] - && @chance(0.05)) || (m > @trait(nestingbegin)[1])) + nest = ((m == self.nestingbegin[1] && d >= self.nestingbegin[2] + && @chance(0.05)) || (m > self.nestingbegin[1])) nest && @setphase(nestbuilding) return end # look for a mate among the neighbouring birds, or move randomly for n in @neighbours(50) - if n.sex != @trait(sex) && n.phase == "mating" && n.mate == -1 - @trait(mate) = n.id - n.mate = @trait(id) + if n.sex != self.sex && n.phase == "mating" && n.mate == -1 + self.mate = n.id + n.mate = self.id @debug "$(animalid(animal)) and $(animalid(n)) have mated." return end @@ -155,42 +152,38 @@ At the moment, this implementation is still in development. Females select a location and build a nest. Males do nothing. (Sound familiar?) """ @phase nestbuilding begin - if !@isalive(@trait(mate)) + if !@isalive(self.mate) @setphase(nonbreeding) return end - if @trait(sex) == female - if isempty(@trait(nest)) + if self.sex == female + if isempty(self.nest) # try to find a nest in the neighbourhood, or move on - nestlocation = @randompixel(10, @trait(habitats)) + nestlocation = @randompixel(10, self.habitats) if isnothing(nestlocation) @randomwalk(20) else # if we've found a location, start the clock on the building time # (building time doubles for the first nest of the year) - @trait(nest) = nestlocation - @trait(nestcompletion) = @rand(nestbuildingtime) - month(model.date) == 4 && (@trait(nestcompletion) *= 2) + self.nest = nestlocation + self.nestcompletion = @rand(nestbuildingtime) + month(model.date) == 4 && (self.nestcompletion *= 2) @debug "$(animalid(animal)) is building a nest." end else # wait while nest is being built, then lay eggs and go to next phase - if @trait(nestcompletion) > 0 - @trait(nestcompletion) -= 1 + if self.nestcompletion > 0 + self.nestcompletion -= 1 else #XXX more accurately, a female lays one egg per day, not all at once - @trait(clutch) = @reproduce(@trait(mate), @rand(eggsperclutch)) - @animal(@trait(mate)).clutch = @trait(clutch) - for c in @trait(clutch) #FIXME find a cleaner solution for this - initskylark(@animal(c), model) - end + @reproduce(@rand(self.eggsperclutch),self.mate) @setphase(breeding) end end else # males stay near the female - @follow(model[@trait(mate)], 5) - @animal(@trait(mate)).phase == "breeding" && @setphase(breeding) + @follow(model[self.mate], 5) + @animal(self.mate).phase == "breeding" && @setphase(breeding) end end @@ -199,32 +192,31 @@ At the moment, this implementation is still in development. """ @phase breeding begin #TODO forage (move random) - for offspring in @trait(clutch) + for offspring in self.clutch # check if offspring are still alive and juvenile, else remove from clutch if !@isalive(offspring) || @animal(offspring).phase == "nonbreeding" - deleteat!(@trait(clutch), findfirst(x->x==offspring, @trait(clutch))) + deleteat!(self.clutch, findfirst(x->x==offspring, self.clutch)) end end # if all young have fledged, move to nonbreeding (if it's July) or breed again - if isempty(@trait(clutch)) - @trait(nest) = () - month(model.date) >= 7 ? @setphase(nonbreeding) : @setphase(nestbuilding) + if isempty(self.clutch) + self.nest = () + month(model.date) >= 7 ? + @setphase(nonbreeding) : + @setphase(nestbuilding) end end end """ - initskylark(skylark, model) - -Initialise a skylark individual. Selects migration dates and checks if the -bird should currently be on migration. Also sets other individual-specific -variables. Called at model initialisation and when an egg is laid. +Initialise a skylark individual. Selects migration dates and checks if the bird +should currently be on migration. Also sets other individual-specific variables. """ -function initskylark(animal::Animal, model::AgentBasedModel) - @debug "Added $(animalid(animal)) at $(animal.pos)" +@phase Skylark.initindividual begin + @debug "Added $(animalid(self)) at $(self.pos)" # calculate migration dates for this individual - animal.migrationdates = migrationdates(animal, model) - leave, arrive = animal.migrationdates + self.migrationdates = migrationdates(self, model) + leave, arrive = self.migrationdates m, d = monthday(model.date) migrate = (((m < arrive[1]) || (m == arrive[1] && d < arrive[2])) || ((m > leave[1]) || (m == leave[1] && d >= leave[2]))) @@ -234,8 +226,8 @@ function initskylark(animal::Animal, model::AgentBasedModel) @migrate(returndate) end # set individual life-history parameters that are defined as ranges for the species - @trait(nestlingtime) = @rand(@trait(nestlingtime)) #FIXME no effect? - @trait(fledglingtime) = @rand(@trait(fledglingtime)) + self.nestlingtime = @rand(self.nestlingtime) + self.fledglingtime = @rand(self.fledglingtime) #TODO other stuff? end @@ -245,7 +237,7 @@ end Select the dates on which this skylark will leave for / return from its migration, based on observed migration patterns. """ -function migrationdates(skylark::Animal, model::AgentBasedModel) +function migrationdates(skylark::Animal, model::SimulationModel) #TODO this ought to be temperature-dependent and dynamic #XXX magic numbers! minleave = skylark.sex == female ? (9, 15) : (10, 1) diff --git a/src/nature/species/wolpertinger.jl b/src/nature/species/wolpertinger.jl index 4f5c14c..0348d58 100644 --- a/src/nature/species/wolpertinger.jl +++ b/src/nature/species/wolpertinger.jl @@ -11,29 +11,30 @@ It is purported to have the body of a hare, the wings of a bird, and the antlers of a deer. """ @species Wolpertinger begin - popsize = Int(round(1/100000*reduce(*, size(model.landscape)))) + popdensity = 100000 fecundity = 0.02 mortality = 0.015 maxspeed = 5 crowding = maxspeed*2 - initialise! = initrandompopulation(popsize) + @initialise(popdensity=popdensity) +end - """ - Wolpertingers are rather stupid creatures, all they do is move around randomly - and occasionally reproduce by spontaneous parthogenesis... - """ - @phase lifephase begin - direction = Tuple(@rand([-1,1], 2)) - for i in 1:@rand(1:@trait(maxspeed)) - walk!(animal, direction, model; ifempty=false) - end - if @rand() < @trait(fecundity) && - @countanimals(species="Wolpertinger") < @trait(crowding) - @reproduce(-1) - end +""" +Wolpertingers are rather stupid creatures, all they do is move around randomly +and occasionally reproduce by spontaneous parthogenesis... +""" +@phase Wolpertinger.lifephase begin + direction = Tuple(@rand([-1,1], 2)) + for i in 1:@rand(1:self.maxspeed) + walk!(animal, direction, model; ifempty=false) + end - @kill @trait(mortality) + if @rand() < self.fecundity && + @countanimals(species="Wolpertinger") < self.crowding + @reproduce(-1) end + + @kill self.mortality end diff --git a/src/nature/species/wyvern.jl b/src/nature/species/wyvern.jl index 7becb71..9fa3d7e 100644 --- a/src/nature/species/wyvern.jl +++ b/src/nature/species/wyvern.jl @@ -18,64 +18,63 @@ legs, but that doesn't make it any less dangerous... aggression = 0.2 huntsuccess = 0.8 - @initialise(@habitat(@landcover() in (grass, soil, agriculture, builtup)), popsize=popsize) - phase = "winter" + @initialise(winter, + habitat=@habitat(@landcover() in (grass, soil, agriculture, builtup)), + popsize=popsize) +end - """ - Wyverns are ferocious hunters, scouring the landscape for their favourite - prey: wolpertingers... - """ - @phase summer begin - for a in @neighbours(@trait(speed)) - # check if a wolpertinger is in pouncing distance - if a.traits["name"] == "Wolpertinger" - move_agent!(animal, a.pos, model) - if @rand() < @trait(huntsuccess) - @debug "$(animalid(animal)) killed $(animalid(a))." - kill_agent!(a, model) - @goto reproduce - end - elseif a.traits["name"] == "Wyvern" && @rand() < @trait(aggression) - # wyverns also fight against each other if they get too close - move_agent!(animal, a.pos, model) - outcome = @rand() - if outcome < 0.4 - @debug "$(animalid(animal)) killed $(animalid(a)) in a fight." - kill_agent!(a, model) - elseif outcome < 0.8 - @kill 1.0 "wounds sustained in a fight" - end +""" +Wyverns are ferocious hunters, scouring the landscape for their favourite +prey: wolpertingers... +""" +@phase Wyvern.summer begin + for a in @neighbours(self.speed) + # check if a wolpertinger is in pouncing distance + if typeof(a) == Wolpertinger + move_agent!(self, a.pos, model) + if @rand() < self.huntsuccess + @debug "$(animalid(self)) killed $(animalid(a))." + kill!(a, model) @goto reproduce end - end - # check if a wolpertinger is in seeing distance, or walk in a random direction - direction = Tuple(@rand([-1,1], 2)) - for a in @neighbours(@trait(vision)) - if a.traits["name"] == "Wolpertinger" - direction = get_direction(animal.pos, a.pos, model) - break + elseif typeof(a) == Wyvern && @rand() < self.aggression + # wyverns also fight against each other if they get too close + move_agent!(self, a.pos, model) + outcome = @rand() + if outcome < 0.4 + @debug "$(animalid(self)) killed $(animalid(a)) in a fight." + kill_agent!(a, model) + elseif outcome < 0.8 + @kill 1.0 "wounds sustained in a fight" end - end - for i in 1:@trait(speed) - walk!(animal, direction, model; ifempty=false) + @goto reproduce end - # reproduce every once in a blue moon - @label reproduce - (@rand() < @trait(fecundity)) && @reproduce(-1) - # hibernate from November to March - if monthday(model.date) == (11,1) - @trait(phase) = "winter" + end + # check if a wolpertinger is in seeing distance, or walk in a random direction + direction = Tuple(@rand([-1,1], 2)) + for a in @neighbours(self.vision) + if typeof(a) == Wolpertinger + direction = get_direction(self.pos, a.pos, model) + break end - (@trait(age) == maxage) && @kill(1.0, "old age") + end + for i in 1:self.speed + walk!(animal, direction, model; ifempty=false) end + # reproduce every once in a blue moon + @label reproduce + (@rand() < self.fecundity) && @reproduce(-1) + # hibernate from November to March + month(model.date) >= 11 && (@setphase(winter)) + (self.age == maxage) && @kill(1.0, "old age") +end - """ - Fortunately, wyverns hibernate in winter. - """ - @phase winter begin - # hibernate from November to March - if monthday(model.date) == (3,1) - @trait(phase) = "summer" - end +""" +Fortunately, wyverns hibernate in winter. +""" +@phase Wyvern.winter begin + # hibernate from November to March + if month(model.date) >= 3 + @setphase(summer) end end diff --git a/src/parameters.toml b/src/parameters.toml index e80a55c..c3d9f57 100644 --- a/src/parameters.toml +++ b/src/parameters.toml @@ -30,7 +30,7 @@ weatherfile = "data/regions/jena-small/weather.csv" # location of the weather da farmmodel = "FieldManager" # which version of the farm model to use (not yet implemented) [nature] -targetspecies = ["Skylark"] # list of target species to simulate +targetspecies = ["Wolpertinger", "Wyvern"]#["Skylark"] # 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 = ["season", "habitat", "pesticides", "weather"] # factors affecting insect growth diff --git a/src/world/landscape.jl b/src/world/landscape.jl index 220e51f..50d93ad 100644 --- a/src/world/landscape.jl +++ b/src/world/landscape.jl @@ -70,7 +70,7 @@ end Cycle through the list of events, removing those that have expired. """ -function updateevents!(model::AgentBasedModel) +function updateevents!(model::SimulationModel) expiredevents = [] for e in 1:length(model.events) event = model.events[e] @@ -91,7 +91,7 @@ end Add a farm event to the specified pixels (a vector of position tuples) for a given duration. """ -function createevent!(model::AgentBasedModel, pixels::Vector{Tuple{Int64,Int64}}, +function createevent!(model::SimulationModel, pixels::Vector{Tuple{Int64,Int64}}, name::EventType, duration::Int64=1) push!(model.events, FarmEvent(name, pixels, duration)) for p in pixels @@ -104,7 +104,7 @@ end Return the land cover class at this position (utility wrapper). """ -function landcover(pos::Tuple{Int64,Int64}, model::AgentBasedModel) +function landcover(pos::Tuple{Int64,Int64}, model::SimulationModel) model.landscape[pos...].landcover end @@ -113,7 +113,7 @@ end Return the farm plot at this position, or nothing if there is none (utility wrapper). """ -function farmplot(pos::Tuple{Int64,Int64}, model::AgentBasedModel) +function farmplot(pos::Tuple{Int64,Int64}, model::SimulationModel) ismissing(model.landscape[pos...].fieldid) ? nothing : model[model.landscape[pos...].fieldid] end @@ -125,7 +125,7 @@ end Calculate the distance from the given location to the closest location matching the habitat descriptor function. Caution: can be computationally expensive! """ -function distanceto(pos::Tuple{Int64,Int64}, model::AgentBasedModel, habitatdescriptor::Function) +function distanceto(pos::Tuple{Int64,Int64}, model::SimulationModel, habitatdescriptor::Function) #XXX allow testing for multiple habitat types? (habitatdescriptor(pos, model)) && (return 0) dist = 1 @@ -160,7 +160,7 @@ end Calculate the distance from the given location to the closest habitat of the specified type. Caution: can be computationally expensive! """ -function distanceto(pos::Tuple{Int64,Int64}, model::AgentBasedModel, habitattype::LandCover) +function distanceto(pos::Tuple{Int64,Int64}, model::SimulationModel, habitattype::LandCover) # can't use @habitat here because macros.jl is loaded later than this file distanceto(pos, model, function(p,m) landcover(p,m) == habitattype end) end @@ -171,7 +171,7 @@ end Calculate the distance from the given location to the closest neighbouring habitat. Caution: can be computationally expensive! """ -function distancetoedge(pos::Tuple{Int64,Int64}, model::AgentBasedModel) +function distancetoedge(pos::Tuple{Int64,Int64}, model::SimulationModel) # can't use @habitat here because macros.jl is loaded later than this file distanceto(pos, model, function(p,m) landcover(p,m) != landcover(pos, model) end) end @@ -182,7 +182,7 @@ end Find a random pixel within a given `range` of the `position` that matches the habitatdescriptor (create this using [`@habitat`](@ref)). """ -function randompixel(pos::Tuple{Int64,Int64}, model::AgentBasedModel, range::Int64=1, +function randompixel(pos::Tuple{Int64,Int64}, model::SimulationModel, range::Int64=1, habitatdescriptor::Function=(pos,model)->nothing) for x in @shuffle!(collect((pos[1]-range):(pos[1]+range))) for y in @shuffle!(collect((pos[2]-range):(pos[2]+range))) @@ -208,7 +208,7 @@ end Make sure that a given position is within the bounds of the model landscape. """ -function safebounds(pos::Tuple{Int64,Int64}, model::AgentBasedModel) +function safebounds(pos::Tuple{Int64,Int64}, model::SimulationModel) dims = size(model.landscape) x, y = pos x <= 0 && (x = 1) diff --git a/src/world/weather.jl b/src/world/weather.jl index 694586f..8905303 100644 --- a/src/world/weather.jl +++ b/src/world/weather.jl @@ -53,7 +53,7 @@ end Return today's average windspeed in m/s. """ -function windspeed(model::AgentBasedModel) +function windspeed(model::SimulationModel) model.weather[model.date].windspeed end @@ -62,7 +62,7 @@ end Return today's total precipitation in mm. """ -function precipitation(model::AgentBasedModel) +function precipitation(model::SimulationModel) model.weather[model.date].precipitation end @@ -71,7 +71,7 @@ end Return today's sunshine duration in hours. """ -function sunshine(model::AgentBasedModel) +function sunshine(model::SimulationModel) model.weather[model.date].sunshine end @@ -80,7 +80,7 @@ end Return today's average vapour pressure in hPa. """ -function vapourpressure(model::AgentBasedModel) +function vapourpressure(model::SimulationModel) model.weather[model.date].vapourpressure end @@ -89,7 +89,7 @@ end Return today's mean temperature in °C. """ -function meantemp(model::AgentBasedModel) +function meantemp(model::SimulationModel) model.weather[model.date].meantemp end @@ -98,7 +98,7 @@ end Return today's maximum temperature in °C. """ -function maxtemp(model::AgentBasedModel) +function maxtemp(model::SimulationModel) model.weather[model.date].maxtemp end @@ -107,6 +107,6 @@ end Return today's minimum temperature in °C. """ -function mintemp(model::AgentBasedModel) +function mintemp(model::SimulationModel) model.weather[model.date].mintemp end -- GitLab