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

Refactored DataOutput

parent 2aaeaa28
No related branches found
No related tags found
No related merge requests found
...@@ -25,7 +25,7 @@ function visualisemap(model::SimulationModel,date=nothing,landcover=nothing) ...@@ -25,7 +25,7 @@ function visualisemap(model::SimulationModel,date=nothing,landcover=nothing)
image!(f[1,1], landcover) image!(f[1,1], landcover)
ax.aspect = DataAspect() ax.aspect = DataAspect()
# check if there are individuals and plot them # check if there are individuals and plot them
inds = model.datatables["individuals"] inds = @subset(model.dataoutputs["individuals"].datastore, :X .== -1) #remove migrants
if iszero(size(inds)[1]) if iszero(size(inds)[1])
@debug "No individual data to map" @debug "No individual data to map"
return f return f
...@@ -54,7 +54,7 @@ Plot a line graph of population sizes of each species over time. ...@@ -54,7 +54,7 @@ Plot a line graph of population sizes of each species over time.
Returns a Makie figure object. Returns a Makie figure object.
""" """
function populationtrends(model::SimulationModel) function populationtrends(model::SimulationModel)
pops = model.datatables["populations"] pops = model.dataoutputs["populations"].datastore
ncolors = max(2, length(@param(nature.targetspecies))) ncolors = max(2, length(@param(nature.targetspecies)))
update_theme!(palette=(color=cgrad(:seaborn_bright, ncolors),), cycle=[:color]) update_theme!(palette=(color=cgrad(:seaborn_bright, ncolors),), cycle=[:color])
f = Figure() f = Figure()
...@@ -78,7 +78,7 @@ Plot a line graph of total population size and individual demographics of skylar ...@@ -78,7 +78,7 @@ Plot a line graph of total population size and individual demographics of skylar
Returns a Makie figure object. Returns a Makie figure object.
""" """
function skylarkpopulation(model::SimulationModel) function skylarkpopulation(model::SimulationModel)
pops = model.datatables["skylark_abundance"] pops = model.dataoutputs["skylark_abundance"].datastore
f = Figure() f = Figure()
dates = @param(core.startdate):@param(core.enddate) dates = @param(core.startdate):@param(core.enddate)
axlimits = (1, length(dates), 0, maximum(pops[!,:TotalAbundance])) axlimits = (1, length(dates), 0, maximum(pops[!,:TotalAbundance]))
......
...@@ -153,31 +153,34 @@ Struct fields: ...@@ -153,31 +153,34 @@ Struct fields:
- frequency: how often to call the output function (daily/monthly/yearly/end/never) - 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) - plotfunction: a function that takes a model object and returns a Makie figure object (optional)
""" """
struct DataOutput mutable struct DataOutput
name::String #FIXME update docstring
header::Vector{String}
outputfunction::Function
frequency::String frequency::String
databuffer::Vector{Vector}
datastore::DataFrame
outputfunction::Union{Function,Nothing}
plotfunction::Union{Function,Nothing} #XXX remove this? (#81) plotfunction::Union{Function,Nothing} #XXX remove this? (#81)
end end
"Retrieve the data stored in a DataOutput (assumes `core.storedata` is true)."
data(d::DataOutput) = d.datastore
##TODO what about output types that don't fit neatly into the standard CSV table format? ##TODO what about output types that don't fit neatly into the standard CSV table format?
## (e.g. end-of-run stats, map data) ## (e.g. end-of-run stats, map data)
""" """
newdataoutput!(model, name, header, outputfunction, frequency) newdataoutput!(model, name, header, frequency, outputfunction, plotfunction)
Create and register a new data output. This function must be called by all submodels Create and register a new data output. This function must be called by all submodels
that want to have their output functions called regularly. that want to have their output functions called regularly.
""" """
function newdataoutput!(model::SimulationModel, name::String, header::Vector{String}, function newdataoutput!(model::SimulationModel, name::String,
outputfunction::Function, frequency::String, header::Vector{String}, frequency::String,
outputfunction::Union{Function,Nothing}=nothing,
plotfunction::Union{Function,Nothing}=nothing) #XXX remove this? (#81) plotfunction::Union{Function,Nothing}=nothing) #XXX remove this? (#81)
if !(frequency in ("daily", "monthly", "yearly", "end", "never")) if !(frequency in ("daily", "monthly", "yearly", "end", "never"))
Base.error("Invalid frequency '$frequency' for $name.") #TODO replace with exception Base.error("Invalid frequency '$frequency' for $name.") #TODO replace with exception
end end
ndo = DataOutput(name, header, outputfunction, frequency, plotfunction)
append!(model.dataoutputs, [ndo])
if frequency != "never" if frequency != "never"
if @param(core.csvoutput) if @param(core.csvoutput)
open(joinpath(@param(core.outdir), name*".csv"), "w") do f open(joinpath(@param(core.outdir), name*".csv"), "w") do f
...@@ -189,9 +192,10 @@ function newdataoutput!(model::SimulationModel, name::String, header::Vector{Str ...@@ -189,9 +192,10 @@ function newdataoutput!(model::SimulationModel, name::String, header::Vector{Str
for h in header for h in header
df[!,h] = Any[] #XXX allow specifying types? df[!,h] = Any[] #XXX allow specifying types?
end end
model.datatables[name] = df
end end
end end
ndo = DataOutput(frequency, [[]], df, outputfunction, plotfunction)
model.dataoutputs[name] = ndo
end end
""" """
...@@ -202,36 +206,48 @@ configured frequency. If `force` is `true`, activate all outputs regardless ...@@ -202,36 +206,48 @@ configured frequency. If `force` is `true`, activate all outputs regardless
of their configuration. of their configuration.
""" """
function outputdata(model::SimulationModel, force=false) function outputdata(model::SimulationModel, force=false)
#XXX enable output every X days, or weekly? #XXX enable output weekly, or on set dates?
#XXX all output functions except for "end" are run on the first update #XXX all output functions except for "end" are run on the first update
# -> should they all be run on the last update, too? # -> should they all be run on the last update, too?
startdate = @param(core.startdate) startdate = @param(core.startdate)
isnextmonth = d -> (day(d) == day(startdate)) isnextmonth = d -> (day(d) == day(startdate))
isnextyear = d -> (month(d) == month(startdate) && day(d) == day(startdate)) isnextyear = d -> (month(d) == month(startdate) && day(d) == day(startdate))
for output in model.dataoutputs for o in keys(model.dataoutputs)
output = model.dataoutputs[o]
(!force && output.frequency == "never") && continue (!force && output.frequency == "never") && continue
# check if this output should be activated today # check if this output should be activated today
if force || (output.frequency == "daily") || if force || (output.frequency == "daily") ||
(output.frequency == "monthly" && isnextmonth(model.date)) || (output.frequency == "monthly" && isnextmonth(model.date)) ||
(output.frequency == "yearly" && isnextyear(model.date)) || (output.frequency == "yearly" && isnextyear(model.date)) ||
(output.frequency == "end" && model.date == @param(core.enddate)) (output.frequency == "end" && model.date == @param(core.enddate))
data = output.outputfunction(model) !isnothing(output.outputfunction) && (output.databuffer = output.outputfunction(model))
isempty(output.databuffer) && continue
if @param(core.csvoutput) if @param(core.csvoutput)
open(joinpath(@param(core.outdir), output.name*".csv"), "a") do f open(joinpath(@param(core.outdir), o*".csv"), "a") do f
for row in data for row in output.databuffer
println(f, join(row, ",")) println(f, join(row, ","))
end end
end end
end end
if @param(core.storedata) if @param(core.storedata)
for row in data for row in output.databuffer
push!(model.datatables[output.name], row) push!(output.datastore, row)
end end
end end
output.databuffer = [[]]
end end
end end
end end
"""
record(model, outputname, data)
Append an observation vector to the given output.
"""
function record(model::SimulationModel, outputname::String, data::Vector)
push!(model.dataoutputs[outputname].databuffer, data)
end
""" """
visualiseoutput(model) visualiseoutput(model)
...@@ -241,11 +257,11 @@ saving each figure to file. ...@@ -241,11 +257,11 @@ saving each figure to file.
function visualiseoutput(model::SimulationModel) #XXX remove this? (#81) function visualiseoutput(model::SimulationModel) #XXX remove this? (#81)
@debug "Visualising output." @debug "Visualising output."
CairoMakie.activate!() # make sure we're using Cairo CairoMakie.activate!() # make sure we're using Cairo
for output in model.dataoutputs for o in keys(model.dataoutputs)
output = model.dataoutputs[o]
isnothing(output.plotfunction) && continue isnothing(output.plotfunction) && continue
figure = output.plotfunction(model) figure = output.plotfunction(model)
save(joinpath(@param(core.outdir), output.name*"."*@param(core.figureformat)), save(joinpath(@param(core.outdir), o*"."*@param(core.figureformat)), figure)
figure)
end end
end end
......
...@@ -16,8 +16,7 @@ mutable struct AgricultureModel{Tcroptype,Tcropstate} <: SimulationModel ...@@ -16,8 +16,7 @@ mutable struct AgricultureModel{Tcroptype,Tcropstate} <: SimulationModel
settings::Dict{String,Any} settings::Dict{String,Any}
rng::AbstractRNG rng::AbstractRNG
logger::AbstractLogger logger::AbstractLogger
dataoutputs::Vector{DataOutput} dataoutputs::Dict{String,DataOutput}
datatables::Dict{String,DataFrame}
date::Date date::Date
landscape::Matrix{Pixel} landscape::Matrix{Pixel}
weather::Dict{Date,Weather} weather::Dict{Date,Weather}
...@@ -126,8 +125,7 @@ function initmodel(settings::Dict{String, Any}) ...@@ -126,8 +125,7 @@ function initmodel(settings::Dict{String, Any})
settings, settings,
StableRNG(settings["core.seed"]), StableRNG(settings["core.seed"]),
logger, logger,
Vector{DataOutput}(), Dict{String,DataOutput}(),
Dict{String, DataFrame}(),
settings["core.startdate"], settings["core.startdate"],
landscape, landscape,
weather, weather,
......
...@@ -11,9 +11,9 @@ Create output files for each data group collected by the nature model. ...@@ -11,9 +11,9 @@ Create output files for each data group collected by the nature model.
""" """
function initecologicaldata(model::SimulationModel) function initecologicaldata(model::SimulationModel)
newdataoutput!(model, "populations", ["Date", "Species", "Abundance"], newdataoutput!(model, "populations", ["Date", "Species", "Abundance"],
savepopulationdata, @param(nature.popoutfreq), populationtrends) @param(nature.popoutfreq), savepopulationdata, populationtrends)
newdataoutput!(model, "individuals", ["Date","ID","X","Y","Species","Sex","Age"], newdataoutput!(model, "individuals", ["Date","ID","X","Y","Species","Sex","Age"],
saveindividualdata, @param(nature.indoutfreq), visualisemap) @param(nature.indoutfreq), saveindividualdata, visualisemap)
initskylarkdata(model) initskylarkdata(model)
end end
...@@ -50,12 +50,14 @@ monthly, yearly, or at the end of a simulation, depending on the parameter ...@@ -50,12 +50,14 @@ monthly, yearly, or at the end of a simulation, depending on the parameter
`nature.indoutfreq`. WARNING: Produces very big files! `nature.indoutfreq`. WARNING: Produces very big files!
""" """
function saveindividualdata(model::SimulationModel) function saveindividualdata(model::SimulationModel)
#XXX doesn't include migrants!
data = [] data = []
for a in model.animals for a in model.animals
isnothing(a) && continue isnothing(a) && continue
push!(data, [model.date,a.id,a.pos[1],a.pos[2],speciesof(a),a.sex,a.age]) push!(data, [model.date,a.id,a.pos[1],a.pos[2],speciesof(a),a.sex,a.age])
end end
for m in model.migrants
push!(data, [model.date, m[1].id, -1, -1, speciesof(m[1]), m[1].sex, m[1].age])
end
data data
end end
...@@ -66,11 +68,11 @@ function initskylarkdata(model::SimulationModel) ...@@ -66,11 +68,11 @@ function initskylarkdata(model::SimulationModel)
newdataoutput!(model, "skylark_abundance", newdataoutput!(model, "skylark_abundance",
["Date", "TotalAbundance", "Mating", "Breeding", ["Date", "TotalAbundance", "Mating", "Breeding",
"Nonbreeding", "Juvenile", "Migrants"], "Nonbreeding", "Juvenile", "Migrants"],
skylarkabundance, "daily", skylarkpopulation) "daily", skylarkabundance, skylarkpopulation)
# newdataoutput!(model, "skylark_territories", ["Date", "ID", "X", "Y"], # newdataoutput!(model, "skylark_territories", ["Date", "ID", "X", "Y"],
# skylarkterritories, "monthly") #TODO add plotting function # skylarkterritories, "monthly") #TODO add plotting function
newdataoutput!(model, "skylark_nests", ["Date", "ID", "X", "Y", "Landcover", "Crop"], newdataoutput!(model, "skylark_nests", ["Date", "ID", "X", "Y", "Landcover", "Crop"],
skylarknests, "monthly") #TODO add plotting function "monthly", skylarknests) #TODO add plotting function
# newdataoutput!(model, "skylark_mortality", ["Date", "N", "Cause"], # newdataoutput!(model, "skylark_mortality", ["Date", "N", "Cause"],
# skylarkmortality, "daily") #TODO add plotting function # skylarkmortality, "daily") #TODO add plotting function
end end
......
...@@ -52,8 +52,7 @@ function inittestmodel(smallmap=true) ...@@ -52,8 +52,7 @@ function inittestmodel(smallmap=true)
TESTSETTINGS, TESTSETTINGS,
StableRNG(TESTSETTINGS["core.seed"]), StableRNG(TESTSETTINGS["core.seed"]),
global_logger(), global_logger(),
Vector{DataOutput}(), Dict{String,DataOutput}(),
Dict{String, DataFrame}(),
TESTSETTINGS["core.startdate"], TESTSETTINGS["core.startdate"],
landscape, landscape,
weather, weather,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment