From 02fa32d94f590c820b43c708a774dd0e3bbdb2c6 Mon Sep 17 00:00:00 2001
From: Daniel Vedder <daniel.vedder@idiv.de>
Date: Tue, 10 Jan 2023 12:14:00 +0100
Subject: [PATCH] Made data outputs a model property

---
 src/Persephone.jl            |  1 +
 src/core/output.jl           | 88 ++++++++++++++++--------------------
 src/core/simulation.jl       |  2 +
 src/nature/ecologicaldata.jl |  4 +-
 test/io_tests.jl             | 18 ++++----
 test/landscape_tests.jl      |  1 +
 6 files changed, 56 insertions(+), 58 deletions(-)

diff --git a/src/Persephone.jl b/src/Persephone.jl
index 43ef2f1..7795c48 100644
--- a/src/Persephone.jl
+++ b/src/Persephone.jl
@@ -30,6 +30,7 @@ export
     FarmPlot,
     Animal,
     Farmer,
+    DataOutput,
     #macros
     @param,
     @species,
diff --git a/src/core/output.jl b/src/core/output.jl
index a75c347..b7ca007 100644
--- a/src/core/output.jl
+++ b/src/core/output.jl
@@ -82,58 +82,50 @@ struct DataOutput
     frequency::String
 end
 
-let outputregistry = Vector{DataOutput}(),
-    nextmonthlyoutput = today(),
-    nextyearlyoutput = today()
-
-    """
-        newdataoutput(model, filename, header, outputfunction, frequency)
+"""
+    newdataoutput(model, 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(model::AgentBasedModel, 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
+Create and register a new data output. This function must be called by all submodels
+that want to have their output functions called regularly.
+"""
+function newdataoutput(model::AgentBasedModel, 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!(model.dataoutputs, [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
+"""
+    outputdata(model)
+ 
+Cycle through all registered data outputs and activate them according to their
+configured frequency.
+"""
+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?
+    startdate = @param(core.startdate)
+    isnextmonth = d -> (day(d) == day(startdate))
+    isnextyear = d -> (month(d) == month(startdate) && day(d) == day(startdate))
+    for output in model.dataoutputs
+        (output.frequency == "never") && continue
+        # check if this output should be activated today
+        if (output.frequency == "daily") ||
+            (output.frequency == "monthly" && isnextmonth(model.date)) ||
+            (output.frequency == "yearly" && isnextyear(model.date)) ||
+            (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
-        (model.date == nextmonthlyoutput) && (nextmonthlyoutput = model.date + Month(1))
-        (model.date == nextyearlyoutput) && (nextyearlyoutput = model.date + Year(1))
     end
 end
diff --git a/src/core/simulation.jl b/src/core/simulation.jl
index 11244c8..0c98df3 100644
--- a/src/core/simulation.jl
+++ b/src/core/simulation.jl
@@ -22,11 +22,13 @@ function initialise(config::String=PARAMFILE, seed::Union{Int64,Nothing}=nothing
     settings = getsettings(config, seed)
     Random.seed!(settings["core"]["seed"])
     events = Vector{FarmEvent}()
+    dataoutputs = Vector{DataOutput}()
     landscape = initlandscape(settings["core"]["landcovermap"], settings["core"]["farmfieldsmap"])
     space = GridSpace(size(landscape), periodic=false)
     properties = Dict{Symbol,Any}(:settings=>settings,
                                   :date=>settings["core"]["startdate"],
                                   :landscape=>landscape,
+                                  :dataoutputs=>dataoutputs,
                                   :events=>events)
     @debug "Setting up model."
     model = AgentBasedModel(Union{Farmer,Animal,FarmPlot}, space, properties=properties,
diff --git a/src/nature/ecologicaldata.jl b/src/nature/ecologicaldata.jl
index 507ad17..564097a 100644
--- a/src/nature/ecologicaldata.jl
+++ b/src/nature/ecologicaldata.jl
@@ -12,8 +12,8 @@ const INDFILE = "individuals.csv"
 Create output files for each data group collected by the nature model.
 """
 function initecologicaldata(model::AgentBasedModel)
-    newdataoutput(model, POPFILE, "Date;Species;Abundance", savepopulationdata,
-                  @param(nature.popoutfreq))
+    newdataoutput(model, POPFILE, "Date;Species;Abundance",
+                  savepopulationdata, @param(nature.popoutfreq))
     newdataoutput(model, INDFILE, "Date;ID;X;Y;Species;Sex;Age",
                   saveindividualdata, @param(nature.indoutfreq))
 end
diff --git a/test/io_tests.jl b/test/io_tests.jl
index 8534fbe..3554457 100644
--- a/test/io_tests.jl
+++ b/test/io_tests.jl
@@ -14,21 +14,23 @@
 end
 
 @testset "Output functions" begin
-    properties = Dict{Symbol,Any}(:settings=>TESTSETTINGS)
+    properties = Dict{Symbol,Any}(:settings=>TESTSETTINGS,
+                                  :dataoutputs=>Vector{DataOutput}())
     space = GridSpace((10,10), periodic=false)
     model = AgentBasedModel(Animal, space, properties=properties, warn=false)
     # test that the output directory is created with all files
+    outdir = @param(core.outdir)
     logstring = "Setting up output directory results_testsuite_$(Dates.today())_s1"
     @test_logs (:debug, logstring) min_level=Logging.Debug Ps.setupdatadir(model)
-    @test isdir(@param(core.outdir))
-    @test isfile(joinpath(@param(core.outdir), @param(core.landcovermap)))
-    @test isfile(joinpath(@param(core.outdir), @param(core.farmfieldsmap)))
-    @test isfile(joinpath(@param(core.outdir), @param(core.configfile)))
-    @test isfile(joinpath(@param(core.outdir), Ps.LOGFILE))
+    @test isdir(outdir)
+    @test isfile(joinpath(outdir, @param(core.landcovermap)))
+    @test isfile(joinpath(outdir, @param(core.farmfieldsmap)))
+    @test isfile(joinpath(outdir, @param(core.configfile)))
+    @test isfile(joinpath(outdir, Ps.LOGFILE))
     # check whether the overwrite warning/protection works
-    logstring = "Overwriting existing output directory $(@param(core.outdir))."
+    logstring = "Overwriting existing output directory $(outdir)."
     @test_logs (:warn, logstring) match_mode=:any Ps.setupdatadir(model)
     #TODO test overwrite protection (requires parameter mutability)
-    rm(@param(core.outdir), force=true, recursive=true)
+    rm(outdir, force=true, recursive=true)
     #TODO test that creating a DataOutput works, and outputs data with the required frequency
 end
diff --git a/test/landscape_tests.jl b/test/landscape_tests.jl
index 78ea08e..958e046 100644
--- a/test/landscape_tests.jl
+++ b/test/landscape_tests.jl
@@ -59,6 +59,7 @@ end
     properties = Dict{Symbol,Any}(:date=>Date(2022, 1, 1),
                                   :landscape=>landscape,
                                   :events=>Vector{FarmEvent}(),
+                                  :dataoutputs=>Vector{DataOutput}(),
                                   :settings=>TESTSETTINGS)
     model = AgentBasedModel(Union{Farmer,Animal,FarmPlot}, space,
                             properties=properties, warn=false)
-- 
GitLab