From a1e8f923bf0c52812939864288b1f8cddfc5221c Mon Sep 17 00:00:00 2001
From: Daniel Vedder <daniel.vedder@idiv.de>
Date: Wed, 12 Jun 2024 10:31:31 +0200
Subject: [PATCH] Added world.mapdirectory parameter

Also updated CONTRIBUTORS.md, added a "none" option for
`core.logoutput`, and added type annotations to the Skylark struct
---
 CHANGELOG.md                  | 22 ++++++++++++++++++----
 CONTRIBUTORS.md               | 22 +++++++++-------------
 LICENSE                       |  2 +-
 README.md                     |  2 +-
 src/analysis/makieplots.jl    |  7 +++++--
 src/core/output.jl            |  8 +++++---
 src/core/simulation.jl        |  6 ++++--
 src/nature/species/skylark.jl | 34 +++++++++++++++-------------------
 src/parameters.toml           | 13 +++++++------
 src/world/landscape.jl        |  6 ++++--
 test/runtests.jl              |  6 ++++--
 test/test_parameters.toml     |  1 +
 12 files changed, 74 insertions(+), 55 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9d58db8..3bf09dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 
+## [v0.5.1] - unreleased
+
+### Added
+
+- `core.logoutput` parameter to define whether logs are printed to screen, file, none, or both
+
+- `world.mapdirectory` parameter specifies the path to the directory in which
+ `landcovermap`, `farmfieldsmap`, and `weatherfile` are located
+ 
+### Changed
+
+### Deprecated
+
+### Removed
+
+### Fixed
+
+
 ## [v0.5.0] - 07-06-2024
 
 This release doesn't add much new functionality, but represents a major restructuring 
@@ -45,14 +63,10 @@ way the species definition macros work and are used.
 
 - requires Julia 1.10
 
-### Deprecated
-
 ### Removed
 
 - Agents.jl dependency (including `AgentBasedModel` and functions for adding/moving/removing agents)
 
-### Fixed
-
 
 ## [v0.4.1] - 2023-11-14
 
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index ce1b330..70d6208 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -2,28 +2,24 @@
 
 ### Lead developer
 
-- Daniel Vedder
+- Daniel Vedder (daniel.vedder@idiv.de)
 
+### Supervisor
 
-### Supporting developer(s)
-
-- Lea Kolb
+- Guy Pe'er (guy.peer@idiv.de)
 
+### Supporting developers
 
-### Code reviewer
+- Marco Matthies
+- Gabriel Díaz Iturry
 
-- Ludmilla Figueiredo
 
+### Code reviewers
 
-### Supervisor
-
-- Guy Pe'er
-
+- Ludmilla Figueiredo
+- Marco Matthies
 
 ### Advisors
 
 - Aletta Bonn
-
 - Kerstin Wiegand
-
-- Birgit Müller
diff --git a/LICENSE b/LICENSE
index 16d4824..03fa7d5 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT LICENSE
 
-Copyright (c) 2022-2023 Daniel Vedder, Lea Kolb
+Copyright (c) 2022-2024 Persefone.jl developers
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 2b394d7..8281283 100644
--- a/README.md
+++ b/README.md
@@ -86,4 +86,4 @@ You can then access all Persefone functions, such as `simulate()`. (See
 
 ---
 
-&copy; 2022-2023 the contributors (MIT license)
+&copy; 2022-2024 the contributors (MIT license)
diff --git a/src/analysis/makieplots.jl b/src/analysis/makieplots.jl
index 739544a..7b1b901 100644
--- a/src/analysis/makieplots.jl
+++ b/src/analysis/makieplots.jl
@@ -4,7 +4,7 @@
 ###
 
 """
-    visualisemap(model, date, landcovermap)
+    visualisemap(model, date, landcover)
 
 Draw the model's land cover map and plot all individuals as points on it at
 the specified date. If no date is passed, use the last date for which data
@@ -15,7 +15,10 @@ Returns a Makie figure object.
 function visualisemap(model::SimulationModel,date=nothing,landcover=nothing)
     # load and plot the map
     # Note: if the landcover map is supplied, it needs to be rotr90'ed
-    isnothing(landcover) && (landcover = rotr90(load(@param(world.landcovermap))))
+    if isnothing(landcover)
+        lcm = joinpath(@param(world.mapdirectory), @param(world.landcovermap))
+        landcover = rotr90(load(lcm))
+    end
     f = Figure()
     ax = Axis(f[1,1])
     hidedecorations!(ax)
diff --git a/src/core/output.jl b/src/core/output.jl
index 9a3eefe..9842fef 100644
--- a/src/core/output.jl
+++ b/src/core/output.jl
@@ -32,7 +32,7 @@ end
 """
     modellogger(loglevel, outdir, output="both")
 
-Create a logger object that writes output both to screen and to a logfile.
+Create a logger object that writes output to screen and/or a logfile.
 This object is stored as `model.logger` and can then be used with `with_logger()`.
 Note: requires [`createdatadir`](@ref) to be run first.
 """
@@ -51,6 +51,8 @@ function modellogger(loglevel::String, outdir::String, output::String="both")
         return ConsoleLogger(logfile, loglevel)
     elseif output == "screen"
         return ConsoleLogger(stdout, loglevel)
+    elseif output == "none"
+        return NullLogger()
     else
         Base.error("Invalid log output target $output, should be file/screen/both.")
     end
@@ -95,8 +97,8 @@ function saveinputfiles(model::SimulationModel)
         TOML.print(f, prepareTOML(model.settings))
     end
     # Copy the map files to the output folder
-    lcmap = @param(world.landcovermap)
-    ffmap = @param(world.farmfieldsmap)
+    lcmap = joinpath(@param(world.mapdirectory), @param(world.landcovermap))
+    ffmap = joinpath(@param(world.mapdirectory), @param(world.farmfieldsmap))
     #TODO replace errors with exceptions
     !(isfile(lcmap)) && Base.error("The map file $(lcmap) doesn't exist.")
     !(isfile(ffmap)) && Base.error("The map file $(ffmap) doesn't exist.")
diff --git a/src/core/simulation.jl b/src/core/simulation.jl
index fbc4235..05158aa 100644
--- a/src/core/simulation.jl
+++ b/src/core/simulation.jl
@@ -110,9 +110,11 @@ function initmodel(settings::Dict{String, Any})
                          settings["core.outdir"],
                          settings["core.logoutput"])
     with_logger(logger) do
-        landscape = initlandscape(settings["world.landcovermap"],
+        landscape = initlandscape(settings["world.mapdirectory"],
+                                  settings["world.landcovermap"],
                                   settings["world.farmfieldsmap"])
-        weather = initweather(settings["world.weatherfile"],
+        weather = initweather(joinpath(settings["world.mapdirectory"],
+                                       settings["world.weatherfile"]),
                               settings["core.startdate"],
                               settings["core.enddate"])
         crops = readcropparameters(settings["crop.cropfile"],
diff --git a/src/nature/species/skylark.jl b/src/nature/species/skylark.jl
index 848858a..c9ecca8 100644
--- a/src/nature/species/skylark.jl
+++ b/src/nature/species/skylark.jl
@@ -4,13 +4,13 @@
 ###
 
 skylarkhabitat = @habitat((@landcover() == grass ||
-                         # settle on grass or arable land (but not maize)
-                         (@landcover() == agriculture && @cropname() != "maize")) &&
-                        @distancetoedge() >= 5) # at least 50m from other habitats
-                        #XXX this ought to check for distance to forest and builtup,
-                        # but that's very expensive (see below)
-                        # @distanceto(forest) > 5 && # at least 50m from forest edges
-                        # @distanceto(builtup) > 5) # and from anthropogenic structures
+                           # settle on grass or arable land (but not maize)
+                           (@landcover() == agriculture && @cropname() != "maize")) &&
+                          @distancetoedge() >= 5) # at least 50m from other habitats
+#XXX this ought to check for distance to forest and builtup,
+# but that's very expensive (see below)
+# @distanceto(forest) > 5 && # at least 50m from forest edges
+# @distanceto(builtup) > 5) # and from anthropogenic structures
 
 """
     Skylark
@@ -30,32 +30,29 @@ At the moment, this implementation is still in development.
       ISBN 3-89104-019-9
 """
 @species Skylark begin
-
     #XXX use Unitful.jl
-    #TODO add type annotations
-    
     eggtime::Int64 = 11 # 11 days from laying to hatching
     eggpredationmortality::Float64 = 0.03 # per-day egg mortality from predation
     nestharvestmortality::Float64 = 0.9 # egg/nestling mortality after a harvest event (XXX guess)
 
-    nestlingtime = 7:11 # 7-11 days from hatching to leaving nest
+    nestlingtime::Union{Int64,UnitRange{Int64}} = 7:11 # 7-11 days from hatching to leaving nest
     nestlingpredationmortality::Float64 = 0.03 # per-day nestling mortality from predation
 
-    fledglingtime = 25:30 # 25-30 days from hatching to independence
+    fledglingtime::Union{Int64,UnitRange{Int64}} = 25:30 # 25-30 days from hatching to independence
     fledglingharvestmortality::Float64 = 0.5 # fledgling mortality after harvest
     fledglingpredationmortality::Float64 = 0.01 # per-day fledgling mortality from predation
     firstyearmortality::Float64 = 0.38 # total mortality in the first year after independence
     
-    migrationdates = () # is defined by each individual in @create(Skylark)
+    migrationdates::Tuple = () # is defined by each individual in @create(Skylark)
     migrationmortality::Float64 = 0.33 # chance of dying during the winter
 
     mate::Int64 = -1 # the agent ID of the mate (-1 if none)
-    nest = () # coordinates of current nest
-    nestingbegin = (April, 10) # begin nesting in the middle of April
-    nestbuildingtime = 4:5 # 4-5 days needed to build a nest (doubled for first nest)
+    nest::Tuple = () # coordinates of current nest
+    nestingbegin::Tuple{Int64,Int64} = (April, 10) # begin nesting in the middle of April
+    nestbuildingtime::Union{Int64,UnitRange{Int64}} = 4:5 # 4-5 days needed to build a nest (doubled for first nest)
     nestcompletion::Int64 = 0 # days left until the nest is built
-    eggsperclutch = 2:5 # 2-5 eggs laid per clutch
-    clutch = [] # IDs of offspring in current clutch
+    eggsperclutch::Union{Int64,UnitRange{Int64}} = 2:5 # 2-5 eggs laid per clutch
+    clutch::Vector{Int64} = Vector{Int64}() # IDs of offspring in current clutch
     breedingdelay::Int64 = 18 # wait 18 days after hatching to start a new brood
     nestingend::Int64 = July # last month of nesting
     
@@ -68,7 +65,6 @@ As an egg, simply check for mortality and hatching.
 @phase Skylark egg begin
     @kill(self.eggpredationmortality, "predation")
     @respond(harvesting, @kill(self.nestharvestmortality, "harvest"))
-
     if self.age == self.eggtime
         @setphase(nestling)
     end
diff --git a/src/parameters.toml b/src/parameters.toml
index 272bb25..d956e37 100644
--- a/src/parameters.toml
+++ b/src/parameters.toml
@@ -10,7 +10,7 @@
 configfile = "src/parameters.toml" # location of the configuration file
 outdir = "results" # location and name of the output folder
 overwrite = "ask" # overwrite the output directory? (true/false/"ask")
-logoutput = "both" # log output to screen/file/both
+logoutput = "both" # log output to screen/file/none/both
 csvoutput = true # save collected data in CSV files
 visualise = true # generate result graphs
 storedata = true # keep collected data in memory
@@ -19,13 +19,14 @@ loglevel = "debug" # verbosity level: "debug", "info", "warn"
 processors = 2 # number of processors to use on parallel runs
 seed = 2 # seed value for the RNG (0 -> random value)
 startdate = 2022-01-01 # first day of the simulation
-#enddate = 2022-12-31 # last day of the simulation
-enddate = 2022-03-31 # last day of the simulation (test value)
+enddate = 2022-12-31 # last day of the simulation
+#enddate = 2022-03-31 # last day of the simulation (test value)
 
 [world]
-landcovermap = "data/regions/jena-small/landcover.tif" # location of the landcover map
-farmfieldsmap = "data/regions/jena-small/fields.tif" # location of the field geometry map
-weatherfile = "data/regions/jena-small/weather.csv" # location of the weather data file
+mapdirectory = "data/regions/jena-small" # the directory in which all geographic data are stored
+landcovermap = "landcover.tif" # name of the landcover map in the map directory
+farmfieldsmap = "fields.tif" # name of the field geometry map in the map directory
+weatherfile = "weather.csv" # name of the weather data file in the map directory
 	
 [farm]
 farmmodel = "FieldManager" # which version of the farm model to use (not yet implemented)
diff --git a/src/world/landscape.jl b/src/world/landscape.jl
index f310cc6..36a47fe 100644
--- a/src/world/landscape.jl
+++ b/src/world/landscape.jl
@@ -38,13 +38,15 @@ mutable struct FarmEvent
 end
 
 """
-    initlandscape(landcovermap, farmfieldsmap)
+    initlandscape(directory, landcovermap, farmfieldsmap)
 
 Initialise the model landscape based on the map files specified in the
 configuration. Returns a matrix of pixels.
 """
-function initlandscape(landcovermap::String, farmfieldsmap::String)
+function initlandscape(directory::String, landcovermap::String, farmfieldsmap::String)
     @debug "Initialising landscape"
+    landcovermap = joinpath(directory, landcovermap)
+    farmfieldsmap = joinpath(directory, farmfieldsmap)
     #TODO replace errors with exception
     !(isfile(landcovermap)) && Base.error("Landcover map $(landcovermap) doesn't exist.")
     !(isfile(farmfieldsmap)) && Base.error("Field map $(farmfieldsmap) doesn't exist.")
diff --git a/test/runtests.jl b/test/runtests.jl
index db4870c..8540f05 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -31,10 +31,12 @@ function inittestmodel(smallmap=true)
     if smallmap
         landscape = smalltestlandscape()
     else
-        landscape = Ps.initlandscape(TESTSETTINGS["world.landcovermap"],
+        landscape = Ps.initlandscape(TESTSETTINGS["world.mapdirectory"],
+                                     TESTSETTINGS["world.landcovermap"],
                                      TESTSETTINGS["world.farmfieldsmap"])
     end
-    weather = Ps.initweather(TESTSETTINGS["world.weatherfile"],
+    weather = Ps.initweather(joinpath(TESTSETTINGS["world.mapdirectory"],
+                                      TESTSETTINGS["world.weatherfile"]),
                              TESTSETTINGS["core.startdate"],
                              TESTSETTINGS["core.enddate"])
     crops = Ps.readcropparameters(TESTSETTINGS["crop.cropfile"],
diff --git a/test/test_parameters.toml b/test/test_parameters.toml
index 3a24704..33413f8 100644
--- a/test/test_parameters.toml
+++ b/test/test_parameters.toml
@@ -21,6 +21,7 @@ startdate = 2022-02-01
 enddate = 2022-03-31
 
 [world]
+mapdirectory = "."
 landcovermap = "landcover_jena.tif" # location of the landcover map
 farmfieldsmap = "fields_jena.tif" # location of the field geometry map
 weatherfile = "weather_jena.csv" # location of the weather data file
-- 
GitLab