diff --git a/Manifest.toml b/Manifest.toml
index 4701490c43bf37648fa48a3db6c3b9024fd36546..07ccd8bde84b9b9208b8ddc15cdcd97407872a09 100644
--- a/Manifest.toml
+++ b/Manifest.toml
@@ -2,7 +2,7 @@
julia_version = "1.9.3"
manifest_format = "2.0"
-project_hash = "88b08cc01ff4cf4b3ac05aaa043f66221dec37b4"
+project_hash = "a93906f69837f39319eb070be15a9e813d23a9ed"
[[deps.AbstractFFTs]]
deps = ["ChainRulesCore", "LinearAlgebra"]
@@ -161,6 +161,12 @@ git-tree-sha1 = "ded953804d019afa9a3f98981d99b33e3db7b6da"
uuid = "944b1d66-785c-5afd-91f1-9de20f533193"
version = "0.7.0"
+[[deps.ColorSchemes]]
+deps = ["ColorTypes", "ColorVectorSpace", "Colors", "FixedPointNumbers", "PrecompileTools", "Random"]
+git-tree-sha1 = "67c1f244b991cad9b0aa4b7540fb758c2488b129"
+uuid = "35d6a980-a343-548e-a6ea-1d62b119f2f4"
+version = "3.24.0"
+
[[deps.ColorTypes]]
deps = ["FixedPointNumbers", "Random"]
git-tree-sha1 = "eb7f0f8307f71fac7c606984ea5fb2817275d6e4"
@@ -347,9 +353,9 @@ version = "0.1.1"
[[deps.FileIO]]
deps = ["Pkg", "Requires", "UUIDs"]
-git-tree-sha1 = "7be5f99f7d15578798f338f5433b6c432ea8037b"
+git-tree-sha1 = "299dc33549f68299137e51e6d49a13b5b1da9673"
uuid = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
-version = "1.16.0"
+version = "1.16.1"
[[deps.FilePathsBase]]
deps = ["Compat", "Dates", "Mmap", "Printf", "Test", "UUIDs"]
diff --git a/Project.toml b/Project.toml
index 91651ed6adbbe90e633fdad8d8b05045aa3f10a2..ed045a943924fb3cdee9caa015db6a7a165ddafc 100644
--- a/Project.toml
+++ b/Project.toml
@@ -7,15 +7,18 @@ version = "0.2.0"
Agents = "46ada45e-f475-11e8-01d0-f70cc89e6671"
ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63"
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
+ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
DataFramesMeta = "1313f7d8-7da2-5740-9ea0-a2ca25f37964"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
+FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
GeoArrays = "2fb1d81b-e6a0-5fc5-82e6-8e06903437ab"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
LoggingExtras = "e6f89c97-d47a-5376-807f-9c37f3926c36"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
+Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
diff --git a/src/Persefone.jl b/src/Persefone.jl
index dd56cdba309bb88de6c5929c3bfb6132ad11d83f..165f3992f082c4aecbd85917456325d981e4cd1e 100644
--- a/src/Persefone.jl
+++ b/src/Persefone.jl
@@ -21,10 +21,13 @@ using
DataFrames,
DataFramesMeta,
Distributed,
+ FileIO,
GeoArrays, #XXX this is a big dependency - can we get rid of it?
Logging,
LoggingExtras,
+ #CairoMakie,
Random,
+ Serialization,
StableRNGs,
TOML
@@ -76,12 +79,18 @@ export
initialise,
stepsimulation!,
createevent!,
- finalise!
+ finalise!,
+ #visualisemap,
+ #populationtrends,
+ #visualiseoutput,
+ savemodelobject,
+ loadmodelobject
## include all module files (note that the order matters - if file
## b references something from file a, it must be included later)
include("core/input.jl")
include("core/output.jl")
+#include("analysis/makieplots.jl")
include("world/landscape.jl")
include("world/weather.jl")
diff --git a/src/core/input.jl b/src/core/input.jl
index 7869c0e21e9bc5b1b32086b4606f612a5295dcf1..7d3ccf7d50c42e56f555701775754d70d80350ac 100644
--- a/src/core/input.jl
+++ b/src/core/input.jl
@@ -163,7 +163,7 @@ function parsecommandline()
arg_type = String
required = false
end
- #XXX this changes the global RNG?! (https://github.com/carlobaldassi/ArgParse.jl/issues/121)
+ #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)
@@ -171,3 +171,24 @@ function parsecommandline()
args
end
+"""
+ loadmodelobject(fullfilename)
+
+Serialise a model object and save it to file for later reference.
+Includes the current model and Julia versions for compatibility checking.
+"""
+function loadmodelobject(fullfilename::String)
+ object = deserialize(fullfilename)
+
+ if !(typeof(object) <: Dict && typeof(object["model"]) <: AgentBasedModel)
+ @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"]
+ model.logger = modellogger(@param(core.loglevel), @param(core.outdir)) #reset logger
+ model
+end
diff --git a/src/core/output.jl b/src/core/output.jl
index debff8c7dfc93e4b78b30120eff07034328d8253..0ce93ea976709348b8f1552376367af6ae05c35e 100644
--- a/src/core/output.jl
+++ b/src/core/output.jl
@@ -20,7 +20,7 @@ function createdatadir(outdir::String, overwrite::Union{Bool,String})
println("Type 'yes' to overwrite this directory. Otherwise, the simulation will abort.")
print("Overwrite? ")
answer = readline()
- (answer == "yes") && (overwrite = true)
+ overwrite = (answer == "yes" || answer == "y")
end
!overwrite ? Base.error("Output directory exists, will not overwrite. Aborting.") :
@warn "Overwriting existing output directory $(outdir)."
@@ -131,12 +131,14 @@ Struct fields:
- header: a list of column names
- outputfunction: a function that takes a model object and returns data values to record (formatted as a vector of vectors)
- frequency: how often to call the output function (daily/monthly/yearly/end/never)
+ - plotfunction: a function that takes a model object and returns a Makie figure object (optional)
"""
struct DataOutput
name::String
header::Vector{String}
outputfunction::Function
frequency::String
+ plotfunction::Union{Function,Nothing}
end
"""
@@ -146,11 +148,12 @@ Create and register a new data output. This function must be called by all submo
that want to have their output functions called regularly.
"""
function newdataoutput!(model::AgentBasedModel, name::String, header::Vector{String},
- outputfunction::Function, frequency::String)
+ outputfunction::Function, frequency::String,
+ plotfunction::Union{Function,Nothing}=nothing)
if !(frequency in ("daily", "monthly", "yearly", "end", "never"))
Base.error("Invalid frequency '$frequency' for $name.") #TODO replace with exception
end
- ndo = DataOutput(name, header, outputfunction, frequency)
+ ndo = DataOutput(name, header, outputfunction, frequency, plotfunction)
append!(model.dataoutputs, [ndo])
if frequency != "never"
if @param(core.csvoutput)
@@ -204,3 +207,36 @@ function outputdata(model::AgentBasedModel)
end
end
end
+
+# """
+# visualiseoutput(model)
+
+# Cycle through all data outputs and call their respective plot functions,
+# saving each figure as a PDF.
+# """
+# function visualiseoutput(model::AgentBasedModel)
+# #TODO write tests
+# @debug "Visualising output."
+# for output in model.dataoutputs
+# isnothing(output.plotfunction) && continue
+# figure = output.plotfunction(model)
+# save(joinpath(@param(core.outdir), output.name*".pdf"), figure)
+# end
+# end
+
+"""
+ savemodelobject(model, filename)
+
+Serialise a model object and save it to file for later reference.
+Includes the current model and Julia versions for compatibility checking.
+
+WARNING: produces large files (>100 MB) and takes a while to execute.
+"""
+function savemodelobject(model::AgentBasedModel, filename::String)
+ object = Dict("model"=>model,
+ "modelversion"=>pkgversion(Persefone),
+ "juliaversion"=>VERSION)
+ filename = joinpath(@param(core.outdir), filename*".dat")
+ serialize(filename, object)
+ @debug "Saved model object to $(filename)."
+end
diff --git a/src/core/simulation.jl b/src/core/simulation.jl
index ea8a26e73a0dea6c370a21628ad69c6b633d8c6f..21bff1a5dcb0ce5fa996d15b3394f48b118d4c1a 100644
--- a/src/core/simulation.jl
+++ b/src/core/simulation.jl
@@ -26,6 +26,7 @@ end
Carry out a complete simulation run using a pre-initialised model object.
"""
function simulate!(model::AgentBasedModel)
+ @info "Simulation run started at $(Dates.now())."
runtime = Dates.value(@param(core.enddate)-@param(core.startdate))+1
step!(model, dummystep, stepsimulation!, runtime)
finalise!(model)
@@ -42,7 +43,6 @@ parameters, in which case it creates a full-factorial simulation experiment
and returns a vector of model objects.
"""
function initialise(config::String=PARAMFILE, seed::Union{Int64,Nothing}=nothing)
- @info "Simulation run started at $(Dates.now())."
settings = getsettings(config, seed)
scanparams = settings["internal.scanparams"]
delete!(settings, "internal.scanparams")
@@ -152,7 +152,7 @@ function finalise!(model::AgentBasedModel)
with_logger(model.logger) do
@info "Simulated $(model.date-@param(core.startdate))."
@info "Simulation run completed at $(Dates.now()),\nwrote output to $(@param(core.outdir))."
- #XXX is there anything to do here?
+ #visualiseoutput(model) #TODO
model
end
end
diff --git a/src/world/landscape.jl b/src/world/landscape.jl
index 1cacf092762467096bddead5430d539c42aa52ad..d97a91c01ee3a3da849acaa3791da42f02e40aa4 100644
--- a/src/world/landscape.jl
+++ b/src/world/landscape.jl
@@ -43,9 +43,11 @@ configuration. Returns a matrix of pixels.
"""
function initlandscape(landcovermap::String, farmfieldsmap::String)
@debug "Initialising landscape"
+ #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.")
landcover = GeoArrays.read(landcovermap)
farmfields = GeoArrays.read(farmfieldsmap)
- #TODO replace error with exception
(size(landcover) != size(farmfields)) && Base.error("Input map sizes don't match.")
width, height = size(landcover)[1:2]
landscape = Matrix{Pixel}(undef, width, height)
diff --git a/test/io_tests.jl b/test/io_tests.jl
index 435bd253c25385511cce3c344484d4fafde8cdc0..7605365e3243dfe4f1a53b6d96bfbedd27743be3 100644
--- a/test/io_tests.jl
+++ b/test/io_tests.jl
@@ -63,3 +63,21 @@ end
@test size(model.datatables["end"]) == (1, 1)
rm(outdir, force=true, recursive=true)
end
+
+@testset "Model object serialization" begin
+ model = inittestmodel()
+ Ps.createdatadir(@param(core.outdir), @param(core.overwrite))
+ @test_logs((:debug, "Saved model object to results_testsuite/test.dat."),
+ min_level=Logging.Debug, match_mode=:any,
+ savemodelobject(model, "test"))
+ @test isfile(joinpath(@param(core.outdir), "test.dat"))
+ model2 = loadmodelobject(joinpath(@param(core.outdir), "test.dat"))
+ @test model.date == model2.date
+ @test model.settings == model2.settings
+ @test length(model.agents) == length(model2.agents)
+ simulate!(model)
+ simulate!(model2)
+ @test model.date == model2.date
+ @test length(model.agents) == length(model2.agents)
+ rm(@param(core.outdir), force=true, recursive=true)
+end
diff --git a/test/runtests.jl b/test/runtests.jl
index dc86b72f29752292c7427b6868526835af8ede2a..f2dca73a018944e26ae461def5c1558ae988476e 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -22,6 +22,8 @@ const TESTPARAMETERS = joinpath(pkgdir(Persefone), "test/test_parameters.toml")
const TESTSETTINGS = Ps.getsettings(TESTPARAMETERS)
"""
+ inittestmodel(smallmap=true)
+
Initialise an AgentBasedModel for testing purposes.
`smallmap`: use a hypothetical small landscape rather than a real one?