Skip to content
Snippets Groups Projects
Commit 47496d12 authored by xo30xoqa's avatar xo30xoqa
Browse files

Added run parallelisation and cleaned up `simulation.jl`

parent 4d7f5127
No related branches found
No related tags found
No related merge requests found
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
julia_version = "1.9.0-alpha1" julia_version = "1.9.0-alpha1"
manifest_format = "2.0" manifest_format = "2.0"
project_hash = "f0da7d5ff50bf02c651315826a4a291410c6b984" project_hash = "6a6afac02132d4ea401777f7459614dc2d5cfa37"
[[deps.AbstractFFTs]] [[deps.AbstractFFTs]]
deps = ["ChainRulesCore", "LinearAlgebra"] deps = ["ChainRulesCore", "LinearAlgebra"]
......
...@@ -7,6 +7,7 @@ version = "0.0.1" ...@@ -7,6 +7,7 @@ version = "0.0.1"
Agents = "46ada45e-f475-11e8-01d0-f70cc89e6671" Agents = "46ada45e-f475-11e8-01d0-f70cc89e6671"
ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63" ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
GeoArrays = "2fb1d81b-e6a0-5fc5-82e6-8e06903437ab" GeoArrays = "2fb1d81b-e6a0-5fc5-82e6-8e06903437ab"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
LoggingExtras = "e6f89c97-d47a-5376-807f-9c37f3926c36" LoggingExtras = "e6f89c97-d47a-5376-807f-9c37f3926c36"
...@@ -16,5 +17,5 @@ StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" ...@@ -16,5 +17,5 @@ StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
[compat] [compat]
julia = ">= 1.9"
Agents = ">= 5.6" Agents = ">= 5.6"
julia = ">= 1.9"
...@@ -16,10 +16,11 @@ using ...@@ -16,10 +16,11 @@ using
Agents, Agents,
ArgParse, ArgParse,
Dates, Dates,
Distributed,
GeoArrays, #XXX this is a big dependency - can we get rid of it? GeoArrays, #XXX this is a big dependency - can we get rid of it?
Logging, Logging,
LoggingExtras, LoggingExtras,
#MacroTools, #http://fluxml.ai/MacroTools.jl/stable/utilities/ #MacroTools, #may be useful: http://fluxml.ai/MacroTools.jl/stable/utilities/
Random, Random,
StableRNGs, StableRNGs,
TOML TOML
......
...@@ -31,24 +31,6 @@ macro param(domainparam) ...@@ -31,24 +31,6 @@ macro param(domainparam)
:($(esc(:model)).settings[$(domain*"."*paramname)]) :($(esc(:model)).settings[$(domain*"."*paramname)])
end end
"""
flattenTOML(dict)
An internal utility function to convert the two-dimensional dict returned
by `TOML.parsefile()` into a one-dimensional dict, so that instead of
writing `settings["domain"]["param"]` one can use `settings["domain.param"]`.
Can be reversed with `expandTOML()`.
"""
function flattenTOML(tomldict)
flatdict = Dict{String, Any}()
for domain in keys(tomldict)
for param in keys(tomldict[domain])
flatdict[domain*"."*param] = tomldict[domain][param]
end
end
flatdict
end
""" """
getsettings(configfile, seed=nothing) getsettings(configfile, seed=nothing)
...@@ -95,12 +77,31 @@ function getsettings(configfile::String, seed::Union{Int64,Nothing}=nothing) ...@@ -95,12 +77,31 @@ function getsettings(configfile::String, seed::Union{Int64,Nothing}=nothing)
settings["core.outdir"] = outdir settings["core.outdir"] = outdir
end end
if settings["core.startdate"] > settings["core.enddate"] if settings["core.startdate"] > settings["core.enddate"]
Base.error("Enddate is earlier than startdate.") Base.error("Enddate is earlier than startdate.") #TODO replace with exception
end end
!isempty(scanparams) && addprocs(settings["core.processors"])
settings["internal.scanparams"] = scanparams settings["internal.scanparams"] = scanparams
settings settings
end end
"""
flattenTOML(dict)
An internal utility function to convert the two-dimensional dict returned
by `TOML.parsefile()` into a one-dimensional dict, so that instead of
writing `settings["domain"]["param"]` one can use `settings["domain.param"]`.
Can be reversed with `expandTOML()`.
"""
function flattenTOML(tomldict)
flatdict = Dict{String, Any}()
for domain in keys(tomldict)
for param in keys(tomldict[domain])
flatdict[domain*"."*param] = tomldict[domain][param]
end
end
flatdict
end
""" """
parsecommandline() parsecommandline()
......
...@@ -45,6 +45,7 @@ function initlandscape(landcovermap::String, farmfieldsmap::String) ...@@ -45,6 +45,7 @@ function initlandscape(landcovermap::String, farmfieldsmap::String)
@debug "Initialising landscape" @debug "Initialising landscape"
landcover = GeoArrays.read(landcovermap) landcover = GeoArrays.read(landcovermap)
farmfields = GeoArrays.read(farmfieldsmap) farmfields = GeoArrays.read(farmfieldsmap)
#TODO replace error with exception
(size(landcover) != size(farmfields)) && Base.error("Input map sizes don't match.") (size(landcover) != size(farmfields)) && Base.error("Input map sizes don't match.")
width, height = size(landcover)[1:2] width, height = size(landcover)[1:2]
landscape = Matrix{Pixel}(undef, width, height) landscape = Matrix{Pixel}(undef, width, height)
......
...@@ -23,6 +23,7 @@ function createdatadir(outdir::String, overwrite::Union{Bool,String}) ...@@ -23,6 +23,7 @@ function createdatadir(outdir::String, overwrite::Union{Bool,String})
(answer == "yes") && (overwrite = true) (answer == "yes") && (overwrite = true)
end end
if !overwrite if !overwrite
#TODO replace with exception
Base.error("Output directory exists, will not overwrite. Aborting.") Base.error("Output directory exists, will not overwrite. Aborting.")
else else
@warn "Overwriting existing output directory $(outdir)." @warn "Overwriting existing output directory $(outdir)."
...@@ -76,6 +77,7 @@ function saveinputfiles(model::AgentBasedModel) ...@@ -76,6 +77,7 @@ function saveinputfiles(model::AgentBasedModel)
# Copy the map files to the output folder # Copy the map files to the output folder
lcmap = @param(core.landcovermap) lcmap = @param(core.landcovermap)
ffmap = @param(core.farmfieldsmap) ffmap = @param(core.farmfieldsmap)
#TODO replace errors with exceptions
!(isfile(lcmap)) && Base.error("The map file $(lcmap) doesn't exist.") !(isfile(lcmap)) && Base.error("The map file $(lcmap) doesn't exist.")
!(isfile(ffmap)) && Base.error("The map file $(ffmap) doesn't exist.") !(isfile(ffmap)) && Base.error("The map file $(ffmap) doesn't exist.")
cp(lcmap, joinpath(@param(core.outdir), basename(lcmap)), force = true) cp(lcmap, joinpath(@param(core.outdir), basename(lcmap)), force = true)
...@@ -127,7 +129,7 @@ that want to have their output functions called regularly. ...@@ -127,7 +129,7 @@ that want to have their output functions called regularly.
function newdataoutput!(model::AgentBasedModel, filename::String, header::String, function newdataoutput!(model::AgentBasedModel, filename::String, header::String,
outputfunction::Function, frequency::String) outputfunction::Function, frequency::String)
if !(frequency in ("daily", "monthly", "yearly", "end", "never")) if !(frequency in ("daily", "monthly", "yearly", "end", "never"))
Base.error("Invalid frequency '$frequency' for $filename.") Base.error("Invalid frequency '$frequency' for $filename.") #TODO replace with exception
end end
ndo = DataOutput(filename, header, outputfunction, frequency) ndo = DataOutput(filename, header, outputfunction, frequency)
append!(model.dataoutputs, [ndo]) append!(model.dataoutputs, [ndo])
......
...@@ -3,15 +3,59 @@ ...@@ -3,15 +3,59 @@
### This file includes the core functions for initialising and running simulations. ### This file includes the core functions for initialising and running simulations.
### ###
#XXX With the parameter scanning, code execution has become rather difficult to follow. #XXX How can I make the model output during a parallel run clearer?
# Can I refactor this into two clear, separate paths - one for the normal case (single
# run) and one for parameter scanning? """
simulate(config=PARAMFILE, seed=nothing)
Initialise one or more model objects and carry out a full simulation experiment,
optionally specifying a configuration file and a seed for the RNG.
This is the default way to run a Persephone simulation.
"""
function simulate(config::String=PARAMFILE, seed::Union{Int64,Nothing}=nothing)
models = initialise(config, seed)
isa(models, Vector) ?
pmap(simulate!, models) :
simulate!(models)
end
"""
simulate!(model)
Carry out a complete simulation run using a pre-initialised model object.
"""
function simulate!(model::AgentBasedModel)
runtime = Dates.value(@param(core.enddate)-@param(core.startdate))+1
step!(model, dummystep, stepsimulation!, runtime)
finalise!(model)
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
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
and returns a vector of model objects.
"""
function initialise(config::String=PARAMFILE, seed::Union{Int64,Nothing}=nothing)
@info "Simulation run started at $(Dates.now())."
settings = getsettings(config, seed)
scanparams = settings["internal.scanparams"]
delete!(settings, "internal.scanparams")
isempty(scanparams) ?
initmodel(settings) :
pmap(initmodel, paramscan(settings, scanparams))
end
""" """
initmodel(settings) initmodel(settings)
Initialise a model object using a ready-made settings dict. This is Initialise a model object using a ready-made settings dict. This is
a helper function for `initialise()` and `initmodelsparallel()`. a helper function for `initialise()`.
""" """
function initmodel(settings::Dict{String, Any}) function initmodel(settings::Dict{String, Any})
@debug "Initialising model object." @debug "Initialising model object."
...@@ -38,36 +82,19 @@ function initmodel(settings::Dict{String, Any}) ...@@ -38,36 +82,19 @@ function initmodel(settings::Dict{String, Any})
end end
end end
"""
initmodelsparallel(settings)
Initialise multiple model objects using ready-made settings dicts. This is
a helper function for `initialise()`.
"""
function initmodelsparallel(settingsdicts::Vector{Dict{String, Any}})
#TODO parallelise model initialisation
@debug "Beginning to initialise model objects."
models = Vector{AgentBasedModel}()
for settings in settingsdicts
push!(models, initmodel(settings))
end
models
end
""" """
paramscan(settings) paramscan(settings)
Initialise a list of model objects, covering all possible parameter combinations Create a list of settings dicts, covering all possible parameter combinations
given by the settings (i.e. a full-factorial experiment). This is a helper function given by the input settings (i.e. a full-factorial experiment). This is a helper
for `initialise()`. function for `initialise()`.
""" """
function paramscan(settings::Dict{String,Any}, scanparams::Vector{String}) function paramscan(settings::Dict{String,Any}, scanparams::Vector{String})
# recursively generate a set of settings dicts covering all combinations isempty(scanparams) && return [settings]
function generatecombinations(params::Vector{String}) param = pop!(scanparams)
(length(params) == 0) && return [settings]
param = pop!(params)
combinations = Vector{Dict{String,Any}}() combinations = Vector{Dict{String,Any}}()
for comb in generatecombinations(params) # recursively generate a set of settings dicts covering all combinations
for comb in paramscan(settings, scanparams)
for value in settings[param] for value in settings[param]
newcombination = deepcopy(comb) newcombination = deepcopy(comb)
newcombination[param] = value newcombination[param] = value
...@@ -82,26 +109,6 @@ function paramscan(settings::Dict{String,Any}, scanparams::Vector{String}) ...@@ -82,26 +109,6 @@ function paramscan(settings::Dict{String,Any}, scanparams::Vector{String})
end end
combinations combinations
end end
generatecombinations(scanparams)
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
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
and returns a vector of model objects.
"""
function initialise(config::String=PARAMFILE, seed::Union{Int64,Nothing}=nothing)
@info "Simulation run started at $(Dates.now())."
settings = getsettings(config, seed)
scanparams = settings["internal.scanparams"]
delete!(settings, "internal.scanparams")
isempty(scanparams) ? initmodel(settings) : initmodelsparallel(settings, scanparameters)
end
""" """
stepsimulation!(model) stepsimulation!(model)
...@@ -139,34 +146,3 @@ function finalise!(model::AgentBasedModel) ...@@ -139,34 +146,3 @@ function finalise!(model::AgentBasedModel)
model model
end end
end end
"""
simulate!(model)
Carry out a complete simulation run using a pre-initialised model object.
"""
function simulate!(model::AgentBasedModel)
runtime = Dates.value(@param(core.enddate)-@param(core.startdate))+1
step!(model, dummystep, stepsimulation!, runtime)
finalise!(model)
end
"""
simulate(config=PARAMFILE, seed=nothing)
Initialise one or more model objects and carry out a full simulation experiment,
optionally specifying a configuration file and a seed for the RNG.
This is the default way to run a Persephone simulation.
"""
function simulate(config::String=PARAMFILE, seed::Union{Int64,Nothing}=nothing)
models = initialise(config, seed)
if isa(models, Vector)
for m in models
@info "Executing run $(m.settings["core.outdir"])"
simulate!(m) #TODO parallelise
end
else
simulate!(models)
end
end
...@@ -25,6 +25,9 @@ by trait dictionaries passed by them during initialisation. ...@@ -25,6 +25,9 @@ by trait dictionaries passed by them during initialisation.
age::Int32 age::Int32
end end
#TODO If I write a `getproperty` method for `Animal`, I could get around the ugly
# `animal.traits[property]` syntax (like Agents.jl does in `model.jl`).
""" """
animalid(animal) animalid(animal)
...@@ -155,8 +158,8 @@ variables: ...@@ -155,8 +158,8 @@ variables:
information). information).
Several utility macros can be used within the body of `@phase` as a short-hand for Several utility macros can be used within the body of `@phase` as a short-hand for
common expressions: `@trait`, `@setphase`, `@respond`, `@here`, `@kill`, common expressions: `@trait`, `@setphase`, `@respond`, `@kill`, `@reproduce`,
`@reproduce`, `@neighbours`, `@rand`. `@neighbours`, `@rand`.
Note that the first phase that is defined in a species definition block will be 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 the phase that animals are assigned at birth, unless the variable `phase` is
...@@ -215,17 +218,6 @@ macro respond(eventname, body) ...@@ -215,17 +218,6 @@ macro respond(eventname, body)
end end
end end
"""
@here(property)
A utility macro to quickly access a property of the animal's current position
(i.e. `landcover`, `fieldid`, or `events` - see the `Pixel` struct).
This can only be used nested within `@phase`.
"""
macro here(property)
:($(esc(:model)).landscape[$(esc(:animal)).pos...].$(property))
end
""" """
@kill @kill
...@@ -369,7 +361,8 @@ end ...@@ -369,7 +361,8 @@ end
@rand(args...) @rand(args...)
Return a random number or element from the sample, using the model RNG. 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`. This is a utility wrapper that can only be used nested within `@phase` or `@habitat`
(or in other contexts where the `model` object is available).
""" """
macro rand(args...) macro rand(args...)
:($(esc(:rand))($(esc(:model)).rng, $(map(esc, args)...))) :($(esc(:rand))($(esc(:model)).rng, $(map(esc, args)...)))
......
...@@ -13,6 +13,7 @@ farmfieldsmap = "data/fields_jena.tif" # location of the field geometry map ...@@ -13,6 +13,7 @@ farmfieldsmap = "data/fields_jena.tif" # location of the field geometry map
outdir = "results" # location and name of the output folder outdir = "results" # location and name of the output folder
overwrite = "ask" # overwrite the output directory? (true/false/"ask") overwrite = "ask" # overwrite the output directory? (true/false/"ask")
loglevel = "debug" # verbosity level: "debug", "info", "warn" loglevel = "debug" # verbosity level: "debug", "info", "warn"
processors = 2 # number of processors to use on parallel runs
seed = 2 # 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 # dates to start and end the simulation
startdate = 2022-01-01 startdate = 2022-01-01
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment