### 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