diff --git a/src/Persefone.jl b/src/Persefone.jl index e256782393a29abb05b29b8d26529f4528a3a802..aae3d8d88a8f49c65d5170128eae1d9bc75388e0 100644 --- a/src/Persefone.jl +++ b/src/Persefone.jl @@ -23,7 +23,7 @@ using Distributed, FileIO, #FIXME an upstream update broke GeoArrays for TableTransforms > 1.15.0 - GeoArrays, + GeoArrays, #XXX this is another big dependency Logging, LoggingExtras, Random, @@ -129,7 +129,7 @@ include("nature/ecologicaldata.jl") # end #include("nature/species/skylark.jl") include("nature/species/wolpertinger.jl") -include("nature/species/wyvern.jl") +#include("nature/species/wyvern.jl") include("core/simulation.jl") #this must be last diff --git a/src/core/output.jl b/src/core/output.jl index e4bdf00a007ef23e7aaffae532abaa5ad12673d1..6d1fc2d533ec47269c8dbd82403d51d63759f84b 100644 --- a/src/core/output.jl +++ b/src/core/output.jl @@ -80,7 +80,7 @@ function saveinputfiles(model::SimulationModel) println(f, "# Simulation run on $(string(Dates.format(Dates.now(), "d u Y HH:MM:SS"))),") # Record the current git commit and versions of dependencies for reproducibility println(f, "# with Persefone $(pkgversion(Persefone)), git commit $(currentcommit),") - println(f, "# running on Julia $(VERSION) with Agents.jl $(pkgversion(Agents)).\n#\n") + println(f, "# running on Julia $(VERSION).\n#\n") if !isempty(strip(read(`git status -s`, String))) println(f, "# WARNING: Your repository contains uncommitted changes. This may") println(f, "# compromise the reproducibility of this simulation run.\n") diff --git a/src/core/simulation.jl b/src/core/simulation.jl index d139fdf37b9e1553a63b4787dcd6f7fbc23d7cdb..d6552a6d75cea2729f816dd99da2f9f655287752 100644 --- a/src/core/simulation.jl +++ b/src/core/simulation.jl @@ -24,7 +24,7 @@ mutable struct AgricultureModel <: SimulationModel crops::Dict{String,CropType} farmers::Vector{Farmer} farmplots::Vector{FarmPlot} - animals::Vector{Union{Animals,Nothing}} + animals::Vector{Union{Animal,Nothing}} migrants::Vector{Pair{Animal,Date}} events::Vector{FarmEvent} end @@ -61,8 +61,9 @@ Carry out a complete simulation run using a pre-initialised model object. 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) - step!(model, dummystep, stepsimulation!, runtime) + for d in 1:runtime + stepsimulation!(model) + end finalise!(model) end @@ -115,7 +116,7 @@ function initmodel(settings::Dict{String, Any}) crops, Vector{Farmer}(), Vector{FarmPlot}(), - Vector{Union{Animals,Nothing}}(), + Vector{Union{Animal,Nothing}}(), Vector{Pair{Animal, Date}}(), Vector{FarmEvent}()) saveinputfiles(model) @@ -172,7 +173,7 @@ function stepsimulation!(model::SimulationModel) end updatenature!(model) updateevents!(model) - outputdata(model) + #outputdata(model) #FIXME model.date += Day(1) model end @@ -181,13 +182,13 @@ end """ finalise!(model) -Wrap up the simulation. Currently doesn't do anything except print some information. +Wrap up the simulation. Finalises and visualises output, then terminates. """ 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))." - @param(core.visualise) && visualiseoutput(model) + #@param(core.visualise) && visualiseoutput(model) #FIXME model end end diff --git a/src/crop/farmplot.jl b/src/crop/farmplot.jl index 11605963d9aec0c92fc6e83f5554da3c33ef27dc..328b5bc3db2122365abfcd6f066e382433771a0d 100644 --- a/src/crop/farmplot.jl +++ b/src/crop/farmplot.jl @@ -11,7 +11,7 @@ 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. """ -mutable struct FarmPlot +mutable struct FarmPlot <: ModelAgent const id::Int64 pixels::Vector{Tuple{Int64, Int64}} croptype::CropType diff --git a/src/nature/ecologicaldata.jl b/src/nature/ecologicaldata.jl index 554f8ae6db157c58be578697e60b639170663dde..449895f6d879bed6e1869a8a3650ef71e98febc5 100644 --- a/src/nature/ecologicaldata.jl +++ b/src/nature/ecologicaldata.jl @@ -24,13 +24,14 @@ daily, monthly, yearly, or at the end of a simulation, depending on the paramete `nature.popoutfreq`. """ function savepopulationdata(model::SimulationModel) + #FIXME pops = Dict{String,Int}(s=>0 for s = @param(nature.targetspecies)) for a in model.animals isnothing(a) && continue - pops[a.name] += 1 + pops[string(typeof(a))] += 1 end - for a in model.migrants - pops[a.first.name] += 1 + for m in model.migrants + pops[string(typeof(m.first))] += 1 end data = [] for p in keys(pops) diff --git a/src/nature/macros.jl b/src/nature/macros.jl index e5072901a421de8d15ef1fddce3f59a633590141..67a1abd60e01f55cad661f66427abf6b97b278da 100644 --- a/src/nature/macros.jl +++ b/src/nature/macros.jl @@ -12,8 +12,7 @@ ## https://github.com/p-i-/MetaGuideJulia/wiki#example-swap-macro-to-illustrate-esc). ## Hence all the `esc`apes in the following code - take care when modifying! -##TODO update documentation - +##FIXME update documentation """ @species(name, body) @@ -48,39 +47,51 @@ the `model` variable (an object of type `SimulationModel`). """ macro species(name, body) quote - Core.@__doc__ @kwdef mutable struct $(esc(name)) <: Animal + #XXX species are created/referenced as Persefone.<speciesname>, is this relevant? + @kwdef mutable struct $(name) <: Animal const id::Int64 - const sex::Sex - const parents::Tuple{Int64,Int64} #XXX assumes sexual reprod. + const sex::Sex = hermaphrodite + const parents::Tuple{Int64,Int64} = (-1, -1) #XXX assumes sexual reprod. pos::Tuple{Int64,Int64} - phase::Function + phase::Function = ()->0 age::Int = 0 energy::Union{EnergyBudget,Nothing} = nothing # DEB is optional offspring::Vector{Int64} = Vector{Int64}() - $(esc(body)) + $(body.args...) end - # define a zero-argument constructor to access default field values - $(esc(name))() = $(esc(name))(-1, hermaphrodite, (-1, -1), (-1, -1), ()->0) + # define a constructor giving the minimum necessary arguments as positional arguments + $(esc(name))(id, sex, parents, pos, phase) = + $(esc(name))(id=id, sex=sex, parents=parents, pos=pos, phase=phase) + # define a zero-argument constructor to access default field values #XXX probably not needed + #$(esc(name))(id) = $(esc(name))(id=id, parents=(-1, -1), pos=(-1, -1)) # 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))) + (@__MODULE__() != $(esc(:Persefone))) && ($(esc(:Persefone)).$name = $(name)) end end """ - @initialise(habitatdescriptor; kwargs...) - -Call this macro within the body of [`@species`](@ref). It passes the given habitat -descriptor function and keyword arguments on to [`initpopulation`](@ref) when setting -up the simulation. + @populate(species, params) -Note: if this macro is not used, the variable `initialise!` must be set manually in the -species definition. +Set the parameters that are used to initialise this species' population. +For parameter options, see [`PopInitParams`](@ref). """ -macro initialise(phase, kwargs...) - :($(esc(:initialise!)) = initpopulation($(esc(phase)); $(map(esc, kwargs)...))) +macro populate(species, params) + #TODO I think I can make this simpler by using parametric methods + # (see https://docs.julialang.org/en/v1/manual/methods/#Parametric-Methods, + # https://stackoverflow.com/questions/42283820/julia-difference-between-type-and-datatype) + println("Will register $(species).") + quote + # convert the macro body to a dict + Core.@__doc__ fun = function() # metaprogramming is great - this is a fun fun function ;-) + $(esc(params)) + return Base.@locals + end + registerpopulationparams($(esc(species)), fun()) + end end +#FIXME update documentation """ @phase(name, body) @@ -94,8 +105,8 @@ such phase, and the conditions under which the animal transitions to another pha `@phase` works by creating a function that will be called by the model if the animal is in the relevant phase. When it is called, it has access to the following variables: -- `animal` a reference to the animal itself. This provides access to `animal.age`, - `animal.sex`, and `animal.<trait>` (where <trait> is a variable that was defined +- `self` a reference to the animal itself. This provides access to `animal.age`, + `self.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 `SimulationModel`). @@ -113,12 +124,32 @@ 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(phaseid, body) - species = String(phaseid.args[1]) - phase = String(phaseid.args[2].value) +macro phase(species, phase, body) + quote + Core.@__doc__ function $(esc(phase))($(esc(:self))::$(species), + $(esc(:model))::SimulationModel) + $(esc(:pos)) = $(esc(:self)).pos # needed for landscape macros + $(esc(body)) + end + end +end + +""" + @create(species, body) + +Define a special phase function ([`create!`](@ref)()) that will be called when an +individual of this species is created, at the initialisation of the simulation +or at birth. + +As for [`@phase`](@ref), the body of this macro has access to the variables +`self` (the individual being created) and `model` (the simulation world), and +can thus use all macros available in [`@phase`](@ref). +""" +macro create(species, body) quote - Core.@__doc__ function $(esc(phase))(self::$(esc(species)), - model::SimulationModel) + #XXX species are created/referenced as Persefone.<speciesname>, is this relevant? + Core.@__doc__ function $(esc(:create!))($(esc(:self))::$(esc(species)), + $(esc(:model))::SimulationModel) $(esc(:pos)) = $(esc(:self)).pos # needed for landscape macros $(esc(body)) end @@ -209,7 +240,6 @@ 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(:self)), $(esc(:model)), $(esc(radius)))) end @@ -334,7 +364,7 @@ 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)), $(map(esc, args)...))) + :(countanimals($(esc(:pos)), $(esc(:model)); $(map(esc, args)...))) end ##TODO test movement macros @@ -346,8 +376,7 @@ 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) - #FIXME remove Agents.jl code - :(move_agent!($(esc(:self)), $(esc(position)), $(esc(:model)))) + :(move!($(esc(:self)), $(esc(:model)), $(esc(position)))) end """ @@ -359,9 +388,8 @@ 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(:self)), $(esc(direction)), $(esc(:model)))) + :(walk!($(esc(:self)), $(esc(:model)), $(esc(direction)))) end """ diff --git a/src/nature/nature.jl b/src/nature/nature.jl index b50dd8fbce17faba35b68b67a3495cc67378987e..b1aeb9ec696b7819d8ecba8af800bb7605657524 100644 --- a/src/nature/nature.jl +++ b/src/nature/nature.jl @@ -13,7 +13,8 @@ Animal This is the generic agent type for all animals. Individual species are created -using the [`@species`](@ref) macro. All species contain the following fields: +using the [`@species`](@ref) macro. In addition to user-defined, species-specific +fields, all species contain the following fields: - `id` An integer unique identifier for this individual. - `sex` male, female, or hermaphrodite. @@ -44,6 +45,17 @@ function animalid(a::Animal) return "$(speciesof(a)) $(a.id)" end +""" + create!(animal, model) + +The `create!` function is called for every individual at birth or at model initialisation. +Species must use [`@create`](@ref) to define a species-specific method. This is the fall- +back method, in case none is implemented for a species. +""" +function create!(a::Animal, model::SimulationModel) + @warn "Species $(speciesof(a)) has no create!() method. Use `@create` to add one." +end + """ stepagent!(animal, model) @@ -63,7 +75,7 @@ function initnature!(model::SimulationModel) # The config file determines which species are simulated in this run for speciesname in @param(nature.targetspecies) # Call each species' initialisation function - @eval $(Symbol(speciesname))().initialise!($(Symbol(speciesname)), model) + initpopulation!(speciesname, model) end # Initialise the data output initecologicaldata(model) diff --git a/src/nature/populations.jl b/src/nature/populations.jl index d218973752e693dcdac7ac6c293480f8fc517f69..63b49bc3053db4c5c71eb9810ea3cff0e815971e 100644 --- a/src/nature/populations.jl +++ b/src/nature/populations.jl @@ -5,98 +5,119 @@ ### """ - initpopulation(habitatdescriptor; popsize=-1, pairs=false, asexual=false) + PopInitParams -Creates a function that initialises individuals at random locations across the landscape. -This can be used to create the `initialise!` variable in a species definition block. +A set of parameters used by [`initpopulation`](@ref) to initialise the population +of a species at the start of a simulation. Define these parameters for each species +using [`@populate`](@ref). -- `habitatdescriptor` is a function that determines whether a given location is suitable - or not (create this using [`@habitat`](@ref)). +- `phase` determines which life phase individuals will be assigned to (required). -- `phase` determines which life phase individuals will be assigned to. If this is `nothing`, - the species' default post-natal life stage will be used (although note that this is - probably not what you want). +- `habitat` is a function that determines whether a given location is suitable + or not (create this using [`@habitat`](@ref)). By default, every cell will be occupied. -- `popsize` determines the number of individuals that will be created. If this is zero or - negative, one individual will be created in every suitable location in the landscape. - If `popsize` is greater than the number of suitable locations, multiple individuals - will be created in one place. +- `popsize` determines the number of individuals that will be created, dispersed over the + suitable locations in the landscape. If this is zero or negative, one individual will + be created in every suitable location. If it is greater than the number of suitable + locations, multiple individuals will be created per location. Alternately, use `popdensity`. - `popdensity`: if this is greater than zero, the chance of creating an individual (or - pair of individuals) at a suitable location is 1/popdensity. + pair of individuals) at a suitable location is 1/popdensity. Use this as an alternative + to `popsize`. - If `pairs` is true, a male and a female individual will be created in each selected - location, otherwise, only one individual will be created at a time. + location, otherwise, only one individual will be created at a time. (default: false) - 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.) + otherwise, they are randomly assigned `male` or `female`. If `pairs` is true, `asexual` + is ignored. (default: false) """ -function initpopulation(phase::Function; habitat::Function=@habitat(true), - popsize::Int64=-1, popdensity::Int64=-1, pairs::Bool=false, - asexual::Bool=false) - (popsize <= 0 && popdensity <= 0) && #XXX not sure what this would do +@kwdef struct PopInitParams + phase::Function + habitat::Function = @habitat(true) + popsize::Int64 = -1 + popdensity::Int64 = -1 + pairs::Bool = false + asexual::Bool = false +end + +""" +A closure to store the parameters needed to initialise each species' population. +Users register a new set of parameters using [`@populate`](@ref). +""" +#TODO I think I can make this simpler by using parametric methods +# (see https://docs.julialang.org/en/v1/manual/methods/#Parametric-Methods, +# https://stackoverflow.com/questions/42283820/julia-difference-between-type-and-datatype) +let populationparams = Dict{String,Tuple{Type,PopInitParams}}() + + global function getpopulationparams(species::String) + populationparams[species] + end + + global function registerpopulationparams(species::Type{T}, initparams::Dict{Symbol,Any}) where T <: Animal + # convert the dict to an InitParam struct and save it + println("Registering $(species).") + #XXX why is this necessary? + spstrings = split(string(species), ".") # strip out the module name, if necessary + speciesstring = length(spstrings) == 1 ? spstrings[1] : spstrings[2] + populationparams[speciesstring] = (species, PopInitParams(; initparams...)) + #populationparams[species] = NamedTuple(k => v for (k,v) in initparams) + end +end + +""" + initpopulation!(species, model) + +Initialise the population of the given species, based on the parameters stored +in [`PopInitParams`](@ref). Define these using [`@populate`](@ref). +""" +function initpopulation!(species::String, model::SimulationModel) + s, p = getpopulationparams(species) # species constructor and parameters + (p.popsize <= 0 && p.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 - 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 habitat((x,y), model) && - (popdensity <= 0 || @chance(1/popdensity)) #XXX what if pd==0? - if pairs - 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 = @eval($(species))(length(model.animals)+1, sex, - (-1, -1), (x,y), phase) - initindividual(a, model) - push!(model.animals, a) - n += 1 - end + (p.popsize > 0 && p.popdensity > 0) && #XXX not sure what this would do + @warn("initpopulation() called with popsize and popdensity both > 0") + n = 0 + lastn = 0 + width, height = size(model.landscape) + while n == 0 || n < p.popsize + for x in @shuffle!(Vector(1:width)) + for y in @shuffle!(Vector(1:height)) + if p.habitat((x,y), model) && + (p.popdensity <= 0 || @chance(1/p.popdensity)) #XXX what if pd==0? + if p.pairs + a1 = s(length(model.animals)+1, male, (-1, -1), (x,y), p.phase) + a2 = s(length(model.animals)+1, female, (-1, -1), (x,y), p.phase) + create!(a1, model) + create!(a2, model) + push!(model.animals, a1) + push!(model.animals, a2) + n += 2 + else + sex = p.asexual ? hermaphrodite : @rand([male, female]) + a = s(length(model.animals)+1, sex, (-1, -1), (x,y), p.phase) + create!(a, model) + push!(model.animals, a) + n += 1 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 $(string(species)) in the landscape." - break + #XXX break randomly to avoid initialising all individuals in a single column? + (p.popsize > 0 && n >= p.popsize) && break end - lastn = n + (p.popsize > 0 && n >= p.popsize) && break end - @info "Initialised $(n) $(string(species))s." + if lastn == n # prevent an infinite loop - we don't have a Cray... + @warn "There are not enough suitable locations for $(species) in the landscape." + break + end + lastn = n end + @info "Initialised $(n) $(species)s." end #XXX initpopulation with dispersal from an original source? #XXX initpopulation based on known occurences in real-life? -""" - 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. - -Note that this is a stump method. Each species should implement its -own version by defining a [`@phase`](@ref) called `initindividual`. -""" -function initindividual(animal::Animal, model::SimulationModel) - @debug "No initindividual() method for species $(typeof(animal))." -end - """ reproduce!(animal, model, mate, n=1) @@ -107,11 +128,14 @@ function reproduce!(animal::Animal, model::SimulationModel, n::Int64=1, mate::Int64=-1) (animal.sex == male) && @warn "Male $(animalid(animal)) is reproducing." for i in 1:n - sex = (animal.sex == hermaphrodite) ? - hermaphrodite : - @rand([male, female]) - child = @eval($(typeof(animal)))(length(model.animals)+1, sex, - (animal.id, mate), animal.pos) + if animal.sex == hermaphrodite + sex = hermaphrodite + else + sex = @rand([male, female]) + end + #FIXME get birth phase + child = typeof(animal)(length(model.animals)+1, sex, + (animal.id, mate), animal.pos) initindividual(child, model) push!(model.animals, child) push!(animal.offspring, child.id) @@ -147,9 +171,11 @@ 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)) : + if isnothing(i) + push!(model.migrants, Pair(animal, arrival)) + else insert!(model.migrants, i, Pair(animal, arrival)) + end model.animals[animal.id] = nothing @debug "$(animalid(animal)) has migrated." end @@ -169,6 +195,7 @@ end Return an iterator over all animals in the given radius around this position. """ function nearby_animals(pos::Tuple{Int64,Int64}, model::SimulationModel, radius::Int64) + #FIXME remove agents.jl code #TODO enable filtering by species neighbours = (model[id] for id in nearby_ids(pos, model, radius)) Iterators.filter(a -> typeof(a) == Animal, neighbours) @@ -180,6 +207,7 @@ end Return an iterator over all animals in the given radius around this animal, excluding itself. """ function nearby_animals(animal::Animal, model::SimulationModel, radius::Int64) + #FIXME remove agents.jl code #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) @@ -192,7 +220,7 @@ Count the number of animals in this location (optionally supplying a species nam """ function countanimals(pos::Tuple{Int64,Int64}, model::SimulationModel; species::String="", radius::Int64=0) - n = 0 + n = 0 #FIXME fix nearby_animals #XXX can we ignore capitalisation in the spelling of `species`? for a in nearby_animals(pos, model, radius) (species == "" || a.traits["name"] == species) && (n += 1) @@ -213,5 +241,66 @@ function followanimal!(follower::Animal, leader::Animal, model::SimulationModel, move_agent!(follower, targetposition, model) end -##TODO add random walk with habitat descriptor +""" + move!(animal, model, position) + +Move the animal to the given position, making sure that this is in-bounds. +Return true if successful, return false and leave the animal where it is if +the position is out of bounds. +""" +function move!(animal::Animal, model::SimulationModel, position::Tuple{Int64,Int64}) + width, height = size(model.landscape) + if position[1] < 0 || position[2] < 0 || position[1] > width || position[2] > height + #XXX should this be a warning? or just pass silently? + @debug "$(animalid(animal)) tried to move out of bounds to $position." + return false + end + animal.pos = position + return true +end + +""" + walk!(animal, model, direction) + +Let the animal move one step in the given direction ("north", "northeast", +"east", "southeast", "south", "southwest", "west", "northwest", "random"). +""" +function walk!(animal::Animal, model::SimulationModel, direction::String) + if direction == "north" + shift = (0,-1) + elseif direction == "northeast" + shift = (1,-1) + elseif direction == "east" + shift = (1,0) + elseif direction == "southeast" + shift = (1,1) + elseif direction == "south" + shift = (0,1) + elseif direction == "southwest" + shift = (-1,1) + elseif direction == "west" + shift = (-1,0) + elseif direction == "northwest" + shift = (-1,-1) + elseif direction == "random" + shift = Tuple(@rand([-1,1], 2)) + else + @error "Invalid direction in @walk: "*direction + end + move!(animal, model, animal.pos .+ shift) +end +""" + walk!(animal, model, direction) + +Let the animal move in the given direction, where the direction is +defined by an (x, y) tuple to specify the shift in coordinates. +""" +function walk!(animal::Animal, model::SimulationModel, direction::Tuple{Int64,Int64}) + move!(animal, model, animal.pos .+ direction) +end + + + +##TODO add random walk with habitat descriptor +##TODO add walktoward or similar function (incl. pathfinding?) diff --git a/src/nature/species/wolpertinger.jl b/src/nature/species/wolpertinger.jl index 0348d587cc3059eda81757abb341196ba7910bed..20dabb56a2e7b8e5edc073ab3779b33bddddf820 100644 --- a/src/nature/species/wolpertinger.jl +++ b/src/nature/species/wolpertinger.jl @@ -11,30 +11,43 @@ It is purported to have the body of a hare, the wings of a bird, and the antlers of a deer. """ @species Wolpertinger begin - popdensity = 100000 fecundity = 0.02 mortality = 0.015 maxspeed = 5 - crowding = maxspeed*2 - - @initialise(popdensity=popdensity) + crowding = 10 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)) +@phase Wolpertinger lifephase begin for i in 1:@rand(1:self.maxspeed) - walk!(animal, direction, model; ifempty=false) - end - - if @rand() < self.fecundity && - @countanimals(species="Wolpertinger") < self.crowding - @reproduce(-1) + @walk("random") + #walk!(animal, direction, model; ifempty=false) end + + #FIXME need to fix countanimals() and reproduce() first + # if @rand() < self.fecundity && + # @countanimals(species="Wolpertinger") < self.crowding + # @reproduce() + # end @kill self.mortality end + +""" +Wolpertingers are ephemeral creatures that require no special initialisation. +""" +@create Wolpertinger begin + @debug "$(animalid(self)) created." + #TODO +end + +""" +Population densities of the endangered Wolpertinger are down to 1 animal per 10km². +""" +@populate Wolpertinger begin + phase = lifephase + popdensity = 100000 #XXX use Unitful.jl for conversion? +end diff --git a/src/parameters.toml b/src/parameters.toml index c3d9f57600a718ac1dc7d9a3423ed68d62bdf915..60397d728fc19a58334bb77cfa73dc052eb507a0 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 = ["Wolpertinger", "Wyvern"]#["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