### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe. ### ### This file includes functions for configuring the model and reading in map files. ### ## Note: much of this code was adapted from the GeMM model by Leidinger et al. ## (https://github.com/CCTB-Ecomods/gemm/blob/master/src/input.jl, archived at ## https://doi.org/10.5281/zenodo.5602906) """ The file that stores all default parameters: `src/parameters.toml` """ const PARAMFILE = joinpath(pkgdir(Persefone), "src/parameters.toml") ## (DO NOT CHANGE THIS VALUE! Instead, specify simulation-specific configuration files ## by using the "--configfile" commandline argument, or when invoking simulate().) #XXX do I need to use absolute paths for all input files in case working dir is changed? # (can be done with `joinpath(dirname(@__FILE__), <filename>)` ) """ @param(domainparam) Return a configuration parameter from the global settings. The argument should be in the form `<domain>.<parameter>`, for example `@param(core.outdir)`. Possible values for `<domain>` are `core`, `nature`, `farm`, or `crop`. For a full list of parameters, see `src/parameters.toml`. Note: this macro only works in a context where the `model` object is available! """ macro param(domainparam) domain = String(domainparam.args[1]) paramname = String(domainparam.args[2].value) :($(esc(:model)).settings[$(domain*"."*paramname)]) end """ getsettings(configfile, seed=nothing) Combines all configuration options to produce a single settings dict. 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 config file supplied by the user (via the --configfile option) defaults::Dict{String, Any} = TOML.parsefile(PARAMFILE) |> flattenTOML commandline = parsecommandline() scanparams = Vector{String}() if haskey(commandline, "configfile") && isfile(commandline["configfile"]) configs = TOML.parsefile(commandline["configfile"]) |> flattenTOML elseif configfile != PARAMFILE && isfile(configfile) configs = TOML.parsefile(configfile) |> flattenTOML else configs = nothing end settings = deepcopy(defaults) for param in keys(defaults) if split(param, ".")[2] in keys(commandline) settings[param] = commandline[split(param, ".")[2]] elseif !isnothing(configs) && param in keys(configs) settings[param] = configs[param] # check whether a parameter is given as a list for parameter scanning vectup = Union{Vector,Tuple} if (isa(configs[param], vectup) && !isa(defaults[param], vectup)) || isa(configs[param], Union{Vector{vectup},Tuple{vectup}}) push!(scanparams, param) end end end !isnothing(seed) && (settings["core.seed"] = seed) settings["internal.scanparams"] = scanparams preprocessparameters(settings, defaults["core.outdir"]) end """ preprocessparameters(settings) Take the raw input parameters and process them where necessary (e.g. convert types or perform checks). This is a helper function for [`getsettings`](@ref). """ function preprocessparameters(settings::Dict{String,Any}, defaultoutdir::String) (settings["core.seed"] == 0) && (settings["core.seed"] = abs(rand(RandomDevice(), Int32))) if settings["core.outdir"] == defaultoutdir outdir = defaultoutdir*"_"*string(Dates.today())*"_s"*string(settings["core.seed"]) settings["core.outdir"] = outdir end if settings["core.startdate"] > settings["core.enddate"] Base.error("Enddate is earlier than startdate.") #TODO replace with exception end settings["world.mapresolution"] = settings["world.mapresolution"] * 1m #FIXME enable parallelisation # if !isempty(settings["internal.scanparams"]) && (nprocs() < 2) # @warn "To parallelise multiple simulation runs, use `julia -p <n>`." # # https://docs.julialang.org/en/v1/manual/distributed-computing/#code-availability # #addprocs(settings["core.processors"]; exeflags="--project") # #@everywhere include("../src/Persefone.jl") # end settings 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 [`prepareTOML`](@ref). """ 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 """ @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 a context where the `model` object is available. """ macro rand(args...) :($(esc(:rand))($(esc(:model)).rng, $(map(esc, args)...))) end """ @shuffle!(collection) Shuffle the given collection in place, using the model RNG. This is a utility wrapper that can only be used a context where the `model` object is available. """ macro shuffle!(collection) :($(esc(:shuffle!))($(esc(:model)).rng, $(esc(collection)))) end """ @chance(odds) Return true if a random number is less than the odds (0.0 <= `odds` <= 1.0), using the model RNG. This is a utility wrapper that can only be used a context where the `model` object is available. """ macro chance(odds) :($(esc(:rand))($(esc(:model)).rng) < $(esc(odds))) end """ parsecommandline() Certain software parameters can be set via the commandline. """ function parsecommandline() versionstring = """ Persefone $(pkgversion(Persefone)) © 2022-2023 Daniel Vedder (MIT license) https://git.idiv.de/xo30xoqa/persefone """ s = ArgParseSettings(add_version=true, version=versionstring) @add_arg_table! s begin "--configfile", "-c" help = "name of the configuration file" arg_type = String required = false "--seed", "-s" help = "inital random seed" arg_type = Int "--outdir", "-o" help = "location of the output directory" arg_type = String required = false "--loglevel", "-l" help = "verbosity: \"debug\", \"info\", or \"warn\"" arg_type = String required = false end #XXX this changes the global RNG?! (https://github.com/carlobaldassi/ArgParse.jl/issues/121) -> should be fixed in Julia 1.10 args = parse_args(s) for a in keys(args) (args[a] == nothing) && delete!(args, a) end args end """ loadmodelobject(fullfilename) Deserialise a model object that was previously saved with `[savemodelobject](@ref)`. """ function loadmodelobject(fullfilename::String) if !isfile(fullfilename) @warn "File $(fullfilename) does not exist. Loading failed." return end object = deserialize(fullfilename) # Do basic integrity checks if !(typeof(object) <: Dict && typeof(object["model"]) <: SimulationModel) @warn "This file does not contain a model object. Loading failed." return end if (object["modelversion"] != pkgversion(Persefone) || object["juliaversion"] != VERSION) @warn "This model object was saved with a different version of Persefone or Julia. It may be incompatible." end model = object["model"] # Reset the logger !isdir(@param(core.outdir)) && mkpath(@param(core.outdir)) model.logger = modellogger(@param(core.loglevel), @param(core.outdir)) model end