From dbff4414cae32e46a4a1d835af9d68e7c518a836 Mon Sep 17 00:00:00 2001 From: Daniel Vedder <daniel.vedder@idiv.de> Date: Sat, 21 Jan 2023 15:06:40 +0100 Subject: [PATCH] Changed model RNG to StableRNG and enforced its use --- run.sh | 2 +- src/Persephone.jl | 3 ++- src/core/input.jl | 2 +- src/core/output.jl | 2 +- src/core/simulation.jl | 11 +++++------ src/nature/ecologicaldata.jl | 15 ++++++++------- src/nature/nature.jl | 13 ++++++++++++- src/nature/populations.jl | 10 +++++----- src/nature/species/wolpertinger.jl | 5 +++-- src/nature/species/wyvern.jl | 10 +++++----- src/parameters.toml | 4 ++-- 11 files changed, 45 insertions(+), 32 deletions(-) diff --git a/run.sh b/run.sh index 7cd3d64..fefbafe 100755 --- a/run.sh +++ b/run.sh @@ -6,6 +6,6 @@ then rm -r temp_results fi -./run.jl -o temp_results -s 2 +./run.jl -o temp_results src/analysis/analyse_nature.R temp_results diff --git a/src/Persephone.jl b/src/Persephone.jl index 1369288..3d86d2d 100644 --- a/src/Persephone.jl +++ b/src/Persephone.jl @@ -21,6 +21,7 @@ using LoggingExtras, #MacroTools, #http://fluxml.ai/MacroTools.jl/stable/utilities/ Random, + StableRNGs, TOML ## define exported functions and variables @@ -56,7 +57,7 @@ export initialise, stepsimulation!, createevent!, - finalise + finalise! ## include all module files (note that the order matters - if file ## b references something from file a, it must be included later) diff --git a/src/core/input.jl b/src/core/input.jl index 872486c..814d72d 100644 --- a/src/core/input.jl +++ b/src/core/input.jl @@ -33,7 +33,7 @@ Precedence: commandline parameters - user config file - default values function getsettings(configfile::String, seed::Union{Int64,Nothing}=nothing) # read in and merge configurations from the commandline, the default config file # and a user-supplied config file - defaults = TOML.parsefile(configfile) + defaults::Dict{String, Dict{String, Any}} = TOML.parsefile(configfile) commandline = parsecommandline() if haskey(commandline, "configfile") && isfile(commandline["configfile"]) configs = TOML.parsefile(commandline["configfile"]) diff --git a/src/core/output.jl b/src/core/output.jl index dca2801..d0d7626 100644 --- a/src/core/output.jl +++ b/src/core/output.jl @@ -42,7 +42,7 @@ function setupdatadir(model::AgentBasedModel) simulationlogger = TeeLogger(ConsoleLogger(logfile, loglevel), ConsoleLogger(stdout, loglevel)) global_logger(simulationlogger) - @debug "Setting up output directory $(@param(core.outdir))" + @debug "Setting up output directory $(@param(core.outdir))." # Export a copy of the current parameter settings to the output folder. # This can be used to replicate this exact run in future, and also # records the current time and git commit. diff --git a/src/core/simulation.jl b/src/core/simulation.jl index 05a90d9..d47c372 100644 --- a/src/core/simulation.jl +++ b/src/core/simulation.jl @@ -20,7 +20,6 @@ configuration file and overriding the `seed` parameter. function initialise(config::String=PARAMFILE, seed::Union{Int64,Nothing}=nothing) @info "Simulation run started at $(Dates.now())." settings = getsettings(config, seed) - Random.seed!(settings["core"]["seed"]) events = Vector{FarmEvent}() dataoutputs = Vector{DataOutput}() landscape = initlandscape(settings["core"]["landcovermap"], settings["core"]["farmfieldsmap"]) @@ -32,7 +31,7 @@ function initialise(config::String=PARAMFILE, seed::Union{Int64,Nothing}=nothing :events=>events) @debug "Setting up model." model = AgentBasedModel(Union{Farmer,Animal,FarmPlot}, space, properties=properties, - rng=Random.Xoshiro(settings["core"]["seed"]), warn=false) + rng=StableRNG(settings["core"]["seed"]), warn=false) setupdatadir(model) initfarms!(model) initfields!(model) @@ -61,15 +60,15 @@ function stepsimulation!(model::AgentBasedModel) end """ - finalise(model) + finalise!(model) Wrap up the simulation. Currently doesn't do anything except print some information. """ -function finalise(model::AgentBasedModel) +function finalise!(model::AgentBasedModel) @info "Simulated $(model.date-@param(core.startdate))." @info "Simulation run completed at $(Dates.now()),\nwrote output to $(@param(core.outdir))." #XXX is there anything to do here? - #genocide!(model) + model end """ @@ -82,5 +81,5 @@ function simulate(config::String=PARAMFILE, seed::Union{Int64,Nothing}=nothing) model = initialise(config, seed) runtime = Dates.value(@param(core.enddate)-@param(core.startdate))+1 step!(model, dummystep, stepsimulation!, runtime) - finalise(model) + finalise!(model) end diff --git a/src/nature/ecologicaldata.jl b/src/nature/ecologicaldata.jl index 564097a..aa21296 100644 --- a/src/nature/ecologicaldata.jl +++ b/src/nature/ecologicaldata.jl @@ -21,9 +21,10 @@ end """ savepopulationdata(model) -Print a comma-separated set of lines to `populations.csv`, giving the current date -and population size for each animal species. May be called never, daily, monthly, -yearly, or at the end of a simulation, depending on the parameter `nature.popoutfreq`. +Return a comma-separated set of lines (to be printed to `populations.csv`), giving +the current date and population size for each animal species. May be called never, +daily, monthly, yearly, or at the end of a simulation, depending on the parameter +`nature.popoutfreq`. """ function savepopulationdata(model::AgentBasedModel) pops = Dict{String,Int}(s=>0 for s = @param(nature.targetspecies)) @@ -41,10 +42,10 @@ end """ saveindividualdata(model) -Print a comma-separated set of lines to `individuals.csv`, listing all properties -of all animal individuals in the model. May be called never, daily, monthly, yearly, or -at the end of a simulation, depending on the parameter `nature.indoutfreq`. -WARNING: Produces very big files! +Return a comma-separated set of lines (to be printed to `individuals.csv`), listing +all properties of all animal individuals in the model. May be called never, daily, +monthly, yearly, or at the end of a simulation, depending on the parameter +`nature.indoutfreq`. WARNING: Produces very big files! """ function saveindividualdata(model::AgentBasedModel) data = "" diff --git a/src/nature/nature.jl b/src/nature/nature.jl index 3606cd1..8010f13 100644 --- a/src/nature/nature.jl +++ b/src/nature/nature.jl @@ -156,7 +156,7 @@ variables: Several utility macros can be used within the body of `@phase` as a short-hand for common expressions: `@trait`, `@setphase`, `@respond`, `@here`, `@kill`, -`@reproduce`, `@neighbours`. +`@reproduce`, `@neighbours`, `@rand`. 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 @@ -197,6 +197,7 @@ end Switch this animal over to a different phase. This can only be used nested within `@phase`. """ macro setphase(newphase) + #XXX make this usable in the top part of a species definition? :($(esc(:animal)).traits["phase"] = $(String(newphase))) end @@ -364,4 +365,14 @@ macro countanimals(args...) :(countanimals($(esc(:pos)), $(esc(:model)); $(map(esc, args)...))) end +""" + @rand(args...) + +Return a random number or element from the sample, using the model RNG. +This is a utility wrapper that can only be used nested within `@phase` or `@habitat`. +""" +macro rand(args...) + :(rand($(esc(:model)).rng, $(map(esc, args)...))) +end + ##TODO add movement macros diff --git a/src/nature/populations.jl b/src/nature/populations.jl index dbe6924..a760088 100644 --- a/src/nature/populations.jl +++ b/src/nature/populations.jl @@ -40,15 +40,15 @@ function initpopulation(habitatdescriptor::Function; phase::Union{String,Nothing (!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)) + for x in shuffle!(model.rng, Vector(1:width)) + for y in shuffle!(model.rng, Vector(1:height)) if habitatdescriptor((x,y), model) if pairs add_agent!((x,y), Animal, model, deepcopy(species), female, 0) add_agent!((x,y), Animal, model, deepcopy(species), male, 0) n += 2 else - sex = asexual ? hermaphrodite : rand([male, female]) + sex = asexual ? hermaphrodite : rand(model.rng, [male, female]) add_agent!((x,y), Animal, model, deepcopy(species), sex, 0) n += 1 end @@ -88,7 +88,7 @@ Produce one or more offspring for the given animal at its current location. """ function reproduce!(animal::Animal, model::AgentBasedModel, n::Int64=1) for i in 1:n - sex = (animal.sex == hermaphrodite) ? hermaphrodite : rand([male, female]) + sex = (animal.sex == hermaphrodite) ? hermaphrodite : rand(model.rng, [male, female]) # We need to generate a fresh species dict here species = @eval $(Symbol(animal.traits["name"]))($model) add_agent!(animal.pos, Animal, model, species, sex, 0) @@ -103,7 +103,7 @@ 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="") - if rand() < probability + if rand(model.rng) < probability postfix = isempty(cause) ? "." : " from $cause." @debug "$(animalid(animal)) has died$(postfix)" kill_agent!(animal, model) diff --git a/src/nature/species/wolpertinger.jl b/src/nature/species/wolpertinger.jl index a4bc779..af1f1b6 100644 --- a/src/nature/species/wolpertinger.jl +++ b/src/nature/species/wolpertinger.jl @@ -25,11 +25,12 @@ of a deer. """ @phase lifephase begin direction = Tuple(rand([-1,1], 2)) - for i in 1:rand(1:@trait(maxspeed)) + for i in 1:@rand(1:@trait(maxspeed)) walk!(animal, direction, model; ifempty=false) end - if rand() < @trait(fecundity) && @countanimals(species="Wolpertinger") < @trait(crowding) + if @rand() < @trait(fecundity) && + @countanimals(species="Wolpertinger") < @trait(crowding) @reproduce end diff --git a/src/nature/species/wyvern.jl b/src/nature/species/wyvern.jl index dc6be0b..f5a8fde 100644 --- a/src/nature/species/wyvern.jl +++ b/src/nature/species/wyvern.jl @@ -30,15 +30,15 @@ legs, but that doesn't make it any less dangerous... # check if a wolpertinger is in pouncing distance if a.traits["name"] == "Wolpertinger" move_agent!(animal, a.pos, model) - if rand() < @trait(huntsuccess) + 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) + 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() + outcome = @rand() if outcome < 0.4 @debug "$(animalid(animal)) killed $(animalid(a)) in a fight." kill_agent!(a, model) @@ -49,7 +49,7 @@ legs, but that doesn't make it any less dangerous... end end # check if a wolpertinger is in seeing distance, or walk in a random direction - direction = Tuple(rand([-1,1], 2)) + 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) @@ -61,7 +61,7 @@ legs, but that doesn't make it any less dangerous... end # reproduce every once in a blue moon @label reproduce - (rand() < @trait(fecundity)) && @reproduce + (@rand() < @trait(fecundity)) && @reproduce # hibernate from November to March if monthday(model.date) == (11,1) @trait(phase) = "winter" diff --git a/src/parameters.toml b/src/parameters.toml index d028184..9f8f8e0 100644 --- a/src/parameters.toml +++ b/src/parameters.toml @@ -13,10 +13,10 @@ farmfieldsmap = "data/fields_jena.tif" # location of the field geometry map outdir = "results" # location and name of the output folder overwrite = "ask" # overwrite the output directory? (true/false/"ask") loglevel = "debug" # verbosity level: "debug", "info", "quiet" -seed = 0 # seed value for the RNG (0 -> random value) +seed = 2 # seed value for the RNG (0 -> random value) # dates to start and end the simulation startdate = 2022-01-01 -#enddate = 2022-01-02 +#enddate = 2022-03-31 enddate = 2022-12-31 [farm] -- GitLab