### 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>)` ) """ The crop models that can be used in the simulation. """ const AVAILABLE_CROPMODELS = ["almass", "simple", "aquacrop"] """ @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, userparams=Dict()) Combines all configuration options to produce a single settings dict. Precedence: function arguments - commandline parameters - user config file - default values """ function getsettings(configfile::String, userparams::Dict{String,Any}=Dict{String,Any}()) # 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 param in keys(userparams) settings[param] = userparams[param] elseif 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 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) #TODO replace errors with exceptions # miscellaneous processing (settings["core.seed"] == 0) && (settings["core.seed"] = abs(rand(RandomDevice(), Int32))) settings["world.mapresolution"] = settings["world.mapresolution"] * 1m # create a standardised name for the output directory if settings["core.outdir"] == defaultoutdir outdir = defaultoutdir*"_"*string(Dates.today())*"_s"*string(settings["core.seed"]) settings["core.outdir"] = outdir end # check if input data are present in the working directory or the package directory if !isdir(settings["world.mapdirectory"]) if isdir(abspath(pkgdir(@__MODULE__), settings["world.mapdirectory"])) settings["world.mapdirectory"] = abspath(pkgdir(@__MODULE__), settings["world.mapdirectory"]) @debug "Using package directory to load map data: $(settings["world.mapdirectory"])." else Base.error("Couldn't find map directory $(settings["world.mapdirectory"]).") end end # check cropmodel settings cropmodels = string.(split(settings["crop.cropmodel"], ",")) for cropmodel in cropmodels if !(cropmodel in AVAILABLE_CROPMODELS) error("cropmodel = \"$cropmodel\", but has to be one of: $AVAILABLE_CROPMODELS") end end cropdirs = string.(split(settings["crop.cropdirectory"], ",")) for cropdir in cropdirs if !isdir(cropdir) error("Can't access crop model data dir $cropdir") end end # sanity checks if settings["core.startdate"] > settings["core.enddate"] Base.error("Enddate is earlier than startdate.") end #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 """ 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