Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
simulation.jl 6.92 KiB
### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe.
###
### This file includes the core functions for initialising and running simulations.
###

#XXX How can I make the model output during a parallel run clearer?

"""
    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 AgricultureModel{Tcroptype,Tcropstate} <: SimulationModel
    settings::Dict{String,Any}
    rng::AbstractRNG
    logger::AbstractLogger
    dataoutputs::Dict{String,DataOutput}
    date::Date
    landscape::Matrix{Pixel}
    weather::Dict{Date,Weather}
    crops::Dict{String,Tcroptype}
    farmers::Vector{Farmer}
    farmplots::Vector{FarmPlot{Tcropstate}}
    animals::Vector{Union{Animal,Nothing}}
    migrants::Vector{Pair{Animal,AnnualDate}}
    events::Vector{FarmEvent}
end

"""
    nagents(model)

Return the total number of agents in a model object.
"""
function nagents(model::AgricultureModel)
    length(model.animals)+length(model.farmers)+length(model.farmplots)
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(configfile=PARAMFILE, params=Dict())

Initialise one or more model objects and carry out a full simulation experiment,
optionally specifying a configuration file and/or specific parameters.

This is the default way to run a Persefone simulation.
"""
function simulate(;configfile::String=PARAMFILE, params::Dict{String,Any}=Dict{String,Any}())
    models = initialise(configfile=configfile, params=params)
    isa(models, Vector) ? 
        map(simulate!, models) : #TODO parallelise
        simulate!(models)
end

"""
    simulate!(model)

Carry out a complete simulation run using a pre-initialised model object.
"""
function simulate!(model::SimulationModel)
    @info "Simulation run started at $(Dates.now())."
    while model.date <= @param(core.enddate)
        stepsimulation!(model)
    end
    finalise!(model)
end

"""
    initialise(configfile=PARAMFILE, params=Dict())

Initialise the model: read in parameters, create the output data directory,
and instantiate the SimulationModel object(s). Optionally allows specifying the
configuration file and overriding specific parameters. 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(;configfile::String=PARAMFILE, params::Dict{String,Any}=Dict{String,Any}())
    settings = getsettings(configfile, params)
    scanparams = settings["internal.scanparams"]
    delete!(settings, "internal.scanparams")
    isempty(scanparams) ?
        initmodel(settings) :
        map(initmodel, paramscan(settings, scanparams)) #TODO parallelise
end

"""
    initmodel(settings)

Initialise a model object using a ready-made settings dict. This is
a helper function for `initialise()`.
"""
function initmodel(settings::Dict{String, Any})
    #TODO catch exceptions and print them to the log file
    @debug "Initialising model object."
    createdatadir(settings["core.outdir"],
                  settings["core.overwrite"])
    logger = modellogger(settings["core.loglevel"],
                         settings["core.outdir"],
                         settings["core.logoutput"])
    with_logger(logger) do
        landscape = initlandscape(settings["world.mapdirectory"],
                                  settings["world.landcovermap"],
                                  settings["world.farmfieldsmap"])
        weather = initweather(joinpath(settings["world.mapdirectory"],
                                       settings["world.weatherfile"]),
                              settings["core.startdate"],
                              settings["core.enddate"])
        crops, Tcroptype, Tcropstate = initcropmodel(settings["crop.cropmodel"],
                                                     settings["crop.cropdirectory"])
        farmers = Vector{Farmer}()
        farmplots = Vector{FarmPlot{Tcropstate}}()
        model = AgricultureModel{Tcroptype,Tcropstate}(
            settings,
            StableRNG(settings["core.seed"]),
            logger,
            Dict{String,DataOutput}(),
            settings["core.startdate"],
            landscape,
            weather,
            crops,
            farmers,
            farmplots,
            Vector{Union{Animal,Nothing}}(),
            Vector{Pair{Animal, Date}}(),
            Vector{FarmEvent}()
        )
        saveinputfiles(model)

        initfields!(model)
        initfarms!(model)
        initnature!(model)
        model
    end
end

"""
    paramscan(settings)

Create a list of settings dicts, covering all possible parameter combinations
given by the input settings (i.e. a full-factorial experiment). This is a helper
function for `initialise()`.
"""
function paramscan(settings::Dict{String,Any}, scanparams::Vector{String})
    isempty(scanparams) && return [settings]
    param = pop!(scanparams)
    combinations = Vector{Dict{String,Any}}()
    # recursively generate a set of settings dicts covering all combinations
    for comb in paramscan(settings, scanparams)
        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

"""
    stepsimulation!(model)

Execute one update of the model.
"""
function stepsimulation!(model::SimulationModel)
    #TODO catch exceptions and print them to the log file
    with_logger(model.logger) do
        @info "Simulating day $(model.date)."
        #XXX move the two loops into the relevant submodels?
        for f in model.farmers
            stepagent!(f, model)
        end
        for p in model.farmplots
            stepagent!(p, model)
        end
        updatenature!(model)
        updateevents!(model)
        outputdata(model)
        model.date += Day(1)
        model
    end
end

"""
    finalise!(model)

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)
        model
    end
end