Skip to content
Snippets Groups Projects
  • xo30xoqa's avatar
    4d7f5127
    Switched to using local loggers · 4d7f5127
    xo30xoqa authored
    Previously, a global logger was used, which would have given problems
    on a parallel run. Also split up the `setupdatadir()` function to
    improve code structure.
    4d7f5127
    History
    Switched to using local loggers
    xo30xoqa authored
    Previously, a global logger was used, which would have given problems
    on a parallel run. Also split up the `setupdatadir()` function to
    improve code structure.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
simulation.jl 6.15 KiB
### Persephone - a socio-economic-ecological model of European agricultural landscapes.
###
### This file includes the core functions for initialising and running simulations.
###

#XXX With the parameter scanning, code execution has become rather difficult to follow.
# Can I refactor this into two clear, separate paths - one for the normal case (single
# run) and one for parameter scanning?

"""
    initmodel(settings)

Initialise a model object using a ready-made settings dict. This is
a helper function for `initialise()` and `initmodelsparallel()`.
"""
function initmodel(settings::Dict{String, Any})
    @debug "Initialising model object."
    createdatadir(settings["core.outdir"], settings["core.overwrite"])
    logger = modellogger(settings["core.loglevel"], settings["core.outdir"])
    with_logger(logger) do
        events = Vector{FarmEvent}()
        dataoutputs = Vector{DataOutput}()
        landscape = initlandscape(settings["core.landcovermap"], settings["core.farmfieldsmap"])
        space = GridSpace(size(landscape), periodic=false)
        properties = Dict{Symbol,Any}(:settings=>settings,
                                      :logger=>logger,
                                      :date=>settings["core.startdate"],
                                      :landscape=>landscape,
                                      :dataoutputs=>dataoutputs,
                                      :events=>events)
        model = AgentBasedModel(Union{Farmer,Animal,FarmPlot}, space, properties=properties,
                                rng=StableRNG(settings["core.seed"]), warn=false)
        saveinputfiles(model)
        initfarms!(model)
        initfields!(model)
        initnature!(model)
        model
    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)

Initialise a list of model objects, covering all possible parameter combinations
given by the settings (i.e. a full-factorial experiment). This is a helper function
for `initialise()`.
"""
function paramscan(settings::Dict{String,Any}, scanparams::Vector{String})
    # recursively generate a set of settings dicts covering all combinations
    function generatecombinations(params::Vector{String})
        (length(params) == 0) && return [settings]
        param = pop!(params)
        combinations = Vector{Dict{String,Any}}()
        for comb in generatecombinations(params)
            for value in settings[param]
                newcombination = deepcopy(comb)
                newcombination[param] = value
                if comb["core.outdir"] == settings["core.outdir"]
                    outdir = joinpath(comb["core.outdir"], "$(split(param, ".")[2])_$(value)")
                else
                    outdir = "$(comb["core.outdir"])_$(split(param, ".")[2])_$(value)"
                end
                newcombination["core.outdir"] = outdir
                push!(combinations, newcombination)
            end
        end
        combinations
    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)

Execute one update of the model.
"""
function stepsimulation!(model::AgentBasedModel)
    with_logger(model.logger) do
        @info "Simulating day $(model.date)."
        for a in Schedulers.ByType((Farmer,FarmPlot,Animal), true)(model)
            try #The animal may have been killed
                stepagent!(model[a], model)
            catch exc
                # check if the KeyError comes from the `model[a]` or the function call
                isa(exc, KeyError) && isa(exc.key, Int) ? continue : throw(exc)
            end
        end
        updateevents!(model)
        outputdata(model)
        model.date += Day(1)
        model
    end
end

"""
    finalise!(model)

Wrap up the simulation. Currently doesn't do anything except print some information.
"""
function finalise!(model::AgentBasedModel)
    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))."
        #XXX is there anything to do here?
        model
    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