Forecasting wildlife habitat (an RSF)

Forest + fire + wildlife, woven together with setupProject

Eliot McIntire and Julie Turner

2026-06-15

The question

As the forest grows and burns over the coming decades, how does wildlife habitat change?

  • a Resource Selection Function (RSF) is a fitted model relating the probability an animal uses a place to that place’s covariates (land cover, forest age/biomass, time since fire, …)
  • RSFpredict takes a previously fitted RSF and re-evaluates it against the simulated landscape – every year
  • the result responds dynamically to succession and disturbance

Three model families

  • forestBiomass_*
  • fire – the scfm family
  • wildlifeRSFpredict

. . .

  • modules live in different GitHub accounts
  • their metadata declares inputs/outputs, so setupProject weaves them into one workflow
modules = c(
  # forest
  "PredictiveEcology/Biomass_borealDataPrep@development",
  "PredictiveEcology/Biomass_core@development",
  "PredictiveEcology/Biomass_regeneration@master",
  # fire (several modules in one repo)
  file.path("PredictiveEcology/scfm@development/modules",
            c("scfmDataPrep", "scfmIgnition", "scfmEscape",
              "scfmSpread", "scfmDiagnostics")),
  # wildlife
  "JWTurn/RSFpredict@main"
)

A fitted model is an input

  • a SpaDES input can be any R object – not just a map
  • here: a previously-fitted RSF, loaded from .rds
  • simulationProcess = "dynamic" → re-predict against the changing landscape each step
  • we don’t have the raw data to refit here – so we ship the model, and could later swap in a fitting module
model = reproducible::prepInputs(
  url = "https://drive.google.com/file/d/1ILE.../view",
  fun = "readRDS",
  destinationPath = "inputs"),

params = list(
  RSFpredict = list(simulationProcess = "dynamic")
)

Study areas & raster templates

  • studyArea – a fixed polygon we report on
  • studyArea_biomassParam – buffered, to avoid edge effects
  • studyAreaCalibration – area to calibrate fire (same here)
  • matching rasterToMatch* templates, derived from one another
  • objects defined earlier are available to later ones
studyArea = prepInputs(url = saURL, fun = "terra::vect"),
studyArea_biomassParam = terra::buffer(studyArea, 10000),
studyAreaCalibration   = studyArea_biomassParam,
rasterToMatch_biomassParam = {
  rtml <- terra::disagg(modelLand[[1]], fact = 2)
  rtml[] <- 1
  terra::mask(rtml, studyArea_biomassParam)
},
rasterToMatchCalibration = rasterToMatch_biomassParam,
rasterToMatch = postProcess(rasterToMatch_biomassParam,
                  cropTo = studyArea, maskTo = studyArea)

Cloud caching for a team

  • slow *DataPrep / fitting steps opt into a shared Google Drive cache
  • it works because the studyArea is fixed:
    • same inputs → same Cache key → same result
  • one person runs the slow fit; everyone else downloads it
  • a random study area would defeat this
options = list(
  reproducible.cloudFolderID = "https://drive.google.com/.../folders/199o..."
),
params = list(
  .globals = list(.studyAreaName = "dehchoN"),
  scfmDataPrep           = list(.useCloud = TRUE),
  Biomass_borealDataPrep = list(.useCloud = TRUE)
)

Run it

  • setupProject() assembles the whole project
  • simInitAndSpades2() runs it from the returned list
  • restartSpades() resumes if interrupted
out <- SpaDES.project::setupProject( ... )

options(reproducible.showSimilar = TRUE)
results <- SpaDES.core::simInitAndSpades2(out)

Explore the outputs interactively

  • .plots = "png" + saved maps → a folder of time-stamped outputs
  • SpaDES.shiny::shine() scans it and builds a Shiny + leaflet viewer
  • step or animate through time; maps on a web basemap, figures as images
  • last - first and custom-difference tabs
if (!require("pak")) install.packages("pak")
pak::pak("SpaDES.shiny")

SpaDES.shiny::shine("outputs")   # a folder
# shine(results)                 # or a simList

Where did the inputs come from?

  • prepInputs() quietly logs every dataset it pulls
  • prepInputsLog() returns that log as a table
  • handy for reporting what data fed a given run
  • (experimental as of reproducible >= 3.1.1.9058)
reproducible::prepInputsLog() |>
  data.table::rbindlist()

Change over time, statically

  • subtract first year from last to see net change
  • the "differences" palette is purpose-built (blue→white→red)
  • rev() so red = negative (worse habitat), blue = positive
diffMap <- results$simPred$year2075 -
           results$simPred$year2025
terra::global(diffMap, fun = 'mean',
              na.rm = TRUE)

terra::plot(diffMap,
  col = rev(terra::map.pal("differences")))

Static-page leaflet: snapshot

  • plotSAsLeaflet(results) renders the simList’s study areas + raster templates as one interactive leaflet widget
  • pan / zoom / toggle layers; ships as static HTML – no Shiny server
  • great for a snapshot in a Quarto book
SpaDES.project::plotSAsLeaflet(results)

Static-page leaflet: change over time

  • plotChangeOverTime(results) discovers every time-series object in outputPath(results) and shows the first → last difference
  • radio buttons to switch between objects (e.g. simPred, simBinMap)
  • per-object legend: zero-centred terra::map.pal("differences"), max at top
  • multi-band rasters split into one radio per band
SpaDES.project::plotChangeOverTime(results)

Takeaway

  • one setupProject() call composes three model families from different authors
  • a fitted model, a fixed study area, and cloud caching make it both reproducible and cheap to re-run as a team
  • after the run: static terra::plot() for snapshots, plotSAsLeaflet() / plotChangeOverTime() for interactive static-HTML views, shine() for full-Shiny exploration
  • the same nimble properties scale from a one-module demo to a multi-family forecast