Something went wrong on our end
-
Marco Matthies authoredMarco Matthies authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
input.jl 8.20 KiB
### 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