Skip to content
Snippets Groups Projects
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