Cache
with SpaDES
As part of a reproducible work flow, caching of various function calls are a critical component. Down the road, it is likely that an entire work flow from raw data to publication, decision support, report writing, presentation building etc., could be built and be reproducible anywhere, on demand. The reproducible::Cache
function is built to work with any R function. However, it becomes very powerful in a SpaDES
context because we can build large, powerful applications that are transparent and tied to the raw data that may be many conceptual steps upstream in the workflow. To do this, we have built several customizations within the SpaDES
package. Important to this is dealing correctly with the simList
, which is an object that has slot that is an environment. But more important are the various tools that can be used at higher levels, i.e., not just for “standard” functions.
SpaDES
Some of the details of the simList
-specific features of this Cache
function include:
The function converts all elements that have an environment as part of their attributes into a format that has no unique environment attribute, using format
if a function, and as.list
in the case of the simList
environment.
When used within SpaDES modules, Cache
(capital C) does not require that the argument cacheRepo
be specified. If called from inside a SpaDES module, Cache
will use the cacheRepo
argument from a call to cachePath(sim)
, taking the sim
from the call stack. Similarly, if no cacheRepo
argument is specified, then it will use getOption("spades.cachePath")
, which will, by default, be a temporary location with no persistence between R sessions! To persist between sessions, use SpaDES::setPaths()
every session.
In a SpaDES
context, there are several levels of caching that can be used as part of a reproducible workflow. Each level can be used to a modeler’s advantage; and, all can be – and are often – used concurrently.
spades
levelAnd entire call to spades
or experiment
can be cached. This will have the effect of eliminating any stochasticity in the model as the output will simply be the cached version of the simList
. This is likely most useful in situations where reproducibility is more important than “new” stochasticity (e.g., building decision support systems, apps, final version of a manuscript).
library(igraph) # for %>%
library(raster)
library(SpaDES)
mySim <- simInit(
times = list(start = 0.0, end = 5.0),
params = list(
.globals = list(stackName = "landscape", burnStats = "testStats"),
randomLandscapes = list(.plotInitialTime = NA),
fireSpread = list(.plotInitialTime = NA)
),
modules = list("randomLandscapes", "fireSpread"),
paths = list(modulePath = system.file("sampleModules", package = "SpaDES.core")))
This functionality can be achieved within a spades
call.
# compare caching ... run once to create cache
system.time(outSim <- spades(Copy(mySim), cache = TRUE, notOlderThan = Sys.time()))
## This is the current event, printed as it is happening:
## eventTime moduleName eventType eventPriority
## 0 checkpoint init 5
## 0 save init 5
## 0 progress init 5
## 0 load init 5
## 0 randomLandscapes init 5
## 0 fireSpread init 5
## 1 fireSpread burn 5
## 1 fireSpread stats 5
## 2 fireSpread burn 5
## 2 fireSpread stats 5
## 3 fireSpread burn 5
## 3 fireSpread stats 5
## 4 fireSpread burn 5
## 4 fireSpread stats 5
## 5 fireSpread burn 5
## 5 fireSpread stats 5
## user system elapsed
## 3.985 0.234 4.304
Note that if there were any visualizations (here we turned them off with .plotInitialTime = NA
above) they will happen the first time through, but not the cached times.
# vastly faster 2nd time
system.time(outSimCached <- spades(Copy(mySim), cache = TRUE))
## Using cached copy of burn event in fireSpread module. Adding to memoised copy.
## user system elapsed
## 0.062 0.013 0.074
all.equal(outSim, outSimCached)
## [1] "Attributes: < Component \"newCache\": 1 element mismatch >"
experiment
levelThis functionality can be achieved within an experiment call. This can be done 2 ways, either: “internally” through the cache argument, which will cache each spades call; or, “externally” which will cache the entire experiment. If there are lots of spades
calls, then the former will be slow as the simList
will be digested once per spades
call.
system.time(sims1 <- experiment(mySim, replicates = 2, cache = TRUE))
## Using cached copy of burn event in fireSpread module. Adding to memoised copy.
## Using cached copy of burn event in fireSpread module. Adding to memoised copy.
## user system elapsed
## 0.167 0.004 0.183
# internal -- second time faster
system.time(sims2 <- experiment(mySim, replicates = 2, cache = TRUE))
## Using memoised copy of burn event in fireSpread module
## Using memoised copy of burn event in fireSpread module
## user system elapsed
## 0.083 0.004 0.086
all.equal(sims1, sims2)
## [1] TRUE
experiment
with Cache
Here, the simList
(and other arguments to experiment) is hashed once, and if it is found to be the same as previous, then the returned list of simList
objects is recovered. This means that even a very large experiment, with many replicates and combinations of parameters and modules can be recovered very quickly. Here we show that you can output objects to disk, so the list of simList
objects doesn’t get too big. Then, when we recover it in the Cached version, all the files are still there, the list of simList
objects is small, so very fast to recover.
# External
outputs(mySim) <- data.frame(objectName = "landscape")
system.time(sims3 <- Cache(experiment, mySim, replicates = 3, .plotInitialTime = NA,
clearSimEnv = TRUE))
## loading cached result from previous experiment call, adding to memoised copy
## user system elapsed
## 0.034 0.004 0.039
The second time is way faster. We see the output files in the same location.
system.time(sims4 <- Cache(experiment, mySim, replicates = 3, .plotInitialTime = NA,
clearSimEnv = TRUE))
## loading memoised result from previous experiment call.
## user system elapsed
## 0.028 0.004 0.032
all.equal(sims3, sims4)
## [1] TRUE
dir(outputPath(mySim), recursive = TRUE)
## [1] "experiment.RData" "landscape_year5.rds"
## [3] "rep1/landscape_year5.rds" "rep2/landscape_year5.rds"
## [5] "rep3/landscape_year5.rds"
Notice that speed up can be enormous; in this case ~100 times.
If the parameter .useCache
in the module’s metadata is set to TRUE
, then every event in the module will be cached. That means that every time that module is called from within a spades or experiment call, Cache
will be called. Only the objects inside the simList
that correspond to the inputObjects
or the outputObjects
from the module metadata will be assessed for caching.
For general use, module-level caching would be mostly useful for modules that have no stochasticity, such as data-preparation modules, GIS modules etc.
In this example, we will use the cache on the randomLandscapes module. This means that each subsequent call to spades will result in identical outputs from the randomLandscapes
module (only!). This would be useful when only one random landscape is needed simply for trying something out, or putting into production code (e.g., publication, decision support, etc.).
# Module-level
params(mySim)$randomLandscapes$.useCache <- TRUE
system.time(randomSim <- spades(Copy(mySim), .plotInitialTime = NA,
notOlderThan = Sys.time(), debug = TRUE))
## This is the current event, printed as it is happening:
## eventTime moduleName eventType eventPriority
## 0 checkpoint init 5
## 0 save init 5
## 0 progress init 5
## 0 load init 5
## 0 randomLandscapes init 5
## 0 fireSpread init 5
## 1 fireSpread burn 5
## 1 fireSpread stats 5
## 2 fireSpread burn 5
## 2 fireSpread stats 5
## 3 fireSpread burn 5
## 3 fireSpread stats 5
## 4 fireSpread burn 5
## 4 fireSpread stats 5
## 5 fireSpread burn 5
## 5 fireSpread stats 5
## 5 save spades 10
## user system elapsed
## 2.205 0.096 2.284
# vastly faster the second time
system.time(randomSimCached <- spades(Copy(mySim), .plotInitialTime = NA,
debug = TRUE))
## This is the current event, printed as it is happening:
## eventTime moduleName eventType eventPriority
## 0 checkpoint init 5
## 0 save init 5
## 0 progress init 5
## 0 load init 5
## 0 randomLandscapes init 5
## Using cached copy of randomLandscapes module
## adding to memoised copy0 fireSpread init 5
## 1 fireSpread burn 5
## 1 fireSpread stats 5
## 2 fireSpread burn 5
## 2 fireSpread stats 5
## 3 fireSpread burn 5
## 3 fireSpread stats 5
## 4 fireSpread burn 5
## 4 fireSpread stats 5
## 5 fireSpread burn 5
## 5 fireSpread stats 5
## 5 save spades 10
## user system elapsed
## 0.501 0.021 0.517
Test that only layers produced in randomLandscapes
are identical, not fireSpread
.
layers <- list("DEM", "forestAge", "habitatQuality", "percentPine", "Fires")
same <- lapply(layers, function(l) identical(randomSim$landscape[[l]],
randomSimCached$landscape[[l]]))
names(same) <- layers
print(same) # Fires is not same because all non-init events in fireSpread are not cached
## $DEM
## [1] TRUE
##
## $forestAge
## [1] TRUE
##
## $habitatQuality
## [1] TRUE
##
## $percentPine
## [1] TRUE
##
## $Fires
## [1] FALSE
If the parameter .useCache
in the module’s metadata is set to a character or character vector, then that or those event(s), identified by their name, will be cached. That means that every time the event is called from within a spades
or experiment
call, Cache
will be called. Only the objects inside the simList
that correspond to the inputObjects
or the outputObjects
as defined in the module metadata will be assessed for caching inputs or outputs, respectively. The fact that all and only the named inputObjects
and outputObjects
are cached and returned may be inefficient (i.e., it may cache more objects than are necessary) for individual events.
Similar to module-level caching, event-level caching would be mostly useful for events that have no stochasticity, such as data-preparation events, GIS events etc. Here, we don’t change the module-level caching for randomLandscapes, but we add to it a cache for only the “init” event for fireSpread
.
params(mySim)$fireSpread$.useCache <- "init"
system.time(randomSim <- spades(Copy(mySim), .plotInitialTime = NA,
notOlderThan = Sys.time(), debug = TRUE))
## This is the current event, printed as it is happening:
## eventTime moduleName eventType eventPriority
## 0 checkpoint init 5
## 0 save init 5
## 0 progress init 5
## 0 load init 5
## 0 randomLandscapes init 5
## 0 fireSpread init 5
## 1 fireSpread burn 5
## 1 fireSpread stats 5
## 2 fireSpread burn 5
## 2 fireSpread stats 5
## 3 fireSpread burn 5
## 3 fireSpread stats 5
## 4 fireSpread burn 5
## 4 fireSpread stats 5
## 5 fireSpread burn 5
## 5 fireSpread stats 5
## 5 save spades 10
## user system elapsed
## 1.994 0.101 2.081
# vastly faster the second time
system.time(randomSimCached <- spades(Copy(mySim), .plotInitialTime = NA,
debug = TRUE))
## This is the current event, printed as it is happening:
## eventTime moduleName eventType eventPriority
## 0 checkpoint init 5
## 0 save init 5
## 0 progress init 5
## 0 load init 5
## 0 randomLandscapes init 5
## Using cached copy of randomLandscapes module
## adding to memoised copy0 fireSpread init 5
## Using cached copy of init event in fireSpread module. Adding to memoised copy.
## 1 fireSpread burn 5
## 1 fireSpread stats 5
## 2 fireSpread burn 5
## 2 fireSpread stats 5
## 3 fireSpread burn 5
## 3 fireSpread stats 5
## 4 fireSpread burn 5
## 4 fireSpread stats 5
## 5 fireSpread burn 5
## 5 fireSpread stats 5
## 5 save spades 10
## user system elapsed
## 0.296 0.004 0.298
Any function can be cached using: Cache(FUN = functionName, ...)
.
This will be a slight change to a function call, such as: projectRaster(raster, crs = crs(newRaster))
to Cache(projectRaster, raster, crs = crs(newRaster))
.
ras <- raster(extent(0, 1e3, 0, 1e3), res = 1)
system.time(map <- Cache(gaussMap, ras, cacheRepo = cachePath(mySim),
notOlderThan = Sys.time()))
## user system elapsed
## 4.333 0.097 4.435
# vastly faster the second time
system.time(mapCached <- Cache(gaussMap, ras, cacheRepo = cachePath(mySim)))
## loading cached result from previous gaussMap call, adding to memoised copy
## user system elapsed
## 0.099 0.008 0.106
all.equal(map, mapCached)
## [1] TRUE
Since the cache is simply an archivist
repository, all archivist
functions will work as is. In addition, there are several helpers in the reproducible
package, including showCache
, keepCache
and clearCache
that may be useful. Also, one can access cached items manually (rather than simply rerunning the same Cache
function again).
# examine a part of the Cache
showCache(mySim)[tagKey == "function", -c("artifact")]
## Cache size:
## Total (including Rasters): 8.3 Mb
## Selected objects (not including Rasters): 8.2 Mb
## tagKey tagValue createdDate
## 1: function doEvent 2018-06-12 15:58:37
## 2: function spades 2018-06-12 15:58:37
## 3: function spades 2018-06-12 11:58:47
## 4: function doEvent 2018-06-12 15:57:07
## 5: function spades 2018-06-12 15:57:07
## 6: function doEvent 2018-06-12 15:52:09
## 7: function spades 2018-06-12 15:52:09
## 8: function spades 2018-06-12 11:57:06
## 9: function experiment 2018-06-12 11:57:14
## 10: function experiment 2018-06-12 11:58:53
## 11: function spades 2018-06-12 15:49:30
## 12: function spades 2018-06-12 15:58:29
## 13: function doEvent 2018-06-12 15:49:38
## 14: function spades 2018-06-12 15:49:38
## 15: function doEvent 2018-06-12 11:57:19
## 16: function spades 2018-06-12 11:57:19
## 17: function doEvent 2018-06-12 11:58:58
## 18: function spades 2018-06-12 11:58:58
## 19: function doEvent 2018-06-12 15:49:37
## 20: function spades 2018-06-12 15:49:38
## 21: function spades 2018-06-12 11:58:45
## 22: function doEvent 2018-06-12 12:00:01
## 23: function spades 2018-06-12 12:00:01
## 24: function spades 2018-06-12 11:57:07
## 25: function doEvent 2018-06-12 15:58:37
## 26: function spades 2018-06-12 15:58:37
## 27: function gaussMap 2018-06-12 15:58:42
## tagKey tagValue createdDate
if (requireNamespace("archivist")) {
# get the RasterLayer that was produced with the gaussMap function:
map <- unique(showCache(mySim, userTags = "gaussMap")$artifact) %>%
archivist::loadFromLocalRepo(repoDir = cachePath(mySim), value = TRUE)
clearPlot()
Plot(map)
}
## Cache size:
## Total (including Rasters): 8.3 Mb
## Selected objects (not including Rasters): 7.1 Mb
In general, we feel that a liberal use of Cache
will make a re-useable and reproducible work flow. shiny
apps can be made, taking advantage of Cache
. Indeed, much of the difficulty in managing data sets and saving them for future use, can be accommodated by caching.
simInit --> many .inputObjects calls
experiment --> many spades calls --> many module calls --> many event calls --> many function calls
Lets say we start to introduce caching to this structure. We start from the “inner” most functions that we could imaging Caching would be useful. Lets say there are some GIS operations, like raster::projectRaster
, which operates on an input shapefile. We can Cache the projectRaster
call to make this much faster, since it will always be the same result for a given input raster.
If we look back at our structure above, we see that we still have LOTS of places that are not Cached. That means that the experiment call will still spawn many spades calls, which will still spawn many module calls, and many event calls, just to get to the one Cache(projectRaster)
call which is Cached. This function will likely be called hundreds of times (because experiment
runs the spades
call 100 times due to replication). This is good, but Cache does take some time. So, even if Cache(projectRaster)
takes only 0.02 seconds, calling it hundreds of times means maybe 4 seconds. If we are doing this for many functions, then this will be too slow.
We can start putting Cache
all up the sequence of calls. Unfortunately, the way we use Cache at each of these levels is a bit different, so we need a slightly different approach for each.
experiment
callCache(experiment)
This will assess the simList
(the objects, times, modules, etc.) and if they are all the same, it will return the final list of simList
s that came from the first experiment
call. NOTE: because this can be large, it is likely that you want clearSimEnv = TRUE
, and have all objects that are needed after the experiment call saved to disk. Any stochasticity/randomness inside modules will be frozen. This is likely ok if the objective is to show results in a web app (via shiny or otherwise) or another visualization about the experiment outputs, e.g., comparing treatments, once sufficient stochasticity has been achieved.
mySimListOut <- Cache(experiment, mySim, clearSimEnv = TRUE)
spades
calls inside experiment
experiment(cache = TRUE)
This will cache each of the spades
calls inside the experiment
call. That means that there are as many cache events as there are replicates and experimental treatments, which, again could be a lot. Like caching the experiment
call, stochasticity/randomness will be frozen. Note, one good use of this is when you are making iterative, incremental replication, e.g.,
mySimOut <- experiment(mySim, replicates = 5, cache = TRUE)
You decide after waiting 10 minutes for it to finish, that you need more replication. Rather than start from zero replicates, you can just pick up where you left off:
mySimOut <- experiment(mySim, replicates = 10, cache = TRUE)
This will only add 5 more replicates.
Pass .useCache = TRUE
as a parameter to the module, during the simInit
Some modules are inherently non-random, such as GIS modules, or parameter fitting statistical modules. We expect these to be identical results each time, so we can safely cache the entire module.
parameters = list(
FireModule = list(.useCache = TRUE)
)
mySim <- simInit(..., params = parameters)
mySimOut <- spades(mySim)
The messaging should indicate the caching is happening on every event in that module.
Note: This option REQUIRES that the metadata in inputs and outputs be exactly correct, i.e., all inputObjects and outputObjects must be correctly identified and listed in the defineModule metadata
If the module is cached, and there are errors when it is run, it almost is garanteed to be a problem with the inputObjects
and outputObjects
incorrectly specified.
Cache(<functionName>, <other arguments>)
This will allow fine scale control of individual function calls.
Once nested Caching is used all the way up to the experiment
level and even further up (e.g., if there is a shiny module), then even very complex models can be put into a complete workflow.
The current vision for SpaDES
is that it will allow this type of “data to decisions” complete workflow that allows for deep, robust models, across disciplines, with easily accessible front ends, that are quick and responsive to users, yet can handle data changes, module changes, etc.