Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
output.jl 5.82 KiB
### Persephone - a socio-economic-ecological model of European agricultural landscapes.
###
### This file includes functions for saving the model output.
###

const LOGFILE = "simulation.log"

## Note: `setupdatadir()` was adapted from the GeMM model by Leidinger et al.
## (https://github.com/CCTB-Ecomods/gemm/blob/master/src/output.jl)

"""
    setupdatadir()

Creates the output directory and copies relevant files into it.
"""
function setupdatadir()
    # Check whether the output directory already exists and handle conflicts
    if isdir(param("core.outdir"))
        overwrite = param("core.overwrite")
        if param("core.overwrite") == "ask"
            println("The chosen output directory $(param("core.outdir")) already exists.")
            println("Type 'yes' to overwrite this directory. Otherwise, the simulation will abort.")
            print("Overwrite? ")
            answer = readline()
            (answer == "yes") && (overwrite = true)
        end
        if !overwrite
            Base.error("Output directory exists, will not overwrite. Aborting.")
        else
            @warn "Overwriting existing output directory $(param("core.outdir"))."
        end
    end
    mkpath(param("core.outdir"))
    # Setup the logging system and logfile
    loglevel = Logging.Info
    if param("core.loglevel") == "debug"
        loglevel = Logging.Debug
    elseif param("core.loglevel") == "quiet"
        loglevel = Logging.Warn
    end
    logfile = open(joinpath(param("core.outdir"), LOGFILE), "w+")
    simulationlogger = TeeLogger(ConsoleLogger(logfile, loglevel),
                                 ConsoleLogger(stdout, loglevel))
    global_logger(simulationlogger)
    @debug "Setting up output directory $(param("core.outdir"))"
    # Export a copy of the current parameter settings to the output folder.
    # This can be used to replicate this exact run in future, and also
    # records the current time and git commit.
    open(joinpath(param("core.outdir"), basename(param("core.configfile"))), "w") do f
        println(f, "#\n# --- Persephone configuration parameters ---")
        println(f, "# This file was generated automatically.")
        println(f, "# Simulation run on $(string(Dates.format(Dates.now(), "d u Y HH:MM:SS"))),")
        println(f, "# with git commit $(read(`git rev-parse HEAD`, String))#\n")
        printparameters(f)
    end
    # Copy the map files to the output folder
    lcmap = param("core.landcovermap")
    ffmap = param("core.farmfieldsmap")
    !(isfile(lcmap)) && Base.error("The map file $(lcmap) doesn't exist.")
    !(isfile(ffmap)) && Base.error("The map file $(ffmap) doesn't exist.")
    cp(lcmap, joinpath(param("core.outdir"), basename(lcmap)), force = true)
    cp(ffmap, joinpath(param("core.outdir"), basename(ffmap)), force = true)
end

"""
    DataOutput

A struct for organising model output. This is designed for text-based data output
that is updated more or less regularly (e.g. population data in csv files).
Submodels can register their own output functions using `newdataoutput()`.

Struct fields:
    - filename: the name of the file to be created in the user-specified output directory
    - header: a string to be written to the start of the file as it is initialised
    - outputfunction: a function that takes a model object and returns a string to write to file
    - frequency: how often to call the output function (daily/monthly/yearly/end/never)
"""
struct DataOutput
    filename::String
    header::String
    outputfunction::Function
    frequency::String
end

let outputregistry = Vector{DataOutput}(),
    nextmonthlyoutput = today(),
    nextyearlyoutput = today()

    """
        newdataoutput(filename, header, outputfunction, frequency)

    Create and register a new data output. This function must be called by all submodels
    that want to have their output functions called regularly.
    """
    global function newdataoutput(filename::String, header::String,
                                  outputfunction::Function, frequency::String)
        if !(frequency in ("daily", "monthly", "yearly", "end", "never"))
            Base.error("Invalid frequency '$frequency' for $filename.")
        end
        ndo = DataOutput(filename, header, outputfunction, frequency)
        append!(outputregistry, [ndo])
        if frequency != "never"
            open(joinpath(param("core.outdir"), filename), "w") do f
                println(f, header)
            end
        end
    end

    """
        outputdata(model)
    
    Cycle through all registered data outputs and activate them according to their
    configured frequency.
    """
    global function outputdata(model::AgentBasedModel)
        #XXX all output functions are run on the first update (regardless of frequency)
        # -> should they all be run on the last update, too?
        if model.date == param("core.startdate")
            nextmonthlyoutput = model.date
            nextyearlyoutput = model.date
        end
        for output in outputregistry
            (output.frequency == "never") && continue
            # check if this output should be activated today
            if (output.frequency == "daily") ||
                (output.frequency == "monthly" && model.date == nextmonthlyoutput) ||
                (output.frequency == "yearly" && model.date == nextyearlyoutput) ||
                (output.frequency == "end" && model.date == param("core.enddate"))
                open(joinpath(param("core.outdir"), output.filename), "a") do f
                    outstring = output.outputfunction(model)
                    (outstring[end] != '\n') && (outstring *= '\n')
                    print(f, outstring)
                end                
            end
        end
        (model.date == nextmonthlyoutput) && (nextmonthlyoutput = model.date + Month(1))
        (model.date == nextyearlyoutput) && (nextyearlyoutput = model.date + Year(1))
    end
end