Skip to content
Snippets Groups Projects
Commit 257d7ae5 authored by xo30xoqa's avatar xo30xoqa
Browse files

Implemented model object de-/serialisation.

This closes #24.
parent 34789d5c
No related branches found
No related tags found
No related merge requests found
......@@ -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"]
......
......@@ -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"
......
......@@ -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")
......
......@@ -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
......@@ -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
......@@ -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
......@@ -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)
......
......@@ -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
......@@ -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?
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment