From a76abf281ea56d49b89c19de2a37ed280833700a Mon Sep 17 00:00:00 2001 From: Eliot McIntire Date: Fri, 11 Sep 2026 08:57:32 -0700 Subject: [PATCH 01/25] Bump development version to 1.0.0.9000 after the v1.0.0 release Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0133867aFrmKtbhpNXasToP9 --- fireSense_IgnitionPredict.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fireSense_IgnitionPredict.R b/fireSense_IgnitionPredict.R index eeffa25..610c4c0 100644 --- a/fireSense_IgnitionPredict.R +++ b/fireSense_IgnitionPredict.R @@ -11,7 +11,7 @@ defineModule(sim, list( person("Alex M", "Chubaty", email = "achubaty@for-cast.ca", role = "ctb") ), childModules = character(), - version = list(SpaDES.core = "0.1.0", fireSense_IgnitionPredict = "1.0.0"), + version = list(SpaDES.core = "0.1.0", fireSense_IgnitionPredict = "1.0.0.9000"), timeframe = as.POSIXlt(c(NA, NA)), timeunit = "year", citation = list("citation.bib"), From 277672da2c28ace29757e2478a2a33a691ed033f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 16:07:27 +0000 Subject: [PATCH 02/25] Re-build fireSense_IgnitionPredict.Rmd --- fireSense_IgnitionPredict.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fireSense_IgnitionPredict.html b/fireSense_IgnitionPredict.html index 349e057..42c96f8 100644 --- a/fireSense_IgnitionPredict.html +++ b/fireSense_IgnitionPredict.html @@ -2994,7 +2994,7 @@

fireSense_IgnitionPredict Manual

-

v.1.0.0

+

v.1.0.0.9000

Last updated: 2026-09-11

From 58da4fe65d80a32ccc3bfb89c31debf3a3abd93c Mon Sep 17 00:00:00 2001 From: Eliot McIntire Date: Fri, 11 Sep 2026 21:33:47 -0700 Subject: [PATCH 03/25] ci: publish a pkgdown site for this module Thin caller for the shared pkgdown-module reusable workflow, which renders fireSense_IgnitionPredict.Rmd as a pkgdown article via SpaDES.core::moduleRmdToVignette(). Builds on pull requests to development and deploys to gh-pages on push. Same caller already running on fireSense_SpreadFit (#21). See PredictiveEcology/SpaDES-modules#40. --- .github/workflows/pkgdown-module.yaml | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/pkgdown-module.yaml diff --git a/.github/workflows/pkgdown-module.yaml b/.github/workflows/pkgdown-module.yaml new file mode 100644 index 0000000..79e472c --- /dev/null +++ b/.github/workflows/pkgdown-module.yaml @@ -0,0 +1,30 @@ +## Thin caller: build this module's pkgdown site and publish it with GitHub Pages. +## The job lives in PredictiveEcology/actions, so its setup and pins move once: +## https://github.com/PredictiveEcology/actions/blob/main/.github/workflows/pkgdown-module.yaml +## Background: PredictiveEcology/SpaDES-modules#40. +## +## Publishes from `development` (the pilot's choice; which branch should publish +## is an open question on #40). Pull requests build the site without deploying. +## The site is at https://predictiveecology.github.io/fireSense_IgnitionPredict/ until a +## custom domain is set with the `url` input. + +name: pkgdown-module + +on: + push: + branches: [development] + pull_request: + branches: [development] + workflow_dispatch: + +jobs: + pkgdown-module: + ## The called workflow's deploy job pushes to gh-pages. A called workflow can + ## only reduce what the caller grants, never widen it, so the write scope has + ## to be granted here. + permissions: + contents: write + uses: PredictiveEcology/actions/.github/workflows/pkgdown-module.yaml@main + with: + module: fireSense_IgnitionPredict + From f884f190bac4013d3daca08cabc76e1cedc6d770 Mon Sep 17 00:00:00 2001 From: Eliot McIntire Date: Sun, 20 Sep 2026 09:46:55 -0700 Subject: [PATCH 04/25] Remove dead code; document functions; correct metadata descs and Rmd Removes scaleAgain() (no caller), commented-out code, the unreachable code after stop("Not tested anymore") and a browser() leftover. No behaviour change. Co-Authored-By: Claude Fable 5.1 --- fireSense_IgnitionPredict.R | 156 ++++++++++++---------------------- fireSense_IgnitionPredict.Rmd | 30 ++++--- 2 files changed, 73 insertions(+), 113 deletions(-) diff --git a/fireSense_IgnitionPredict.R b/fireSense_IgnitionPredict.R index 610c4c0..ff0c1a7 100644 --- a/fireSense_IgnitionPredict.R +++ b/fireSense_IgnitionPredict.R @@ -1,8 +1,8 @@ defineModule(sim, list( name = "fireSense_IgnitionPredict", - description = "Predict rates of fire frequency from a model fitted using the - fireSense_IgnitionFit module. These can be used to feed the - ignition component of a landscape fire model (e.g fireSense).", + description = paste( + "Predicts annual ignition and escape probabilities from the models fitted by", + "fireSense_IgnitionFit and fireSense_EscapeFit, and draws the pixels that ignite and escape."), keywords = c("fire frequency", "additive property", "poisson", "negative binomial", "fireSense"), authors = c( person("Eliot", "McIntire", email = "eliot.mcintire@nrcan-rncan.gc.ca", role = c("aut", "cre")), @@ -22,33 +22,24 @@ defineModule(sim, list( ), loadOrder = list(after = "fireSense_dataPrepPredict"), parameters = bindrows( - # Eliot removed this Jan 2026; why not put this in the `reqdPkgs`? And also, it is not used anymore. - # defineParameter("ignitionFit_Predict_Package", "character", "glmmTMB", NA, NA, - # desc = paste( - # "The package used to fit the ignitionFit model.", - # "It wil be loaded using Require." - # ) - # ), defineParameter("modelAlgorithm", "character", "xgboost", NA, NA, - "Can be `xgboost`, `glmmtmb`, `glm.nb`, `glmmadaptive`, `glm`; only `xgboost` is supported currently"), + paste("Algorithm used to fit the models; only `xgboost` is supported.", + "Must agree with the value in the other fireSense modules.")), defineParameter("rescaleVars", "logical", default = TRUE, - desc = paste("Attempt to rescale variables? If `rescalers` is defined,", - "use it to rescale variables as `var / rescalers['var']`. ", - "Otherwise, `scale()` will be used to rescale variables to `[0,1]`,", - "if they are not already within this range.")), + desc = paste("Rescale the covariates before predicting? With `xgboost` they are standardized", + "with `scale()`. Must agree with the value in the other fireSense modules.")), defineParameter(".runInitialTime", "numeric", start(sim), NA, NA, - desc = "when to start this module? By default, the start - time of the simulation." + desc = "Time of the first prediction." ), defineParameter(".runInterval", "numeric", 1, NA, NA, - desc = paste("optional. Interval between two runs of this module"), - ("expressed in units of simulation time. By default, 1 year.") + desc = "Interval between predictions, in years. `NA` predicts once." ), defineParameter(".saveInitialTime", "numeric", NA, NA, NA, - desc = "optional. When to start saving output to a file." + desc = "Time of the first `save` event. `NA` means never." ), defineParameter(".saveInterval", "numeric", NA, NA, NA, - desc = "optional. Interval between save events." + desc = paste("Interval between `save` events.", + "If not `NA`, the ignition probability raster is also plotted each year.") ), defineParameter(".useCache", "logical", FALSE, NA, NA, desc = paste( @@ -60,43 +51,53 @@ defineModule(sim, list( ), inputObjects = bindrows( expectsInput("fireSense_EscapeFitted", "fireSense_EscapeFit", - desc = "An object of class `fireSense_EscapeFit` created with the `fireSense_IgnitionFit` module.", + desc = "Fitted escape models (`$modelList$model`, one per fold), from `fireSense_EscapeFit`.", sourceURL = NA ), expectsInput("fireSense_IgnitionFitted", "fireSense_IgnitionFit", - desc = "An object of class `fireSense_IgnitionFit` created with the `fireSense_IgnitionFit` module.", + desc = paste("Fitted ignition models (`$modelList$model`, one per fold) and `$modelList$fittingRes`,", + "from `fireSense_IgnitionFit`."), sourceURL = NA ), expectsInput("fireSense_igAndEscapePred_Covariates", "data.table", desc = paste( - "A `data.table` with prediction variables and a", - "column named 'pixelID'" + "This year's covariates, from `fireSense_dataPrepPredict`.", + "`pixelID` is the cell index of `ignitionFitRTM`." ) ), expectsInput("flammableRTM", "SpatRaster", sourceURL = NA, - desc = "RTM without ice/rocks/urban/water. Flammable map with 0 and 1." + desc = "Binary raster, 1 where the pixel is flammable." ), ), outputObjects = bindrows( createsOutput("fireSense_IgAndEscapeProbRas", "SpatRaster", - desc = "a raster layer of the annual ignition and escape probabilities" + desc = paste("Two layers, `ignitionProb` (expected ignitions per pixel) and `escapeProb`,", + "at the resolution of `ignitionFitRTM`.") ), createsOutput( "ignitionsAndEscapes", "data.table", - paste("A data.table containing pixelID (referencing flammableRTM),", - "ignitions, escapes, and their associated probabilities") + paste("One row per ignited pixel, in random order: `pixelID` (cell index of `flammableRTM`),", + "and `igProb`, `ignitions`, `escapeProb`, `escapes` of the coarse pixel it was drawn from.") ) ) )) +#' Event dispatcher +#' +#' Events: `init`, `run` (predict, repeated every `.runInterval`), `save`. +#' +#' @param sim A `simList`. +#' @param eventTime Time of the event. +#' @param eventType Name of the event. +#' @param debug Not used. +#' +#' @return The `simList`, invisibly. doEvent.fireSense_IgnitionPredict <- function(sim, eventTime, eventType, debug = FALSE) { moduleName <- currentModule(sim) switch(eventType, init = { - # Require(P(sim)$ignitionFit_Predict_Package) - sim <- scheduleEvent(sim, eventTime = P(sim)$.runInitialTime, moduleName, "run") if (!is.na(P(sim)$.saveInitialTime)) { @@ -124,6 +125,15 @@ doEvent.fireSense_IgnitionPredict <- function(sim, eventTime, eventType, debug = return(invisible(sim)) } +#' Predict and draw this year's ignitions and escapes +#' +#' Averages the per-fold predictions of the ignition models, draws ignitions per coarse pixel +#' (Poisson), then does the same for escapes (binomial, given ignition). Each ignition is placed in +#' a random flammable `flammableRTM` pixel inside its coarse pixel. +#' +#' @param sim A `simList`. +#' +#' @return The `simList`, invisibly, with `fireSense_IgAndEscapeProbRas` and `ignitionsAndEscapes`. IgnitionPredictRun <- function(sim) { ## checks if (is.null(sim$fireSense_IgnitionFitted$lambdaRescaleFactor)) { @@ -144,30 +154,6 @@ IgnitionPredictRun <- function(sim) { set(igCov, NULL, "pixelID", pixelId) - # scaleData <- sim$fireSense_IgnitionFitted$scaleData - # if (!is.null(scaleData)) { - # vars <- unlist(scaleData$dimnames) - # vars <- setdiff(vars, c("pixelID", "year")) - # - # - # browser() - # # these rescalings are possibly because the rescales are on the raw species, - # # but the fuel classes may have been merged - # intersect(vars, names(igCov)) - # for (v in vars) { - # set(igCov, NULL, v, - # scaleAgain(igCov[[v]], - # scaleData$`scaled:center`[[v]], - # scaleData$`scaled:scale`[[v]])) - # } - # } - # if (!is.null(sim$fireSense_IgnitionFitted$rescales)) { - # igCov <- rescaleVarsByMagnitude( - # igCov, - # sim$fireSense_IgnitionFitted$rescales - # ) - # } - # From here, it makes predictions from each KFold, then averages them to get the probabilities for # each cell; same for Escape, which is conditional on having ignited. modsHere <- sim$fireSense_IgnitionFitted$modelList$model @@ -177,7 +163,7 @@ IgnitionPredictRun <- function(sim) { predict(model, newdata = igCov) }) - predsIgnsMat <- do.call(cbind, predsIgnList)# |> sort() |> unname() + predsIgnsMat <- do.call(cbind, predsIgnList) predsIgns <- rowMeans(predsIgnsMat) igns <- rpois(NROW(predsIgns), lambda = predsIgns) # can't use rtweedie because don't know the dispersion parameter @@ -190,7 +176,7 @@ IgnitionPredictRun <- function(sim) { predsEsc <- predict(model, newdata = igCov[whHasIgns]) pmax(pmin(1, predsEsc), 0) # the escape model is tweedie, so can go above 1, below 0 rarely }) - predsEscsMat <- do.call(cbind, predsEscList)# |> sort() |> unname() + predsEscsMat <- do.call(cbind, predsEscList) predsEscs <- rowMeans(predsEscsMat) escs <- rbinom(NROW(predsEscs), size = igns[whHasIgns], prob = predsEscs) @@ -200,37 +186,6 @@ IgnitionPredictRun <- function(sim) { igDisAggFactor <- ceiling(sim$fireSense_IgnitionFitted$modelList$fittingRes / c(res(sim$flammableRTM)[1])) } else { stop("Not tested anymore") - # TODO: I think fireSenseUtils::predictIgnition is now redundant - # TODO: let a user pass a package:fun to prdict, like LandR.CS, else this? - igCov[, igProb := predict(sim$fireSense_IgnitionFitted$model, - newdata = igCov, - se.fit = FALSE, - re.form = NA, type = "response" - )] - # TODO: this is 1 in all applications except Ceres' (which predate the new model) - igCov[, igProb := igProb * sim$fireSense_IgnitionFitted$lambdaRescaleFactor] - - ignitionFamily <- params(sim)$fireSense_IgnitionFit$ignitionFamily[[1]] |> format() - - possTypes <- data.table(family = c("poisson", "nbinom"), generator = c("rpois", "rnbinom")) - generator <- possTypes[family %in% ignitionFamily]$generator - generator <- eval(parse(text = generator)) - - igCov[, ignitions := generator( - n = length(igProb), - lambda = igProb - )] - - # Escape - igCov[, escapeProb := predict(sim$fireSense_EscapeFitted$model, - newdata = igCov, - se.fit = FALSE, - re.form = NA, type = "response" - )] - - igCov[, escapes := rbinom(n = .N, size = ignitions, prob = escapeProb)] - - igDisAggFactor <- ceiling(sim$fireSense_IgnitionFitted$fittingRes / c(res(sim$flammableRTM)[1])) } # Ignite - accounting for spatial resolution of models @@ -240,16 +195,13 @@ IgnitionPredictRun <- function(sim) { igRas[igCov$pixelID] <- igCov$pixelID # can't use disagg because it may not be round pixels - # igRas <- disagg(igRas, fact = igDisAggFactor) igRas <- postProcess(igRas, to = sim$flammableRTM, method = "near") igRas[sim$flammableRTM[] != 1] <- NA igRasDT <- as.data.table(igRas, cells = TRUE) setnames(igRasDT, new = c("pixelID", "chunkyPixels")) - # igs <- igCov[igRasDT, on = c("pixelID" = "chunkyPixels")] # nomatch = NULL --> removes pixels that aren't in igCov, which are non flammable igs <- igRasDT[igCov, on = c("chunkyPixels" = "pixelID"), nomatch = NULL] - # igs <- igRasDT[igCov, on = c("chunkyPixels" = "pixelID")] # randomly sample # use unique because of multiple pixelID per chunkyPixel drawTable <- unique(igs[ignitions > 0, .(chunkyPixels, ignitions, escapes)]) @@ -259,7 +211,6 @@ IgnitionPredictRun <- function(sim) { pixelID_pop <- igs[chunkyPixels == sampledChunk$chunkyPixels] drawn <- sample(pixelID_pop$pixelID, size = sampledChunk$ignitions) igPixels <- try(ig[pixelID %in% drawn, .(pixelID, igProb, ignitions, escapeProb, escapes)]) - if (is(igPixels, "try-error")) browser() return(igPixels) } ) |> @@ -283,6 +234,11 @@ IgnitionPredictRun <- function(sim) { return(invisible(sim)) } +#' Write the predicted raster to `outputPath` +#' +#' @param sim A `simList`. +#' +#' @return The `simList`, invisibly. IgnitionPredictSave <- function(sim) { timeUnit <- timeunit(sim) currentTime <- time(sim, timeUnit) @@ -295,15 +251,13 @@ IgnitionPredictSave <- function(sim) { return(invisible(sim)) } +#' Supply default inputs +#' +#' There are none. +#' +#' @param sim A `simList`. +#' +#' @return The `simList`, invisibly. .inputObjects <- function(sim) { - # cacheTags <- c(currentModule(sim), "otherFunctions:.inputObjects") - # dPath <- asPath(inputPath(sim), 1) - # message(currentModule(sim), ": using dataPath '", dPath, "'.") - return(invisible(sim)) } - - -scaleAgain <- function(v, center, scale) { - (v - center)/ scale -} diff --git a/fireSense_IgnitionPredict.Rmd b/fireSense_IgnitionPredict.Rmd index cb8775d..a6af4ab 100644 --- a/fireSense_IgnitionPredict.Rmd +++ b/fireSense_IgnitionPredict.Rmd @@ -45,11 +45,20 @@ download.file(url = "https://img.shields.io/badge/Made%20with-Markdown-1f425f.pn ### Module summary -Predicts fire frequency or rates of fire counts using a model fitted with the `fireSense_IgnitionFit` module. -Use them to feed the ignition component of a landscape fire model (e.g fireSense [@Marchal:2017a; @Marchal:2017b; @Marchal:2019]). +Each year, predicts ignitions and escapes from the models fitted by *fireSense_IgnitionFit* and *fireSense_EscapeFit*, for the ignition component of fireSense [@Marchal:2017a; @Marchal:2017b; @Marchal:2019]. + +1. The covariates in `fireSense_igAndEscapePred_Covariates` are rescaled with `fireSenseUtils::prepareCovariatesOuter()`, the function used for fitting (`rescaleVars`, `modelAlgorithm`). +2. Expected ignitions per coarse pixel are the mean of the predictions of the per-fold ignition models; the number of ignitions is drawn from a Poisson. +3. For coarse pixels with ignitions, the escape probability is the mean of the per-fold escape models, clamped to [0, 1]; escapes are drawn from a binomial with size = ignitions. +4. Each ignition is placed in a randomly chosen flammable pixel of `flammableRTM` inside its coarse pixel. + +Only `xgboost` models are supported. ### Module inputs and parameters +`ignitionFitRTM` (the coarse raster used for fitting, from *fireSense_dataPrepFit*) is also read from the `simList`, though it is not declared as an input. +`modelAlgorithm` and `rescaleVars` must have the same value in every module that defines them. + Table \@ref(tab:moduleInputs-fireSense-IgnitionPredict) shows the full list of module inputs. ```{r moduleInputs-fireSense-IgnitionPredict, echo = FALSE, eval = TRUE, message = FALSE, warning = FALSE} @@ -69,17 +78,14 @@ knitr::kable(df_params, caption = "List of (ref:fireSense-IgnitionPredict) param ``` ### Events - -- Module initialization; -- Make predictions; + +- `init`: schedules `run` at `.runInitialTime`, and `save` at `.saveInitialTime` if that is not `NA`. +- `run`: makes the predictions and draws described above; repeats every `.runInterval`. +- `save`: writes a raster to `outputPath(sim)`; repeats every `.saveInterval`. It writes `sim$fireSense_IgnitionPredicted`, which the module no longer creates, so it currently fails. ### Plotting - -Write what is plotted. -### Saving - -Write what is saved. +If `.saveInterval` is not `NA`, the ignition probability raster is plotted each year with `Plots()`. ### Module outputs @@ -93,8 +99,8 @@ knitr::kable(df_outputs, caption = "List of (ref:fireSense-IgnitionPredict) outp ### Links to other modules - -Predictions made with this module can be used to feed the ignition component of a landscape fire model (e.g fireSense). +Runs after *fireSense_dataPrepPredict*, which supplies the covariates. `ignitionsAndEscapes` is used by *fireSense* to start fires. +It is normally run as part of the [fireSense](https://github.com/PredictiveEcology/fireSense) module group. ### Getting help From 6c60cb24ce319a4d7e4cf0314997c500d9c68f90 Mon Sep 17 00:00:00 2001 From: Eliot McIntire Date: Sun, 20 Sep 2026 11:46:29 -0700 Subject: [PATCH 05/25] Add value-level tests for the run event, what reaches the models, and scheduling 86 expectations (was 5). Fitted models are stand-ins that log the newdata they are given, so no xgboost is needed. All pass on development and on this branch; three tests of correct behaviour are skipped as known defects. Co-Authored-By: Claude Fable 5.1 --- tests/testthat/setup-toyIgnition.R | 121 +++++++++++++++++++++++ tests/testthat/test-covariate-scaling.R | 44 +++++++++ tests/testthat/test-events.R | 43 ++++++++ tests/testthat/test-failures.R | 28 ++++++ tests/testthat/test-metadata.R | 30 +++++- tests/testthat/test-run-values.R | 125 ++++++++++++++++++++++++ 6 files changed, 386 insertions(+), 5 deletions(-) create mode 100644 tests/testthat/setup-toyIgnition.R create mode 100644 tests/testthat/test-covariate-scaling.R create mode 100644 tests/testthat/test-events.R create mode 100644 tests/testthat/test-failures.R create mode 100644 tests/testthat/test-run-values.R diff --git a/tests/testthat/setup-toyIgnition.R b/tests/testthat/setup-toyIgnition.R new file mode 100644 index 0000000..9277287 --- /dev/null +++ b/tests/testthat/setup-toyIgnition.R @@ -0,0 +1,121 @@ +## Toy inputs for the `run` event. A setup file rather than a helper, so that it shares an +## environment with `moduleName` and `testPaths` from setup.R. +## +## Tests run inside the namespace of the package rendition, which does not import +## data.table, so `dt[i, j]` there is NOT data.table-aware: tables are handled with base +## subsetting and data.table::set() only. +## +## The fitted models are stand-ins of class "toyFold". xgboost is not among the module's +## `reqdPkgs`, so CI does not have it; and a stand-in lets a test see exactly what the +## module hands to predict(). Each "model" is a function of `newdata`; every call is +## logged in `toyLog`. + +## The run event calls postProcess() unqualified, but `reproducible` is not in the module's +## `reqdPkgs`: in a project some other module attaches it. It is always installed (SpaDES.core +## imports it), so attach it here; otherwise the event stops with +## 'could not find function "postProcess"'. +suppressPackageStartupMessages(library(reproducible)) + +toyLog <- new.env() +toyLogReset <- function() rm(list = ls(toyLog), envir = toyLog) + +predict.toyFold <- function(object, newdata, ...) { + key <- sprintf("%s_%03d", object$label, length(ls(toyLog)) + 1L) + assign(key, as.data.frame(newdata), envir = toyLog) + object$fun(as.data.frame(newdata)) +} +registerS3method("predict", "toyFold", predict.toyFold) + +toyFold <- function(fun, label) structure(list(fun = fun, label = label), class = "toyFold") + +## calls logged for one model label, in call order +toyCalls <- function(label) { + keys <- sort(grep(paste0("^", label, "_"), ls(toyLog), value = TRUE)) + lapply(keys, get, envir = toyLog) +} + +## Grids, both 0..4 x 0..4. The fine (simulation) grid is 8 x 8 with 0.5-unit cells; the +## coarse (fitting) grid is 2 x 2 with 2-unit cells, so each coarse cell holds 16 fine cells: +## coarse 1 = fine rows 1-4, cols 1-4; coarse 2 = rows 1-4, cols 5-8; +## coarse 3 = rows 5-8, cols 1-4; coarse 4 = rows 5-8, cols 5-8. +## Fine cell number = (row - 1) * 8 + col. +coarseOfFine <- function(cell) { + row <- (cell - 1) %/% 8 + 1 + col <- (cell - 1) %% 8 + 1 + (row > 4) * 2 + (col > 4) + 1 +} + +## By default fine cells 5 and 6 (in coarse 2) and 64 (in coarse 4) are not flammable. +toyFlammable <- function(nonFlammable = c(5L, 6L, 64L)) { + r <- terra::rast(nrows = 8, ncols = 8, xmin = 0, xmax = 4, ymin = 0, ymax = 4, vals = 1, + crs = "EPSG:3005") + r[nonFlammable] <- 0 + r +} + +toyCoarse <- function() { + terra::rast(nrows = 2, ncols = 2, xmin = 0, xmax = 4, ymin = 0, ymax = 4, vals = 1, + crs = "EPSG:3005") +} + +## one row per coarse cell, as fireSense_dataPrepPredict makes them +toyIgCovariates <- function(MDC = c(100, 120, 140, 160), year = 2001) { + data.table::data.table(pixelID = 1:4, MDC = MDC, youngAge = c(0, 0.25, 0.5, 1), + year = year) +} + +## `ignFuns` and `escFuns` are lists of functions of newdata, one per fold +toyIgInputs <- function(ignFuns, escFuns, covs = toyIgCovariates(), + foldNames = paste0("Fold", seq_along(ignFuns)), + flammableRTM = toyFlammable()) { + ign <- Map(toyFold, ignFuns, paste0("ign", seq_along(ignFuns))) + esc <- Map(toyFold, escFuns, paste0("esc", seq_along(escFuns))) + names(ign) <- foldNames + names(esc) <- paste0("Fold", seq_along(escFuns)) + list( + fireSense_IgnitionFitted = structure( + list(modelList = list(model = ign, fittingRes = 2)), class = "fireSense_IgnitionFit"), + fireSense_EscapeFitted = structure( + list(modelList = list(model = esc)), class = "fireSense_EscapeFit"), + fireSense_igAndEscapePred_Covariates = covs, + flammableRTM = flammableRTM, + ignitionFitRTM = toyCoarse() + ) +} + +toyIgSim <- function(objects, params = list(), times = list(start = 2001, end = 2001)) { + SpaDES.core::simInit( + times = c(times, timeunit = "year"), + modules = moduleName, + params = stats::setNames(list(params), moduleName), + objects = objects, + paths = testPaths + ) +} + +## fireSenseUtils::rescaleCovariates() warns on every call that there is no `ignitions` +## column to remove; that is noise here, so muffle that one warning only. +toyIgRun <- function(objects, ..., seed = 42) { + toyLogReset() + sim <- toyIgSim(objects, ...) + set.seed(seed) + withCallingHandlers( + SpaDES.core::spades(sim, debug = FALSE), + warning = function(w) { + if (grepl("Tried to assign NULL to column 'ignitions'", conditionMessage(w))) + invokeRestart("muffleWarning") + }) +} + +igTable <- function(sim) as.data.frame(sim$ignitionsAndEscapes) +probVals <- function(sim) terra::values(sim$fireSense_IgAndEscapeProbRas) + +## a constant prediction, whatever the covariates +constant <- function(x) function(nd) rep(x, nrow(nd)) +## a prediction that depends on which coarse pixel it is: vals[pixelID] +byPixel <- function(vals) function(nd) vals[nd$pixelID] + +evOf <- function(dt, type) { + df <- as.data.frame(dt) + df[df$moduleName == "fireSense_IgnitionPredict" & df$eventType == type, , drop = FALSE] +} diff --git a/tests/testthat/test-covariate-scaling.R b/tests/testthat/test-covariate-scaling.R new file mode 100644 index 0000000..b068ba0 --- /dev/null +++ b/tests/testthat/test-covariate-scaling.R @@ -0,0 +1,44 @@ +## What the run event hands to the fitted models. + +oneFold <- function(covs, ...) toyIgInputs(list(constant(0)), list(constant(0)), covs = covs, ...) + +test_that("with rescaleVars = FALSE the models get the covariates exactly as supplied", { + covs <- toyIgCovariates() + sim <- toyIgRun(oneFold(covs), params = list(rescaleVars = FALSE)) + nd <- toyCalls("ign1")[[1]] + expect_setequal(names(nd), c("pixelID", "MDC", "youngAge", "year")) + expect_identical(nd$MDC, c(100, 120, 140, 160)) + expect_identical(nd$youngAge, c(0, 0.25, 0.5, 1)) + expect_identical(nd$year, rep(2001, 4)) + expect_identical(nd$pixelID, 1:4) +}) + +test_that("pixelID reaches the models as the cell number, never rescaled", { + sim <- toyIgRun(oneFold(toyIgCovariates()), params = list(rescaleVars = TRUE)) + expect_identical(toyCalls("ign1")[[1]]$pixelID, 1:4) +}) + +test_that("covariates are standardised with the FIT's centre and scale", { + skip(paste("known defect: IgnitionPredictRun() standardises each year's covariates with that", + "year's own mean and sd (fireSenseUtils::prepareCovariatesOuter -> scale()),", + "not with fireSense_IgnitionFitted$scaleData")) + ## The fit standardised MDC with centre 100 and scale 50, and stored that in `scaleData` + ## (as fireSense_IgnitionFit does). A cool year and a year that is 100 MDC units hotter + ## everywhere must therefore look different to the model: + ## cool: (c(100, 120, 140, 160) - 100) / 50 = 0, 0.4, 0.8, 1.2 + ## hot: (c(200, 220, 240, 260) - 100) / 50 = 2, 2.4, 2.8, 3.2 + ## On development both years reach the model as -1.162, -0.387, 0.387, 1.162, and `year` + ## (constant within a year) as NaN. + scaleData <- list(`scaled:center` = c(MDC = 100, youngAge = 0.5, year = 2000), + `scaled:scale` = c(MDC = 50, youngAge = 0.5, year = 10)) + seen <- lapply(list(cool = 0, hot = 100), function(add) { + ins <- oneFold(toyIgCovariates(MDC = c(100, 120, 140, 160) + add)) + ins$fireSense_IgnitionFitted$scaleData <- scaleData + toyIgRun(ins, params = list(rescaleVars = TRUE)) + toyCalls("ign1")[[1]] + }) + expect_equal(seen$cool$MDC, c(0, 0.4, 0.8, 1.2)) + expect_equal(seen$hot$MDC, c(2, 2.4, 2.8, 3.2)) + expect_equal(seen$cool$youngAge, c(-1, -0.5, 0, 1)) + expect_equal(seen$cool$year, rep(0.1, 4)) +}) diff --git a/tests/testthat/test-events.R b/tests/testthat/test-events.R new file mode 100644 index 0000000..802d8ec --- /dev/null +++ b/tests/testthat/test-events.R @@ -0,0 +1,43 @@ +inputsNoIgnition <- function() toyIgInputs(list(constant(0)), list(constant(0))) + +test_that("run repeats every .runInterval from .runInitialTime", { + sim <- toyIgRun(inputsNoIgnition(), times = list(start = 2001, end = 2003)) + expect_equal(evOf(SpaDES.core::completed(sim), "run")$eventTime, 2001:2003) + expect_equal(evOf(SpaDES.core::events(sim), "run")$eventTime, 2004) + expect_equal(evOf(SpaDES.core::completed(sim), "init")$eventTime, 2001) + ## one call to the ignition model per run event + expect_length(toyCalls("ign1"), 3L) +}) + +test_that(".runInitialTime and .runInterval are honoured", { + sim <- toyIgRun(inputsNoIgnition(), params = list(.runInitialTime = 2002, .runInterval = 2), + times = list(start = 2001, end = 2005)) + expect_equal(evOf(SpaDES.core::completed(sim), "run")$eventTime, c(2002, 2004)) + expect_equal(evOf(SpaDES.core::events(sim), "run")$eventTime, 2006) +}) + +test_that(".runInterval = NA predicts once", { + sim <- toyIgRun(inputsNoIgnition(), params = list(.runInterval = NA_real_), + times = list(start = 2001, end = 2003)) + expect_equal(evOf(SpaDES.core::completed(sim), "run")$eventTime, 2001) + expect_identical(nrow(evOf(SpaDES.core::events(sim), "run")), 0L) +}) + +test_that("no save event by default; .saveInitialTime schedules one, last in its year", { + sim <- toyIgRun(inputsNoIgnition(), times = list(start = 2001, end = 2002)) + expect_identical(nrow(evOf(SpaDES.core::completed(sim), "save")), 0L) + expect_identical(nrow(evOf(SpaDES.core::events(sim), "save")), 0L) + ## scheduled beyond end(sim) so that the (broken) save event itself never runs + sim <- toyIgRun(inputsNoIgnition(), params = list(.saveInitialTime = 2010), + times = list(start = 2001, end = 2001)) + sv <- evOf(SpaDES.core::events(sim), "save") + expect_equal(sv$eventTime, 2010) + expect_equal(sv$eventPriority, SpaDES.core::.last()) +}) + +test_that("nothing is predicted before the first run event", { + sim <- toyIgRun(inputsNoIgnition(), params = list(.runInitialTime = 2005), + times = list(start = 2001, end = 2002)) + expect_null(sim$ignitionsAndEscapes) + expect_null(sim$fireSense_IgAndEscapeProbRas) +}) diff --git a/tests/testthat/test-failures.R b/tests/testthat/test-failures.R new file mode 100644 index 0000000..e637a3d --- /dev/null +++ b/tests/testthat/test-failures.R @@ -0,0 +1,28 @@ +test_that("a fitted object without a list of fold models stops", { + ins <- toyIgInputs(list(constant(1)), list(constant(1))) + ins$fireSense_IgnitionFitted$modelList$model <- NULL + expect_error(toyIgRun(ins), "Not tested anymore") +}) + +test_that("an ignition placed in a coarse cell with ONE flammable fine cell lands in that cell", { + skip(paste("known defect: sample(x, size) with length-1 x samples from 1:x, so the ignition", + "can land in any lower-numbered fine cell, in another coarse cell")) + ## coarse 4 = fine rows 5-8, cols 5-8; leave only fine cell 64 flammable in it + nf <- setdiff(as.integer(outer(5:8, (4:7) * 8, `+`)), 64L) + for (seed in 1:8) { + ins <- toyIgInputs(list(byPixel(c(0, 0, 0, 0.7))), list(constant(1)), + flammableRTM = toyFlammable(nf)) + ig <- igTable(toyIgRun(ins, seed = seed)) + expect_true(all(ig$pixelID == 64L)) + } +}) + +test_that("more ignitions than flammable fine cells does not stop the simulation", { + skip(paste("known defect: sample() without replacement errors with 'cannot take a sample", + "larger than the population' when ignitions > flammable fine cells in the coarse cell")) + nf <- setdiff(as.integer(outer(5:8, (4:7) * 8, `+`)), 64L) + ins <- toyIgInputs(list(byPixel(c(0, 0, 0, 20))), list(constant(1)), + flammableRTM = toyFlammable(nf)) + ig <- igTable(toyIgRun(ins)) + expect_true(all(ig$pixelID == 64L)) +}) diff --git a/tests/testthat/test-metadata.R b/tests/testthat/test-metadata.R index cc81f63..7257338 100644 --- a/tests/testthat/test-metadata.R +++ b/tests/testthat/test-metadata.R @@ -34,11 +34,31 @@ test_that("outputs are the expected names and classes", { ) }) -test_that("parameters are the expected names", { +## deparsed defaults, so that a changed default fails as loudly as a renamed parameter +paramTable <- function(md) { + p <- md$parameters + out <- data.frame( + class = as.character(unlist(p$paramClass)), + default = vapply(p$default, function(d) paste(deparse(as.vector(d)), collapse = ""), ""), + row.names = p$paramName + ) + out[order(rownames(out), method = "radix"), ] # C order, whatever the locale +} + +test_that("parameters have the expected names, classes and defaults", { md <- SpaDES.core::moduleMetadata(module = moduleName, path = modulePath) - expect_identical( - sort(md$parameters$paramName), - sort(c(".runInitialTime", ".runInterval", ".saveInitialTime", ".saveInterval", - ".useCache", "modelAlgorithm", "rescaleVars")) + expected <- data.frame( + class = c("numeric", "numeric", "numeric", "numeric", "logical", "character", "logical"), + ## .runInitialTime defaults to start(sim), which is 0 when only metadata is parsed + default = c("0", "1", "NA", "NA", "FALSE", "\"xgboost\"", "TRUE"), + row.names = c(".runInitialTime", ".runInterval", ".saveInitialTime", ".saveInterval", ".useCache", + "modelAlgorithm", "rescaleVars") ) + expect_identical(paramTable(md), expected) +}) + +test_that("fireSenseUtils is a declared dependency, so CI installs it", { + md <- SpaDES.core::moduleMetadata(module = moduleName, path = modulePath) + expect_true(any(grepl("^PredictiveEcology/fireSenseUtils@development", unlist(md$reqdPkgs)))) + expect_identical(md$timeunit, "year") }) diff --git a/tests/testthat/test-run-values.R b/tests/testthat/test-run-values.R new file mode 100644 index 0000000..d53f763 --- /dev/null +++ b/tests/testthat/test-run-values.R @@ -0,0 +1,125 @@ +## What the run event predicts and draws, on a 2 x 2 coarse grid over an 8 x 8 fine grid. +## Default toy: two ignition folds that predict 0, 2, 3, 0 and 0, 4, 3, 0 expected +## ignitions for coarse cells 1..4, so only coarse cells 2 and 3 can ignite. + +twoFolds <- function(escFuns = list(constant(1)), ...) { + toyIgInputs(list(byPixel(c(0, 2, 3, 0)), byPixel(c(0, 4, 3, 0))), escFuns, ...) +} + +test_that("ignition probability is the mean of the per-fold predictions, on the coarse grid", { + sim <- toyIgRun(twoFolds()) + ras <- sim$fireSense_IgAndEscapeProbRas + expect_identical(names(ras), c("ignitionProb", "escapeProb")) + expect_true(terra::compareGeom(ras, toyCoarse())) + ## fold means: (0+0)/2, (2+4)/2, (3+3)/2, (0+0)/2 + expect_equal(probVals(sim)[, "ignitionProb"], c(0, 3, 3, 0)) +}) + +test_that("escape probability is the fold mean, clamped to [0, 1] per fold, only where there are ignitions", { + ## fold 1 predicts 1.7 (clamped to 1), fold 2 predicts 0.5: mean 0.75 + sim <- toyIgRun(twoFolds(list(constant(1.7), constant(0.5)))) + expect_equal(probVals(sim)[, "escapeProb"], c(NA, 0.75, 0.75, NA)) + ## fold 1 predicts -0.5 (clamped to 0), fold 2 predicts 0.1: mean 0.05 + sim <- toyIgRun(twoFolds(list(constant(-0.5), constant(0.1)))) + expect_equal(probVals(sim)[, "escapeProb"], c(NA, 0.05, 0.05, NA)) + expect_equal(unique(igTable(sim)$escapeProb), 0.05) +}) + +test_that("escape models only see the coarse cells that ignited", { + sim <- toyIgRun(twoFolds()) + escCalls <- toyCalls("esc1") + expect_length(escCalls, 1L) + expect_identical(escCalls[[1]]$pixelID, 2:3) + ## the ignition models see all four + expect_identical(toyCalls("ign1")[[1]]$pixelID, 1:4) + expect_identical(toyCalls("ign2")[[1]]$pixelID, 1:4) +}) + +test_that("only models whose name contains 'Fold' are used", { + ins <- toyIgInputs(list(byPixel(c(0, 2, 3, 0)), byPixel(c(0, 4, 3, 0)), constant(1000)), + list(constant(1)), foldNames = c("Fold1", "Fold2", "full")) + sim <- toyIgRun(ins) + expect_equal(probVals(sim)[, "ignitionProb"], c(0, 3, 3, 0)) # not (…+1000)/3 + expect_length(toyCalls("ign3"), 0L) +}) + +test_that("each ignition is a distinct flammable fine cell inside its own coarse cell", { + for (seed in 1:3) { + sim <- toyIgRun(twoFolds(), seed = seed) + ig <- igTable(sim) + expect_identical(names(ig), c("pixelID", "igProb", "ignitions", "escapeProb", "escapes")) + expect_gt(nrow(ig), 0) + expect_false(anyDuplicated(ig$pixelID) > 0) + coarse <- coarseOfFine(ig$pixelID) + expect_true(all(coarse %in% c(2, 3))) # coarse 1 and 4 have 0 expected ignitions + expect_false(any(ig$pixelID %in% c(5, 6))) # not flammable, though inside coarse 2 + ## one row per ignition: a coarse cell with n ignitions has n rows, each saying n + expect_identical(as.vector(table(coarse)[as.character(coarse)]), as.integer(ig$ignitions)) + expect_equal(ig$igProb, rep(3, nrow(ig))) + } +}) + +test_that("escapes are certain with probability 1 and impossible with probability 0", { + ig <- igTable(toyIgRun(twoFolds(list(constant(1))))) + expect_identical(ig$escapes, ig$ignitions) + expect_true(all(ig$ignitions >= 1L)) + ig <- igTable(toyIgRun(twoFolds(list(constant(0))))) + expect_identical(ig$escapes, rep(0L, nrow(ig))) + expect_gt(nrow(ig), 0) +}) + +test_that("the draws are reproducible under set.seed", { + a <- igTable(toyIgRun(twoFolds(list(constant(0.5))), seed = 7)) + b <- igTable(toyIgRun(twoFolds(list(constant(0.5))), seed = 7)) + expect_identical(a, b) + d <- igTable(toyIgRun(twoFolds(list(constant(0.5))), seed = 8)) + expect_false(identical(a, d)) +}) + +test_that("no expected ignitions anywhere gives no ignitions", { + sim <- toyIgRun(toyIgInputs(list(constant(0)), list(constant(1)))) + expect_identical(nrow(sim$ignitionsAndEscapes), 0L) + expect_equal(probVals(sim)[, "ignitionProb"], c(0, 0, 0, 0)) + expect_true(all(is.na(probVals(sim)[, "escapeProb"]))) + expect_length(toyCalls("esc1")[[1]]$pixelID, 0L) +}) + +test_that("coarse cells with an NA covariate are dropped: no prediction, no ignition", { + covs <- toyIgCovariates() + data.table::set(covs, 2L, "MDC", NA_real_) + ## fold predicts 3 wherever it is asked + sim <- toyIgRun(toyIgInputs(list(constant(3)), list(constant(1)), covs = covs)) + expect_identical(toyCalls("ign1")[[1]]$pixelID, c(1L, 3L, 4L)) + expect_equal(probVals(sim)[, "ignitionProb"], c(3, NA, 3, 3)) + expect_false(any(coarseOfFine(igTable(sim)$pixelID) == 2)) +}) + +test_that("a fully non-flammable coarse cell gets no ignitions even if the model predicts them", { + ## coarse 2 = fine rows 1-4, cols 5-8 + nf <- as.integer(outer(5:8, (0:3) * 8, `+`)) + expect_true(all(coarseOfFine(nf) == 2)) + sim <- toyIgRun(twoFolds(flammableRTM = toyFlammable(nf))) + ig <- igTable(sim) + expect_gt(nrow(ig), 0) + expect_true(all(coarseOfFine(ig$pixelID) == 3)) + ## the probability raster still reports the prediction for coarse 2 + expect_equal(probVals(sim)[, "ignitionProb"], c(0, 3, 3, 0)) +}) + +test_that("the covariate table in the simList is not modified", { + sim <- toyIgRun(twoFolds()) + expect_equal(as.data.frame(sim$fireSense_igAndEscapePred_Covariates), + as.data.frame(toyIgCovariates())) +}) + +test_that("draws with seed 42 match development", { + ## Stochastic, so pinned: values from origin/development at 277672d (R 4.6.1). + ## `ignitions` and `escapes` are the counts of the coarse cell, repeated on each of its rows. + ig <- igTable(toyIgRun(twoFolds(list(constant(0.5))), seed = 42)) + ig <- ig[order(ig$pixelID), ] + expect_identical(ig$pixelID, c(14L, 15L, 21L, 23L, 34L, 36L, 50L, 58L)) + ## cells 14..23 are in coarse 2 (4 ignitions, 2 escapes); 34..58 in coarse 3 (4 and 3) + expect_identical(as.integer(ig$ignitions), rep(4L, 8)) + expect_identical(as.integer(ig$escapes), rep(c(2L, 3L), each = 4)) + expect_equal(ig$escapeProb, rep(0.5, 8)) +}) From e8519de2e5b2b4af545c064cfef9938312ef818b Mon Sep 17 00:00:00 2001 From: Eliot McIntire Date: Sun, 20 Sep 2026 19:52:06 -0700 Subject: [PATCH 06/25] Make the save event empty; remove two values computed but never read The save event wrote sim$fireSense_IgnitionPredicted, which nothing creates, so it failed whenever .saveInitialTime was set. It now only emits a message, and IgnitionPredictSave() is gone. igDisAggFactor and the lambdaRescaleFactor default in IgnitionPredictRun() were computed and never read; the latter also wrote into the input fireSense_IgnitionFitted. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_013B4sgRg9EwyHaAQdDzUaQW --- NEWS.md | 4 ++++ fireSense_IgnitionPredict.R | 37 ++++------------------------------- fireSense_IgnitionPredict.Rmd | 2 +- tests/testthat/test-events.R | 18 ++++++++++++++++- 4 files changed, 26 insertions(+), 35 deletions(-) diff --git a/NEWS.md b/NEWS.md index 6c2d9bc..d85648d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ +# fireSense_IgnitionPredict (development version) + +- The `save` event now does nothing and says so. It used to fail, because it wrote `sim$fireSense_IgnitionPredicted`, which the module no longer creates. `IgnitionPredictSave()` is removed. + # fireSense_IgnitionPredict 1.0.0 First release from `development` since `master` was last updated (2021-03-11). Full history: https://github.com/PredictiveEcology/fireSense_IgnitionPredict/compare/5a242fa...v1.0.0 diff --git a/fireSense_IgnitionPredict.R b/fireSense_IgnitionPredict.R index ff0c1a7..b09f888 100644 --- a/fireSense_IgnitionPredict.R +++ b/fireSense_IgnitionPredict.R @@ -35,11 +35,10 @@ defineModule(sim, list( desc = "Interval between predictions, in years. `NA` predicts once." ), defineParameter(".saveInitialTime", "numeric", NA, NA, NA, - desc = "Time of the first `save` event. `NA` means never." + desc = "Time of the `save` event, which does nothing. `NA` means never." ), defineParameter(".saveInterval", "numeric", NA, NA, NA, - desc = paste("Interval between `save` events.", - "If not `NA`, the ignition probability raster is also plotted each year.") + desc = "If not `NA`, the ignition probability raster is plotted each year." ), defineParameter(".useCache", "logical", FALSE, NA, NA, desc = paste( @@ -85,7 +84,7 @@ defineModule(sim, list( #' Event dispatcher #' -#' Events: `init`, `run` (predict, repeated every `.runInterval`), `save`. +#' Events: `init`, `run` (predict, repeated every `.runInterval`), `save` (does nothing). #' #' @param sim A `simList`. #' @param eventTime Time of the event. @@ -111,11 +110,7 @@ doEvent.fireSense_IgnitionPredict <- function(sim, eventTime, eventType, debug = } }, save = { - sim <- IgnitionPredictSave(sim) - - if (!is.na(P(sim)$.saveInterval)) { - sim <- scheduleEvent(sim, time(sim) + P(sim)$.saveInterval, moduleName, "save", .last()) - } + message("fireSense_IgnitionPredict: the `save` event does nothing.") }, warning(paste("Undefined event type: '", current(sim)[1, "eventType", with = FALSE], "' in module '", current(sim)[1, "moduleName", with = FALSE], "'", @@ -135,11 +130,6 @@ doEvent.fireSense_IgnitionPredict <- function(sim, eventTime, eventType, debug = #' #' @return The `simList`, invisibly, with `fireSense_IgAndEscapeProbRas` and `ignitionsAndEscapes`. IgnitionPredictRun <- function(sim) { - ## checks - if (is.null(sim$fireSense_IgnitionFitted$lambdaRescaleFactor)) { - sim$fireSense_IgnitionFitted$lambdaRescaleFactor <- 1 - } - igCov <- copy(sim$fireSense_igAndEscapePred_Covariates) igCov <- na.omit(igCov) @@ -182,8 +172,6 @@ IgnitionPredictRun <- function(sim) { set(igCov, NULL, c("igProb", "ignitions"), list(predsIgns, igns)) set(igCov, whHasIgns, c("escapeProb", "escapes"), list(predsEscs, escs)) - - igDisAggFactor <- ceiling(sim$fireSense_IgnitionFitted$modelList$fittingRes / c(res(sim$flammableRTM)[1])) } else { stop("Not tested anymore") } @@ -234,23 +222,6 @@ IgnitionPredictRun <- function(sim) { return(invisible(sim)) } -#' Write the predicted raster to `outputPath` -#' -#' @param sim A `simList`. -#' -#' @return The `simList`, invisibly. -IgnitionPredictSave <- function(sim) { - timeUnit <- timeunit(sim) - currentTime <- time(sim, timeUnit) - - writeRaster( - sim$fireSense_IgnitionPredicted, - filename = file.path(paths(sim)$out, paste0("fireSense_IgnitionPredicted_", timeUnit, currentTime, ".tif")) - ) - - return(invisible(sim)) -} - #' Supply default inputs #' #' There are none. diff --git a/fireSense_IgnitionPredict.Rmd b/fireSense_IgnitionPredict.Rmd index a6af4ab..ec76109 100644 --- a/fireSense_IgnitionPredict.Rmd +++ b/fireSense_IgnitionPredict.Rmd @@ -81,7 +81,7 @@ knitr::kable(df_params, caption = "List of (ref:fireSense-IgnitionPredict) param - `init`: schedules `run` at `.runInitialTime`, and `save` at `.saveInitialTime` if that is not `NA`. - `run`: makes the predictions and draws described above; repeats every `.runInterval`. -- `save`: writes a raster to `outputPath(sim)`; repeats every `.saveInterval`. It writes `sim$fireSense_IgnitionPredicted`, which the module no longer creates, so it currently fails. +- `save`: does nothing but say so. To save the predicted raster, name `fireSense_IgAndEscapeProbRas` in `outputs(sim)`. ### Plotting diff --git a/tests/testthat/test-events.R b/tests/testthat/test-events.R index 802d8ec..5d243f3 100644 --- a/tests/testthat/test-events.R +++ b/tests/testthat/test-events.R @@ -27,7 +27,6 @@ test_that("no save event by default; .saveInitialTime schedules one, last in its sim <- toyIgRun(inputsNoIgnition(), times = list(start = 2001, end = 2002)) expect_identical(nrow(evOf(SpaDES.core::completed(sim), "save")), 0L) expect_identical(nrow(evOf(SpaDES.core::events(sim), "save")), 0L) - ## scheduled beyond end(sim) so that the (broken) save event itself never runs sim <- toyIgRun(inputsNoIgnition(), params = list(.saveInitialTime = 2010), times = list(start = 2001, end = 2001)) sv <- evOf(SpaDES.core::events(sim), "save") @@ -35,6 +34,23 @@ test_that("no save event by default; .saveInitialTime schedules one, last in its expect_equal(sv$eventPriority, SpaDES.core::.last()) }) +test_that("the save event says it does nothing, and changes nothing", { + ins <- toyIgInputs(list(byPixel(c(0, 3, 0, 2))), list(constant(0.5))) + without <- toyIgRun(ins, times = list(start = 2001, end = 2002)) + expect_message( + with <- toyIgRun(ins, params = list(.saveInitialTime = 2001, .saveInterval = 1), + times = list(start = 2001, end = 2002)), + "`save` event does nothing") + ## it ran once and did not reschedule itself + expect_equal(evOf(SpaDES.core::completed(with), "save")$eventTime, 2001) + expect_identical(nrow(evOf(SpaDES.core::events(with), "save")), 0L) + expect_identical(sort(ls(with)), sort(ls(without))) + expect_identical(igTable(with), igTable(without)) + expect_identical(probVals(with), probVals(without)) + expect_identical(with$fireSense_IgnitionFitted, without$fireSense_IgnitionFitted) + expect_length(list.files(SpaDES.core::outputPath(with), recursive = TRUE), 0L) +}) + test_that("nothing is predicted before the first run event", { sim <- toyIgRun(inputsNoIgnition(), params = list(.runInitialTime = 2005), times = list(start = 2001, end = 2002)) From f3c7256764974afd6ce08a5e6e915d0de62147ef Mon Sep 17 00:00:00 2001 From: Eliot McIntire Date: Sun, 20 Sep 2026 19:56:51 -0700 Subject: [PATCH 07/25] Standardize prediction covariates with the fit's center and scale IgnitionPredictRun() passed each year's covariates to fireSenseUtils::prepareCovariatesOuter(), which calls scale() on them, so they were standardized with that year's own mean and sd. The models were fitted on covariates standardized once over all fitting years, so a hot year and a cool year reached them looking the same. The fitted objects already carry the fit's constants as `scaleData`; use those, separately for ignition and escape. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_013B4sgRg9EwyHaAQdDzUaQW --- NEWS.md | 6 ++ fireSense_IgnitionPredict.R | 36 +++++--- tests/testthat/setup-toyIgnition.R | 131 +++++++++++++++++++++++++++++ tests/testthat/test-fit-scaling.R | 52 ++++++++++++ 4 files changed, 215 insertions(+), 10 deletions(-) create mode 100644 tests/testthat/setup-toyIgnition.R create mode 100644 tests/testthat/test-fit-scaling.R diff --git a/NEWS.md b/NEWS.md index 6c2d9bc..fb512fe 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,9 @@ +# fireSense_IgnitionPredict (development version) + +## Bug fixes + +- Covariates are now standardized with the center and scale stored by the fit (`fireSense_IgnitionFitted$scaleData`, `fireSense_EscapeFitted$scaleData`). They were standardized with each year's own mean and sd, so every year looked average to the models. With `rescaleVars = TRUE`, a fitted object without `scaleData` is now an error. + # fireSense_IgnitionPredict 1.0.0 First release from `development` since `master` was last updated (2021-03-11). Full history: https://github.com/PredictiveEcology/fireSense_IgnitionPredict/compare/5a242fa...v1.0.0 diff --git a/fireSense_IgnitionPredict.R b/fireSense_IgnitionPredict.R index 610c4c0..a9a8bdb 100644 --- a/fireSense_IgnitionPredict.R +++ b/fireSense_IgnitionPredict.R @@ -134,15 +134,15 @@ IgnitionPredictRun <- function(sim) { igCov <- na.omit(igCov) - modelAlgorithm <- paramCheckOtherMods(sim, "modelAlgorithm") + paramCheckOtherMods(sim, "modelAlgorithm") rescaleVars <- paramCheckOtherMods(sim, "rescaleVars") - data <- prepareCovariatesOuter(igCov, - algorithm = modelAlgorithm, - rescaleVars = rescaleVars, useCache = FALSE) - pixelId <- igCov$pixelID - igCov <- data$covariates - set(igCov, NULL, "pixelID", pixelId) - + # The models were fitted on covariates standardized once, over all fitting years; a year's + # own mean and sd would make every year look average. + igCovIgn <- igCovEsc <- igCov + if (rescaleVars) { + igCovIgn <- scaleAsFit(igCov, sim$fireSense_IgnitionFitted$scaleData, "fireSense_IgnitionFitted") + igCovEsc <- scaleAsFit(igCov, sim$fireSense_EscapeFitted$scaleData, "fireSense_EscapeFitted") + } # scaleData <- sim$fireSense_IgnitionFitted$scaleData # if (!is.null(scaleData)) { @@ -174,7 +174,7 @@ IgnitionPredictRun <- function(sim) { modsOnly <- modsHere[grep("Fold", names(modsHere))] if (!is.null(modsOnly)) { predsIgnList <- Map(model = modsOnly, function(model) { - predict(model, newdata = igCov) + predict(model, newdata = igCovIgn) }) predsIgnsMat <- do.call(cbind, predsIgnList)# |> sort() |> unname() @@ -187,7 +187,7 @@ IgnitionPredictRun <- function(sim) { predsEscList <- Map(model = modsEscOnly, function(model) { - predsEsc <- predict(model, newdata = igCov[whHasIgns]) + predsEsc <- predict(model, newdata = igCovEsc[whHasIgns]) pmax(pmin(1, predsEsc), 0) # the escape model is tweedie, so can go above 1, below 0 rarely }) predsEscsMat <- do.call(cbind, predsEscList)# |> sort() |> unname() @@ -307,3 +307,19 @@ IgnitionPredictSave <- function(sim) { scaleAgain <- function(v, center, scale) { (v - center)/ scale } + +# Standardize `covariates` with the center and scale the fit used (`scaleData`, the attributes of +# the `scale()`d fitting covariates), not with their own. `pixelID` is a cell number, left alone. +scaleAsFit <- function(covariates, scaleData, fittedName) { + if (is.null(scaleData)) { + stop("rescaleVars is TRUE but sim$", fittedName, "$scaleData is NULL; ", + "the covariates cannot be standardized as they were for the fit.") + } + covariates <- copy(covariates) + center <- scaleData[["scaled:center"]] + scale <- scaleData[["scaled:scale"]] + for (v in setdiff(intersect(names(center), colnames(covariates)), "pixelID")) { + set(covariates, NULL, v, (covariates[[v]] - center[[v]]) / scale[[v]]) + } + covariates +} diff --git a/tests/testthat/setup-toyIgnition.R b/tests/testthat/setup-toyIgnition.R new file mode 100644 index 0000000..76dd3d8 --- /dev/null +++ b/tests/testthat/setup-toyIgnition.R @@ -0,0 +1,131 @@ +## Toy inputs for the `run` event. A setup file rather than a helper, so that it shares an +## environment with `moduleName` and `testPaths` from setup.R. +## +## Tests run inside the namespace of the package rendition, which does not import +## data.table, so `dt[i, j]` there is NOT data.table-aware: tables are handled with base +## subsetting and data.table::set() only. +## +## The fitted models are stand-ins of class "toyFold". xgboost is not among the module's +## `reqdPkgs`, so CI does not have it; and a stand-in lets a test see exactly what the +## module hands to predict(). Each "model" is a function of `newdata`; every call is +## logged in `toyLog`. + +## The run event calls postProcess() unqualified, but `reproducible` is not in the module's +## `reqdPkgs`: in a project some other module attaches it. It is always installed (SpaDES.core +## imports it), so attach it here; otherwise the event stops with +## 'could not find function "postProcess"'. +suppressPackageStartupMessages(library(reproducible)) + +toyLog <- new.env() +toyLogReset <- function() rm(list = ls(toyLog), envir = toyLog) + +predict.toyFold <- function(object, newdata, ...) { + key <- sprintf("%s_%03d", object$label, length(ls(toyLog)) + 1L) + assign(key, as.data.frame(newdata), envir = toyLog) + object$fun(as.data.frame(newdata)) +} +registerS3method("predict", "toyFold", predict.toyFold) + +toyFold <- function(fun, label) structure(list(fun = fun, label = label), class = "toyFold") + +## calls logged for one model label, in call order +toyCalls <- function(label) { + keys <- sort(grep(paste0("^", label, "_"), ls(toyLog), value = TRUE)) + lapply(keys, get, envir = toyLog) +} + +## Grids, both 0..4 x 0..4. The fine (simulation) grid is 8 x 8 with 0.5-unit cells; the +## coarse (fitting) grid is 2 x 2 with 2-unit cells, so each coarse cell holds 16 fine cells: +## coarse 1 = fine rows 1-4, cols 1-4; coarse 2 = rows 1-4, cols 5-8; +## coarse 3 = rows 5-8, cols 1-4; coarse 4 = rows 5-8, cols 5-8. +## Fine cell number = (row - 1) * 8 + col. +coarseOfFine <- function(cell) { + row <- (cell - 1) %/% 8 + 1 + col <- (cell - 1) %% 8 + 1 + (row > 4) * 2 + (col > 4) + 1 +} + +## By default fine cells 5 and 6 (in coarse 2) and 64 (in coarse 4) are not flammable. +toyFlammable <- function(nonFlammable = c(5L, 6L, 64L)) { + r <- terra::rast(nrows = 8, ncols = 8, xmin = 0, xmax = 4, ymin = 0, ymax = 4, vals = 1, + crs = "EPSG:3005") + r[nonFlammable] <- 0 + r +} + +toyCoarse <- function() { + terra::rast(nrows = 2, ncols = 2, xmin = 0, xmax = 4, ymin = 0, ymax = 4, vals = 1, + crs = "EPSG:3005") +} + +## one row per coarse cell, as fireSense_dataPrepPredict makes them +toyIgCovariates <- function(MDC = c(100, 120, 140, 160), year = 2001) { + data.table::data.table(pixelID = 1:4, MDC = MDC, youngAge = c(0, 0.25, 0.5, 1), + year = year) +} + +## The centre and scale the fit standardised its covariates with, as fireSense_IgnitionFit +## stores them (the attributes of a scale()d matrix; `pixelID` and `year` are in there too). +toyScaleData <- function(center = c(pixelID = 2.5, MDC = 100, youngAge = 0.5, year = 2000), + scale = c(pixelID = 1.3, MDC = 50, youngAge = 0.5, year = 10)) { + list(`scaled:center` = center, `scaled:scale` = scale) +} + +## `ignFuns` and `escFuns` are lists of functions of newdata, one per fold +toyIgInputs <- function(ignFuns, escFuns, covs = toyIgCovariates(), + foldNames = paste0("Fold", seq_along(ignFuns)), + flammableRTM = toyFlammable(), + ignScaleData = toyScaleData(), escScaleData = toyScaleData()) { + ign <- Map(toyFold, ignFuns, paste0("ign", seq_along(ignFuns))) + esc <- Map(toyFold, escFuns, paste0("esc", seq_along(escFuns))) + names(ign) <- foldNames + names(esc) <- paste0("Fold", seq_along(escFuns)) + list( + fireSense_IgnitionFitted = structure( + list(modelList = list(model = ign, fittingRes = 2), scaleData = ignScaleData), + class = "fireSense_IgnitionFit"), + fireSense_EscapeFitted = structure( + list(modelList = list(model = esc), scaleData = escScaleData), + class = "fireSense_EscapeFit"), + fireSense_igAndEscapePred_Covariates = covs, + flammableRTM = flammableRTM, + ignitionFitRTM = toyCoarse() + ) +} + +toyIgSim <- function(objects, params = list(), times = list(start = 2001, end = 2001)) { + SpaDES.core::simInit( + times = c(times, timeunit = "year"), + modules = moduleName, + params = stats::setNames(list(params), moduleName), + objects = objects, + paths = testPaths + ) +} + +## fireSenseUtils::rescaleCovariates() warns on every call that there is no `ignitions` +## column to remove; that is noise here, so muffle that one warning only. +toyIgRun <- function(objects, ..., seed = 42) { + toyLogReset() + sim <- toyIgSim(objects, ...) + set.seed(seed) + withCallingHandlers( + SpaDES.core::spades(sim, debug = FALSE), + warning = function(w) { + if (grepl("Tried to assign NULL to column 'ignitions'", conditionMessage(w))) + invokeRestart("muffleWarning") + }) +} + +igTable <- function(sim) as.data.frame(sim$ignitionsAndEscapes) +probVals <- function(sim) terra::values(sim$fireSense_IgAndEscapeProbRas) + +## a constant prediction, whatever the covariates +constant <- function(x) function(nd) rep(x, nrow(nd)) +## a prediction that depends on which coarse pixel it is: vals[pixelID] +byPixel <- function(vals) function(nd) vals[nd$pixelID] + +evOf <- function(dt, type) { + df <- as.data.frame(dt) + df[df$moduleName == "fireSense_IgnitionPredict" & df$eventType == type, , drop = FALSE] +} diff --git a/tests/testthat/test-fit-scaling.R b/tests/testthat/test-fit-scaling.R new file mode 100644 index 0000000..3a7a977 --- /dev/null +++ b/tests/testthat/test-fit-scaling.R @@ -0,0 +1,52 @@ +## The models were fitted on covariates standardised once, over all fitting years. Prediction +## must use that same centre and scale, which fireSense_IgnitionFit stores as `scaleData`. + +oneFold <- function(covs, ...) toyIgInputs(list(constant(1)), list(constant(0)), covs = covs, ...) + +test_that("covariates are standardised with the FIT's centre and scale, whatever the year's own mean", { + ## fit: MDC centre 100, scale 50. A cool year, and one 100 MDC units hotter everywhere: + ## cool: (c(100, 120, 140, 160) - 100) / 50 = 0, 0.4, 0.8, 1.2 + ## hot: (c(200, 220, 240, 260) - 100) / 50 = 2, 2.4, 2.8, 3.2 + ## Standardised with each year's own mean and sd, both would be -1.162, -0.387, 0.387, 1.162. + seen <- lapply(list(cool = 0, hot = 100), function(add) { + toyIgRun(oneFold(toyIgCovariates(MDC = c(100, 120, 140, 160) + add))) + toyCalls("ign1")[[1]] + }) + expect_equal(seen$cool$MDC, c(0, 0.4, 0.8, 1.2)) + expect_equal(seen$hot$MDC, c(2, 2.4, 2.8, 3.2)) + expect_equal(seen$cool$youngAge, c(-1, -0.5, 0, 1)) + expect_equal(seen$cool$year, rep(0.1, 4)) + ## pixelID is in the fit's scaleData too, but it is the cell number + expect_identical(seen$cool$pixelID, 1:4) +}) + +test_that("an integer covariate is standardised, not truncated", { + covs <- toyIgCovariates() + covs$year <- 2001L + toyIgRun(oneFold(covs)) + expect_equal(toyCalls("ign1")[[1]]$year, rep(0.1, 4)) +}) + +test_that("the escape models get the escape fit's centre and scale", { + esc <- toyScaleData(center = c(MDC = 120), scale = c(MDC = 20)) + toyIgRun(oneFold(toyIgCovariates(), escScaleData = esc)) + ## every coarse pixel ignites (lambda = 1 with seed 42 gives zeros too, so match on pixelID) + ign <- toyCalls("ign1")[[1]] + escSeen <- toyCalls("esc1")[[1]] + expect_gt(nrow(escSeen), 0) + expect_equal(ign$MDC, c(0, 0.4, 0.8, 1.2)) + expect_equal(escSeen$MDC, (c(100, 120, 140, 160)[escSeen$pixelID] - 120) / 20) + ## not in the escape fit's scaleData, so left alone + expect_equal(escSeen$youngAge, c(0, 0.25, 0.5, 1)[escSeen$pixelID]) +}) + +test_that("with rescaleVars = FALSE the models get the covariates exactly as supplied", { + toyIgRun(oneFold(toyIgCovariates()), params = list(rescaleVars = FALSE)) + nd <- toyCalls("ign1")[[1]] + expect_identical(nd$MDC, c(100, 120, 140, 160)) + expect_identical(nd$youngAge, c(0, 0.25, 0.5, 1)) +}) + +test_that("rescaleVars = TRUE with a fit that carries no scaleData is an error", { + expect_error(toyIgRun(oneFold(toyIgCovariates(), ignScaleData = NULL)), "scaleData") +}) From 495f045c5ecd6b7bc40dc95923a8526ccf3797b3 Mon Sep 17 00:00:00 2001 From: Eliot McIntire Date: Sun, 20 Sep 2026 19:58:04 -0700 Subject: [PATCH 08/25] Give the toy fitted objects scaleData, as the real ones have Keeps setup-toyIgnition.R identical to the copy in the scaling fix (PR #21), so the two merge in either order and these tests still pass once prediction needs scaleData. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_013B4sgRg9EwyHaAQdDzUaQW --- tests/testthat/setup-toyIgnition.R | 16 +++++++++++++--- tests/testthat/test-covariate-scaling.R | 4 +++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/testthat/setup-toyIgnition.R b/tests/testthat/setup-toyIgnition.R index 9277287..76dd3d8 100644 --- a/tests/testthat/setup-toyIgnition.R +++ b/tests/testthat/setup-toyIgnition.R @@ -64,19 +64,29 @@ toyIgCovariates <- function(MDC = c(100, 120, 140, 160), year = 2001) { year = year) } +## The centre and scale the fit standardised its covariates with, as fireSense_IgnitionFit +## stores them (the attributes of a scale()d matrix; `pixelID` and `year` are in there too). +toyScaleData <- function(center = c(pixelID = 2.5, MDC = 100, youngAge = 0.5, year = 2000), + scale = c(pixelID = 1.3, MDC = 50, youngAge = 0.5, year = 10)) { + list(`scaled:center` = center, `scaled:scale` = scale) +} + ## `ignFuns` and `escFuns` are lists of functions of newdata, one per fold toyIgInputs <- function(ignFuns, escFuns, covs = toyIgCovariates(), foldNames = paste0("Fold", seq_along(ignFuns)), - flammableRTM = toyFlammable()) { + flammableRTM = toyFlammable(), + ignScaleData = toyScaleData(), escScaleData = toyScaleData()) { ign <- Map(toyFold, ignFuns, paste0("ign", seq_along(ignFuns))) esc <- Map(toyFold, escFuns, paste0("esc", seq_along(escFuns))) names(ign) <- foldNames names(esc) <- paste0("Fold", seq_along(escFuns)) list( fireSense_IgnitionFitted = structure( - list(modelList = list(model = ign, fittingRes = 2)), class = "fireSense_IgnitionFit"), + list(modelList = list(model = ign, fittingRes = 2), scaleData = ignScaleData), + class = "fireSense_IgnitionFit"), fireSense_EscapeFitted = structure( - list(modelList = list(model = esc)), class = "fireSense_EscapeFit"), + list(modelList = list(model = esc), scaleData = escScaleData), + class = "fireSense_EscapeFit"), fireSense_igAndEscapePred_Covariates = covs, flammableRTM = flammableRTM, ignitionFitRTM = toyCoarse() diff --git a/tests/testthat/test-covariate-scaling.R b/tests/testthat/test-covariate-scaling.R index b068ba0..7f3cd14 100644 --- a/tests/testthat/test-covariate-scaling.R +++ b/tests/testthat/test-covariate-scaling.R @@ -21,7 +21,9 @@ test_that("pixelID reaches the models as the cell number, never rescaled", { test_that("covariates are standardised with the FIT's centre and scale", { skip(paste("known defect: IgnitionPredictRun() standardises each year's covariates with that", "year's own mean and sd (fireSenseUtils::prepareCovariatesOuter -> scale()),", - "not with fireSense_IgnitionFitted$scaleData")) + "not with fireSense_IgnitionFitted$scaleData.", + "Fixed, with its own tests, in", + "https://github.com/PredictiveEcology/fireSense_IgnitionPredict/pull/21")) ## The fit standardised MDC with centre 100 and scale 50, and stored that in `scaleData` ## (as fireSense_IgnitionFit does). A cool year and a year that is 100 MDC units hotter ## everywhere must therefore look different to the model: From c5ab180de71145ec26126a7cd67ac17aed8248b7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 03:27:54 +0000 Subject: [PATCH 09/25] Re-build fireSense_IgnitionPredict.Rmd --- fireSense_IgnitionPredict.html | 2 +- fireSense_IgnitionPredict.md | 87 ++++++++++++++++++++-------------- 2 files changed, 52 insertions(+), 37 deletions(-) diff --git a/fireSense_IgnitionPredict.html b/fireSense_IgnitionPredict.html index 42c96f8..3dc4dac 100644 --- a/fireSense_IgnitionPredict.html +++ b/fireSense_IgnitionPredict.html @@ -2995,7 +2995,7 @@

fireSense_IgnitionPredict Manual

v.1.0.0.9000

-

Last updated: 2026-09-11

+

Last updated: 2026-09-21

diff --git a/fireSense_IgnitionPredict.md b/fireSense_IgnitionPredict.md index c693046..d909f64 100644 --- a/fireSense_IgnitionPredict.md +++ b/fireSense_IgnitionPredict.md @@ -1,7 +1,7 @@ --- title: "fireSense_IgnitionPredict Manual" -subtitle: "v.0.2.0" -date: "Last updated: 2025-04-08" +subtitle: "v.1.0.0.9000" +date: "Last updated: 2026-09-21" output: bookdown::html_document2: toc: true @@ -30,7 +30,7 @@ always_allow_html: true #### Authors: -Jean Marchal [aut], Eliot McIntire [aut], Ian Eddy [aut, cre], Alex M Chubaty [ctb] +Eliot McIntire [aut, cre], Ian Eddy [aut], Jean Marchal [aut], Alex M Chubaty [ctb] ## Module Overview @@ -44,7 +44,7 @@ Use them to feed the ignition component of a landscape fire model (e.g fireSense Table \@ref(tab:moduleInputs-fireSense-IgnitionPredict) shows the full list of module inputs. - +
@@ -56,9 +56,27 @@ Table \@ref(tab:moduleInputs-fireSense-IgnitionPredict) shows the full list of m + + + + + + + + + + + + + + + + + + @@ -67,7 +85,7 @@ Table \@ref(tab:moduleInputs-fireSense-IgnitionPredict) shows the full list of m Summary of user-visible parameters (Table \@ref(tab:moduleParams-fireSense-IgnitionPredict)) -
(\#tab:moduleInputs-fireSense-IgnitionPredict)(\#tab:moduleInputs-fireSense-IgnitionPredict)List of (ref:fireSense-IgnitionPredict) input objects and their description.
fireSense_EscapeFitted fireSense_EscapeFit An object of class `fireSense_EscapeFit` created with the `fireSense_IgnitionFit` module. NA
fireSense_IgnitionFitted fireSense_IgnitionFit An object of class `fireSense_IgnitionFit` created with the `fireSense_IgnitionFit` module. NA
fireSense_igAndEscapePred_Covariates data.table A `data.table` with prediction variables and a column named 'pixelID' NA
flammableRTM SpatRaster RTM without ice/rocks/urban/water. Flammable map with 0 and 1. NA
+
@@ -81,60 +99,52 @@ Summary of user-visible parameters (Table \@ref(tab:moduleParams-fireSense-Ignit - + - + - + - - - + + + - + - + + - - + - + + - - + - + - + - - + + - - - - - - - - - + @@ -142,7 +152,7 @@ Summary of user-visible parameters (Table \@ref(tab:moduleParams-fireSense-Ignit - +
(\#tab:moduleParams-fireSense-IgnitionPredict)(\#tab:moduleParams-fireSense-IgnitionPredict)List of (ref:fireSense-IgnitionPredict) parameters and their description.
.plots modelAlgorithm character screen xgboost NA NA Used by Plots function, which can be optionally used here Can be `xgboost`, `glmmtmb`, `glm.nb`, `glmmadaptive`, `glm`; only `xgboost` is supported currently
.plotInitialTime numeric 0 rescaleVars logical TRUE NA NA Describes the simulation time at which the first plot event should occur. Attempt to rescale variables? If `rescalers` is defined, use it to rescale variables as `var / rescalers['var']`. Otherwise, `scale()` will be used to rescale variables to `[0,1]`, if they are not already within this range.
.plotInterval .runInitialTime numeric 0 NA NA NA Describes the simulation time interval between plot events. when to start this module? By default, the start time of the simulation.
.saveInitialTime .runInterval numeric 1 NA NA NA Describes the simulation time at which the first save event should occur. optional. Interval between two runs of this moduleexpressed in units of simulation time. By default, 1 year.
.saveInterval .saveInitialTime numeric NA NA NA This describes the simulation time interval between save events. optional. When to start saving output to a file.
.studyAreaName character .saveInterval numeric NA NA NA Human-readable name for the study area used - e.g., a hash of the studyarea obtained using `reproducible::studyAreaName()`
.seed list NA NA Named list of seeds to use for each event (names). optional. Interval between save events.
.useCache FALSE NA NA Should caching of events or module be used? Should this entire module be run with caching activated? This is generally intended for data-type modules, where stochasticity and time are not relevant
@@ -164,7 +174,7 @@ Write what is saved. Description of the module outputs (Table \@ref(tab:moduleOutputs-fireSense-IgnitionPredict)). - +
@@ -175,9 +185,14 @@ Description of the module outputs (Table \@ref(tab:moduleOutputs-fireSense-Ignit - - - + + + + + + + +
(\#tab:moduleOutputs-fireSense-IgnitionPredict)(\#tab:moduleOutputs-fireSense-IgnitionPredict)List of (ref:fireSense-IgnitionPredict) outputs and their description.
NA NA NA fireSense_IgAndEscapeProbRas SpatRaster a raster layer of the annual ignition and escape probabilities
ignitionsAndEscapes data.table A data.table containing pixelID (referencing flammableRTM), ignitions, escapes, and their associated probabilities
From e4cb0a3c3edd4ed4a91312ff5b29b6153309071a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 04:08:31 +0000 Subject: [PATCH 10/25] Re-build fireSense_IgnitionPredict.Rmd --- fireSense_IgnitionPredict.html | 54 ++++++++++++++++++---------------- fireSense_IgnitionPredict.md | 54 +++++++++++++++++++--------------- 2 files changed, 58 insertions(+), 50 deletions(-) diff --git a/fireSense_IgnitionPredict.html b/fireSense_IgnitionPredict.html index 3dc4dac..4483e72 100644 --- a/fireSense_IgnitionPredict.html +++ b/fireSense_IgnitionPredict.html @@ -3015,11 +3015,19 @@

Authors:

Module Overview

Module summary

-

Predicts fire frequency or rates of fire counts using a model fitted with the fireSense_IgnitionFit module. -Use them to feed the ignition component of a landscape fire model (e.g fireSense (Marchal, Steve G. Cumming, et al. 2017b; Marchal, Steve G. Cumming, et al. 2017a; Marchal et al. 2019)).

+

Each year, predicts ignitions and escapes from the models fitted by fireSense_IgnitionFit and fireSense_EscapeFit, for the ignition component of fireSense (Marchal, Steve G. Cumming, et al. 2017b; Marchal, Steve G. Cumming, et al. 2017a; Marchal et al. 2019).

+
    +
  1. The covariates in fireSense_igAndEscapePred_Covariates are rescaled with fireSenseUtils::prepareCovariatesOuter(), the function used for fitting (rescaleVars, modelAlgorithm).
  2. +
  3. Expected ignitions per coarse pixel are the mean of the predictions of the per-fold ignition models; the number of ignitions is drawn from a Poisson.
  4. +
  5. For coarse pixels with ignitions, the escape probability is the mean of the per-fold escape models, clamped to [0, 1]; escapes are drawn from a binomial with size = ignitions.
  6. +
  7. Each ignition is placed in a randomly chosen flammable pixel of flammableRTM inside its coarse pixel.
  8. +
+

Only xgboost models are supported.

Module inputs and parameters

+

ignitionFitRTM (the coarse raster used for fitting, from fireSense_dataPrepFit) is also read from the simList, though it is not declared as an input. +modelAlgorithm and rescaleVars must have the same value in every module that defines them.

Table 1 shows the full list of module inputs.

@@ -3165,7 +3173,7 @@

Module inputs and parameters

NA @@ -3185,7 +3193,7 @@

Module inputs and parameters

NA @@ -3205,7 +3213,7 @@

Module inputs and parameters

NA @@ -3225,7 +3233,7 @@

Module inputs and parameters

NA @@ -3245,7 +3253,7 @@

Module inputs and parameters

NA @@ -3273,21 +3281,15 @@

Module inputs and parameters

Events

-
    -
  • Module initialization;
  • -
  • Make predictions;
  • +
  • init: schedules run at .runInitialTime, and save at .saveInitialTime if that is not NA.
  • +
  • run: makes the predictions and draws described above; repeats every .runInterval.
  • +
  • save: does nothing but say so. To save the predicted raster, name fireSense_IgAndEscapeProbRas in outputs(sim).

Plotting

- -

Write what is plotted.

-
-
-

Saving

- -

Write what is saved.

+

If .saveInterval is not NA, the ignition probability raster is plotted each year with Plots().

Module outputs

@@ -3318,7 +3320,7 @@

Module outputs

SpatRaster
@@ -3329,7 +3331,7 @@

Module outputs

data.table @@ -3337,8 +3339,8 @@

Module outputs

Getting help

diff --git a/fireSense_IgnitionPredict.md b/fireSense_IgnitionPredict.md index d909f64..9189f90 100644 --- a/fireSense_IgnitionPredict.md +++ b/fireSense_IgnitionPredict.md @@ -37,11 +37,20 @@ Eliot McIntire [aut, cre], Ian Eddy @@ -58,25 +67,25 @@ Table \@ref(tab:moduleInputs-fireSense-IgnitionPredict) shows the full list of m
- + - + - + - + @@ -104,7 +113,7 @@ Summary of user-visible parameters (Table \@ref(tab:moduleParams-fireSense-Ignit - + @@ -112,7 +121,7 @@ Summary of user-visible parameters (Table \@ref(tab:moduleParams-fireSense-Ignit - + @@ -120,7 +129,7 @@ Summary of user-visible parameters (Table \@ref(tab:moduleParams-fireSense-Ignit - + @@ -128,7 +137,7 @@ Summary of user-visible parameters (Table \@ref(tab:moduleParams-fireSense-Ignit - + @@ -136,7 +145,7 @@ Summary of user-visible parameters (Table \@ref(tab:moduleParams-fireSense-Ignit - + @@ -144,7 +153,7 @@ Summary of user-visible parameters (Table \@ref(tab:moduleParams-fireSense-Ignit - + @@ -158,17 +167,14 @@ Summary of user-visible parameters (Table \@ref(tab:moduleParams-fireSense-Ignit
@@ -3050,7 +3058,7 @@

Module inputs and parameters

fireSense_EscapeFit
-An object of class fireSense_EscapeFit created with the fireSense_IgnitionFit module. +Fitted escape models ($modelList$model, one per fold), from fireSense_EscapeFit. NA @@ -3064,7 +3072,7 @@

Module inputs and parameters

fireSense_IgnitionFit
-An object of class fireSense_IgnitionFit created with the fireSense_IgnitionFit module. +Fitted ignition models ($modelList$model, one per fold) and $modelList$fittingRes, from fireSense_IgnitionFit. NA @@ -3078,7 +3086,7 @@

Module inputs and parameters

data.table
-A data.table with prediction variables and a column named ‘pixelID’ +This year’s covariates, from fireSense_dataPrepPredict. pixelID is the cell index of ignitionFitRTM. NA @@ -3092,7 +3100,7 @@

Module inputs and parameters

SpatRaster
-RTM without ice/rocks/urban/water. Flammable map with 0 and 1. +Binary raster, 1 where the pixel is flammable. NA @@ -3145,7 +3153,7 @@

Module inputs and parameters

NA
-Can be xgboost, glmmtmb, glm.nb, glmmadaptive, glm; only xgboost is supported currently +Algorithm used to fit the models; only xgboost is supported. Must agree with the value in the other fireSense modules.
-Attempt to rescale variables? If rescalers is defined, use it to rescale variables as var / rescalers['var']. Otherwise, scale() will be used to rescale variables to [0,1], if they are not already within this range. +Rescale the covariates before predicting? With xgboost they are standardized with scale(). Must agree with the value in the other fireSense modules.
-when to start this module? By default, the start time of the simulation. +Time of the first prediction.
-optional. Interval between two runs of this moduleexpressed in units of simulation time. By default, 1 year. +Interval between predictions, in years. NA predicts once.
-optional. When to start saving output to a file. +Time of the save event, which does nothing. NA means never.
-optional. Interval between save events. +If not NA, the ignition probability raster is plotted each year.
-a raster layer of the annual ignition and escape probabilities +Two layers, ignitionProb (expected ignitions per pixel) and escapeProb, at the resolution of ignitionFitRTM.
-A data.table containing pixelID (referencing flammableRTM), ignitions, escapes, and their associated probabilities +One row per ignited pixel, in random order: pixelID (cell index of flammableRTM), and igProb, ignitions, escapeProb, escapes of the coarse pixel it was drawn from.
fireSense_EscapeFitted fireSense_EscapeFit An object of class `fireSense_EscapeFit` created with the `fireSense_IgnitionFit` module. Fitted escape models (`$modelList$model`, one per fold), from `fireSense_EscapeFit`. NA
fireSense_IgnitionFitted fireSense_IgnitionFit An object of class `fireSense_IgnitionFit` created with the `fireSense_IgnitionFit` module. Fitted ignition models (`$modelList$model`, one per fold) and `$modelList$fittingRes`, from `fireSense_IgnitionFit`. NA
fireSense_igAndEscapePred_Covariates data.table A `data.table` with prediction variables and a column named 'pixelID' This year's covariates, from `fireSense_dataPrepPredict`. `pixelID` is the cell index of `ignitionFitRTM`. NA
flammableRTM SpatRaster RTM without ice/rocks/urban/water. Flammable map with 0 and 1. Binary raster, 1 where the pixel is flammable. NA
xgboost NA NA Can be `xgboost`, `glmmtmb`, `glm.nb`, `glmmadaptive`, `glm`; only `xgboost` is supported currently Algorithm used to fit the models; only `xgboost` is supported. Must agree with the value in the other fireSense modules.
rescaleVars TRUE NA NA Attempt to rescale variables? If `rescalers` is defined, use it to rescale variables as `var / rescalers['var']`. Otherwise, `scale()` will be used to rescale variables to `[0,1]`, if they are not already within this range. Rescale the covariates before predicting? With `xgboost` they are standardized with `scale()`. Must agree with the value in the other fireSense modules.
.runInitialTime 0 NA NA when to start this module? By default, the start time of the simulation. Time of the first prediction.
.runInterval 1 NA NA optional. Interval between two runs of this moduleexpressed in units of simulation time. By default, 1 year. Interval between predictions, in years. `NA` predicts once.
.saveInitialTime NA NA NA optional. When to start saving output to a file. Time of the `save` event, which does nothing. `NA` means never.
.saveInterval NA NA NA optional. Interval between save events. If not `NA`, the ignition probability raster is plotted each year.
.useCache
### Events - -- Module initialization; -- Make predictions; + +- `init`: schedules `run` at `.runInitialTime`, and `save` at `.saveInitialTime` if that is not `NA`. +- `run`: makes the predictions and draws described above; repeats every `.runInterval`. +- `save`: does nothing but say so. To save the predicted raster, name `fireSense_IgAndEscapeProbRas` in `outputs(sim)`. ### Plotting - -Write what is plotted. -### Saving - -Write what is saved. +If `.saveInterval` is not `NA`, the ignition probability raster is plotted each year with `Plots()`. ### Module outputs @@ -187,20 +193,20 @@ Description of the module outputs (Table \@ref(tab:moduleOutputs-fireSense-Ignit fireSense_IgAndEscapeProbRas SpatRaster - a raster layer of the annual ignition and escape probabilities + Two layers, `ignitionProb` (expected ignitions per pixel) and `escapeProb`, at the resolution of `ignitionFitRTM`. ignitionsAndEscapes data.table - A data.table containing pixelID (referencing flammableRTM), ignitions, escapes, and their associated probabilities + One row per ignited pixel, in random order: `pixelID` (cell index of `flammableRTM`), and `igProb`, `ignitions`, `escapeProb`, `escapes` of the coarse pixel it was drawn from. ### Links to other modules - -Predictions made with this module can be used to feed the ignition component of a landscape fire model (e.g fireSense). +Runs after *fireSense_dataPrepPredict*, which supplies the covariates. `ignitionsAndEscapes` is used by *fireSense* to start fires. +It is normally run as part of the [fireSense](https://github.com/PredictiveEcology/fireSense) module group. ### Getting help From 204a5f02d7527be62a65c2e883984b349afad5f4 Mon Sep 17 00:00:00 2001 From: Eliot McIntire Date: Wed, 23 Sep 2026 08:36:19 -0700 Subject: [PATCH 11/25] Predict ignitions and escapes with each ELF's own models With per-ELF fit lists and the per-pixel ELF raster, each ELF's ignition and escape models predict its own coarse pixels. The draws are still made once over all pixels, so one ELF gives exactly the old result. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01CwcjqqK59FmTJscyi7xUqv --- NEWS.md | 4 ++ fireSense_IgnitionPredict.R | 116 +++++++++++++++++++++++---------- tests/testthat/test-metadata.R | 5 +- tests/testthat/test-multiELF.R | 42 ++++++++++++ 4 files changed, 132 insertions(+), 35 deletions(-) create mode 100644 tests/testthat/test-multiELF.R diff --git a/NEWS.md b/NEWS.md index 224dfe3..0ad92e5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # fireSense_IgnitionPredict (development version) +- Several fitted ELFs in one study area: with `fireSense_IgnitionFittedList` and `fireSense_EscapeFittedList` (one fit per ELF, named by `ELFind`) and `rasterToMatchLargeELF`, each ELF's models predict the coarse pixels of that ELF, with its own `scaleData`. The Poisson and binomial draws are still made once over all pixels, so one ELF gives exactly the previous result. + +# fireSense_IgnitionPredict (development version) + ## Bug fixes - Covariates are now standardized with the center and scale stored by the fit (`fireSense_IgnitionFitted$scaleData`, `fireSense_EscapeFitted$scaleData`). They were standardized with each year's own mean and sd, so every year looked average to the models. With `rescaleVars = TRUE`, a fitted object without `scaleData` is now an error. diff --git a/fireSense_IgnitionPredict.R b/fireSense_IgnitionPredict.R index 66b618b..0167ac7 100644 --- a/fireSense_IgnitionPredict.R +++ b/fireSense_IgnitionPredict.R @@ -11,7 +11,7 @@ defineModule(sim, list( person("Alex M", "Chubaty", email = "achubaty@for-cast.ca", role = "ctb") ), childModules = character(), - version = list(SpaDES.core = "0.1.0", fireSense_IgnitionPredict = "1.0.0.9000"), + version = list(SpaDES.core = "0.1.0", fireSense_IgnitionPredict = "1.0.0.9001"), timeframe = as.POSIXlt(c(NA, NA)), timeunit = "year", citation = list("citation.bib"), @@ -53,6 +53,13 @@ defineModule(sim, list( desc = "Fitted escape models (`$modelList$model`, one per fold), from `fireSense_EscapeFit`.", sourceURL = NA ), + expectsInput("fireSense_IgnitionFittedList", "list", + desc = paste("Only with several fitted ELFs: one `fireSense_IgnitionFitted` per ELF, named by `ELFind`.", + "Each ELF's model predicts the coarse pixels of that ELF.")), + expectsInput("fireSense_EscapeFittedList", "list", + desc = "Only with several fitted ELFs: one `fireSense_EscapeFitted` per ELF, named as `fireSense_IgnitionFittedList`."), + expectsInput("rasterToMatchLargeELF", "SpatRaster", + desc = "Only with several fitted ELFs: each pixel's ELF (`ELFind`), from `fireSense_ELFs` with a `studyAreaLarge`."), expectsInput("fireSense_IgnitionFitted", "fireSense_IgnitionFit", desc = paste("Fitted ignition models (`$modelList$model`, one per fold) and `$modelList$fittingRes`,", "from `fireSense_IgnitionFit`."), @@ -138,43 +145,37 @@ IgnitionPredictRun <- function(sim) { rescaleVars <- paramCheckOtherMods(sim, "rescaleVars") # The models were fitted on covariates standardized once, over all fitting years; a year's # own mean and sd would make every year look average. - igCovIgn <- igCovEsc <- igCov - if (rescaleVars) { - igCovIgn <- scaleAsFit(igCov, sim$fireSense_IgnitionFitted$scaleData, "fireSense_IgnitionFitted") - igCovEsc <- scaleAsFit(igCov, sim$fireSense_EscapeFitted$scaleData, "fireSense_EscapeFitted") - } + ## The fitted models: one ignition and one escape fit, or one of each per fitted ELF (named by ELF), each + ## applied to the coarse pixels of its own ELF (sim$rasterToMatchLargeELF). The Poisson and binomial draws + ## are made once over all pixels, as before, so one ELF gives exactly the old result. + fits <- ignitionFitsByELF(sim, igCov$pixelID) # From here, it makes predictions from each KFold, then averages them to get the probabilities for # each cell; same for Escape, which is conditional on having ignited. - modsHere <- sim$fireSense_IgnitionFitted$modelList$model - modsOnly <- modsHere[grep("Fold", names(modsHere))] - if (!is.null(modsOnly)) { - predsIgnList <- Map(model = modsOnly, function(model) { - predict(model, newdata = igCovIgn) - }) - - predsIgnsMat <- do.call(cbind, predsIgnList) - predsIgns <- rowMeans(predsIgnsMat) - igns <- rpois(NROW(predsIgns), lambda = predsIgns) # can't use rtweedie because don't know the dispersion parameter - - modsEscHere <- sim$fireSense_EscapeFitted$modelList$model - modsEscOnly <- modsEscHere[grep("Fold", names(modsEscHere))] - whHasIgns <- which(igns > 0) - - predsEscList <- Map(model = modsEscOnly, - function(model) { - predsEsc <- predict(model, newdata = igCovEsc[whHasIgns]) - pmax(pmin(1, predsEsc), 0) # the escape model is tweedie, so can go above 1, below 0 rarely - }) - predsEscsMat <- do.call(cbind, predsEscList) - predsEscs <- rowMeans(predsEscsMat) - escs <- rbinom(NROW(predsEscs), size = igns[whHasIgns], prob = predsEscs) - - set(igCov, NULL, c("igProb", "ignitions"), list(predsIgns, igns)) - set(igCov, whHasIgns, c("escapeProb", "escapes"), list(predsEscs, escs)) - } else { - stop("Not tested anymore") + predsIgns <- rep(NA_real_, NROW(igCov)) + for (f in fits) { + covsHere <- igCov[f$rows] + # The models were fitted on covariates standardized once, over all fitting years; a year's + # own mean and sd would make every year look average. + if (rescaleVars) covsHere <- scaleAsFit(covsHere, f$ign$scaleData, "fireSense_IgnitionFitted") + predsIgns[f$rows] <- foldMeanPrediction(f$ign, covsHere) + } + igns <- rpois(NROW(predsIgns), lambda = predsIgns) # can't use rtweedie because don't know the dispersion parameter + + whHasIgns <- which(igns > 0) + predsEscs <- rep(NA_real_, NROW(igCov)) + for (f in fits) { + rowsEsc <- intersect(whHasIgns, f$rows) + covsHere <- igCov[rowsEsc] + if (rescaleVars) covsHere <- scaleAsFit(covsHere, f$esc$scaleData, "fireSense_EscapeFitted") + # the escape model is tweedie, so can go above 1, below 0 rarely + predsEscs[rowsEsc] <- foldMeanPrediction(f$esc, covsHere, clamp01 = TRUE) } + predsEscs <- predsEscs[whHasIgns] + escs <- rbinom(NROW(predsEscs), size = igns[whHasIgns], prob = predsEscs) + + set(igCov, NULL, c("igProb", "ignitions"), list(predsIgns, igns)) + set(igCov, whHasIgns, c("escapeProb", "escapes"), list(predsEscs, escs)) # Ignite - accounting for spatial resolution of models # disaggregate the coarse raster to size of flammableRTM @@ -249,3 +250,50 @@ scaleAsFit <- function(covariates, scaleData, fittedName) { } covariates } + +#' The fitted models, per ELF +#' +#' With `sim$fireSense_IgnitionFittedList` and `sim$fireSense_EscapeFittedList` (named by `ELFind`), each +#' coarse pixel is assigned to its ELF from `sim$rasterToMatchLargeELF` (the value at the pixel's centre) +#' and gets that ELF's models. Otherwise all pixels get `sim$fireSense_IgnitionFitted` and +#' `sim$fireSense_EscapeFitted`. +#' +#' @param sim A `simList`. +#' @param pixelID the cells of `sim$ignitionFitRTM` to predict. +#' @return list, one element per fit, each with `ign`, `esc` and `rows` (indices into `pixelID`). +ignitionFitsByELF <- function(sim, pixelID) { + ignL <- sim$fireSense_IgnitionFittedList + if (length(ignL) > 1L) { + escL <- sim$fireSense_EscapeFittedList + if (!setequal(names(ignL), names(escL))) + stop("fireSense_IgnitionPredict: fireSense_IgnitionFittedList and fireSense_EscapeFittedList must name ", + "the same ELFs") + if (is.null(sim$rasterToMatchLargeELF)) + stop("fireSense_IgnitionPredict: several ignition fits need sim$rasterToMatchLargeELF to say which ELF ", + "each pixel is in") + elfCoarse <- postProcess(sim$rasterToMatchLargeELF, to = sim$ignitionFitRTM, method = "near") + v <- terra::values(elfCoarse[[1]], mat = FALSE)[pixelID] + lv <- terra::levels(elfCoarse[[1]])[[1]] + elf <- if (is.data.frame(lv) && NCOL(lv) >= 2) as.character(lv[[2]][match(v, lv[[1]])]) else as.character(v) + noELF <- sum(!elf %in% names(ignL)) + if (noELF > 0) + warning("fireSense_IgnitionPredict: ", noELF, " coarse pixels are in no ELF with an ignition fit; ", + "they get no ignitions", call. = FALSE) + return(lapply(names(ignL), function(id) + list(ign = ignL[[id]], esc = escL[[id]], rows = which(elf == id)))) + } + list(list(ign = sim$fireSense_IgnitionFitted, esc = sim$fireSense_EscapeFitted, + rows = seq_along(pixelID))) +} + +## Mean over the K-fold models of one fit's predictions +foldMeanPrediction <- function(fit, newdata, clamp01 = FALSE) { + mods <- fit$modelList$model + modsOnly <- mods[grep("Fold", names(mods))] + if (is.null(modsOnly) || !length(modsOnly)) stop("Not tested anymore") + preds <- lapply(modsOnly, function(model) { + p <- predict(model, newdata = newdata) + if (clamp01) pmax(pmin(1, p), 0) else p + }) + rowMeans(do.call(cbind, preds)) +} diff --git a/tests/testthat/test-metadata.R b/tests/testthat/test-metadata.R index 7257338..8629a2f 100644 --- a/tests/testthat/test-metadata.R +++ b/tests/testthat/test-metadata.R @@ -18,9 +18,12 @@ test_that("inputs are the expected names and classes", { expect_identical( inputs[order(names(inputs))], c(fireSense_EscapeFitted = "fireSense_EscapeFit", + fireSense_EscapeFittedList = "list", fireSense_igAndEscapePred_Covariates = "data.table", fireSense_IgnitionFitted = "fireSense_IgnitionFit", - flammableRTM = "SpatRaster") + fireSense_IgnitionFittedList = "list", + flammableRTM = "SpatRaster", + rasterToMatchLargeELF = "SpatRaster") ) }) diff --git a/tests/testthat/test-multiELF.R b/tests/testthat/test-multiELF.R new file mode 100644 index 0000000..346faf4 --- /dev/null +++ b/tests/testthat/test-multiELF.R @@ -0,0 +1,42 @@ +## Several fitted ELFs in one study area (the 2-ELF Mackenzie forecast, 2026-09). Each ELF's ignition and +## escape models, fitted in that ELF's own run, predict the coarse pixels of that ELF. Poisson and binomial +## draws are still made once over all pixels. +## +## Toy: ELF "A" is the left half of the grid (coarse cells 1 and 3), "B" the right half (2 and 4). +## A's ignition model predicts 2 everywhere it is asked, B's predicts 5. + +fitPair <- function(ign, esc, tag) list( + ign = structure(list(modelList = list(model = list(Fold1 = toyFold(ign, paste0("ign", tag))), fittingRes = 2), + scaleData = toyScaleData()), class = "fireSense_IgnitionFit"), + esc = structure(list(modelList = list(model = list(Fold1 = toyFold(esc, paste0("esc", tag)))), + scaleData = toyScaleData()), class = "fireSense_EscapeFit")) + +multiInputs <- function() { + o <- toyIgInputs(list(constant(0)), list(constant(0))) # the single fits are not used + a <- fitPair(constant(2), constant(0.5), "A"); b <- fitPair(constant(5), constant(0.5), "B") + o$fireSense_IgnitionFittedList <- list(A = a$ign, B = b$ign) + o$fireSense_EscapeFittedList <- list(A = a$esc, B = b$esc) + elf <- toyFlammable(integer(0)) + terra::values(elf) <- rep(rep(1:2, each = 4), times = 8) # fine cols 1-4 -> 1 (A), 5-8 -> 2 (B) + levels(elf) <- data.frame(id = 1:2, ELFind = c("A", "B")) + o$rasterToMatchLargeELF <- elf + o +} + +test_that("each ELF's ignition model predicts only its own coarse pixels", { + sim <- toyIgRun(multiInputs()) + expect_equal(toyCalls("ignA")[[1]]$pixelID, c(1L, 3L)) + expect_equal(toyCalls("ignB")[[1]]$pixelID, c(2L, 4L)) + expect_equal(probVals(sim)[, "ignitionProb"], c(2, 5, 2, 5)) + ## escapes: each ELF's escape model sees only its own ignited pixels + expect_true(all(toyCalls("escA")[[1]]$pixelID %in% c(1L, 3L))) + expect_true(all(toyCalls("escB")[[1]]$pixelID %in% c(2L, 4L))) + expect_length(toyCalls("ign1"), 0L) # the single fit is ignored +}) + +test_that("with several fits, a missing ELF raster or mismatched lists stop with a message", { + o <- multiInputs(); o$rasterToMatchLargeELF <- NULL + expect_error(toyIgRun(o), "rasterToMatchLargeELF") + o <- multiInputs(); o$fireSense_EscapeFittedList <- o$fireSense_EscapeFittedList["A"] + expect_error(toyIgRun(o), "same ELFs") +}) From de7a64eada8d6410c2df084e2132b56b86932092 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 03:54:59 +0000 Subject: [PATCH 12/25] Re-build fireSense_IgnitionPredict.Rmd --- fireSense_IgnitionPredict.html | 46 ++++++++++++++++++++++++++++++++-- fireSense_IgnitionPredict.md | 22 ++++++++++++++-- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/fireSense_IgnitionPredict.html b/fireSense_IgnitionPredict.html index 4483e72..b13d702 100644 --- a/fireSense_IgnitionPredict.html +++ b/fireSense_IgnitionPredict.html @@ -2994,8 +2994,8 @@

fireSense_IgnitionPredict Manual

-

v.1.0.0.9000

-

Last updated: 2026-09-21

+

v.1.0.0.9001

+

Last updated: 2026-09-24

@@ -3066,6 +3066,48 @@

Module inputs and parameters

+fireSense_IgnitionFittedList + + +list + + +Only with several fitted ELFs: one fireSense_IgnitionFitted per ELF, named by ELFind. Each ELF’s model predicts the coarse pixels of that ELF. + + +NA + + + + +fireSense_EscapeFittedList + + +list + + +Only with several fitted ELFs: one fireSense_EscapeFitted per ELF, named as fireSense_IgnitionFittedList. + + +NA + + + + +rasterToMatchLargeELF + + +SpatRaster + + +Only with several fitted ELFs: each pixel’s ELF (ELFind), from fireSense_ELFs with a studyAreaLarge. + + +NA + + + + fireSense_IgnitionFitted diff --git a/fireSense_IgnitionPredict.md b/fireSense_IgnitionPredict.md index 9189f90..ee267d4 100644 --- a/fireSense_IgnitionPredict.md +++ b/fireSense_IgnitionPredict.md @@ -1,7 +1,7 @@ --- title: "fireSense_IgnitionPredict Manual" -subtitle: "v.1.0.0.9000" -date: "Last updated: 2026-09-21" +subtitle: "v.1.0.0.9001" +date: "Last updated: 2026-09-24" output: bookdown::html_document2: toc: true @@ -70,6 +70,24 @@ Table \@ref(tab:moduleInputs-fireSense-IgnitionPredict) shows the full list of m Fitted escape models (`$modelList$model`, one per fold), from `fireSense_EscapeFit`. NA + + fireSense_IgnitionFittedList + list + Only with several fitted ELFs: one `fireSense_IgnitionFitted` per ELF, named by `ELFind`. Each ELF's model predicts the coarse pixels of that ELF. + NA + + + fireSense_EscapeFittedList + list + Only with several fitted ELFs: one `fireSense_EscapeFitted` per ELF, named as `fireSense_IgnitionFittedList`. + NA + + + rasterToMatchLargeELF + SpatRaster + Only with several fitted ELFs: each pixel's ELF (`ELFind`), from `fireSense_ELFs` with a `studyAreaLarge`. + NA + fireSense_IgnitionFitted fireSense_IgnitionFit From 18905cb3010b70747981bc870ace8db09a61cd88 Mon Sep 17 00:00:00 2001 From: Eliot McIntire Date: Thu, 24 Sep 2026 11:57:02 -0700 Subject: [PATCH 13/25] Add the .studyAreaName parameter .studyAreaName is the parameter PredictiveEcology modules use for the study area's name. Defined here (default NA) for consistency; the module does not use it yet. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CwcjqqK59FmTJscyi7xUqv --- NEWS.md | 1 + fireSense_IgnitionPredict.R | 4 +++- tests/testthat/test-metadata.R | 6 +++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/NEWS.md b/NEWS.md index 0ad92e5..d26cc44 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ # fireSense_IgnitionPredict (development version) +- New parameter `.studyAreaName` (default `NA`), the name PredictiveEcology modules use for the study area. This module does not use it yet. - Several fitted ELFs in one study area: with `fireSense_IgnitionFittedList` and `fireSense_EscapeFittedList` (one fit per ELF, named by `ELFind`) and `rasterToMatchLargeELF`, each ELF's models predict the coarse pixels of that ELF, with its own `scaleData`. The Poisson and binomial draws are still made once over all pixels, so one ELF gives exactly the previous result. # fireSense_IgnitionPredict (development version) diff --git a/fireSense_IgnitionPredict.R b/fireSense_IgnitionPredict.R index 0167ac7..8c0fcfa 100644 --- a/fireSense_IgnitionPredict.R +++ b/fireSense_IgnitionPredict.R @@ -11,7 +11,7 @@ defineModule(sim, list( person("Alex M", "Chubaty", email = "achubaty@for-cast.ca", role = "ctb") ), childModules = character(), - version = list(SpaDES.core = "0.1.0", fireSense_IgnitionPredict = "1.0.0.9001"), + version = list(SpaDES.core = "0.1.0", fireSense_IgnitionPredict = "1.0.0.9002"), timeframe = as.POSIXlt(c(NA, NA)), timeunit = "year", citation = list("citation.bib"), @@ -40,6 +40,8 @@ defineModule(sim, list( defineParameter(".saveInterval", "numeric", NA, NA, NA, desc = "If not `NA`, the ignition probability raster is plotted each year." ), + defineParameter(".studyAreaName", "character", NA, NA, NA, + "Human-readable name for the study area used."), defineParameter(".useCache", "logical", FALSE, NA, NA, desc = paste( "Should this entire module be run with caching activated?", diff --git a/tests/testthat/test-metadata.R b/tests/testthat/test-metadata.R index 8629a2f..2e14c10 100644 --- a/tests/testthat/test-metadata.R +++ b/tests/testthat/test-metadata.R @@ -51,10 +51,10 @@ paramTable <- function(md) { test_that("parameters have the expected names, classes and defaults", { md <- SpaDES.core::moduleMetadata(module = moduleName, path = modulePath) expected <- data.frame( - class = c("numeric", "numeric", "numeric", "numeric", "logical", "character", "logical"), + class = c("numeric", "numeric", "numeric", "numeric", "character", "logical", "character", "logical"), ## .runInitialTime defaults to start(sim), which is 0 when only metadata is parsed - default = c("0", "1", "NA", "NA", "FALSE", "\"xgboost\"", "TRUE"), - row.names = c(".runInitialTime", ".runInterval", ".saveInitialTime", ".saveInterval", ".useCache", + default = c("0", "1", "NA", "NA", "NA", "FALSE", "\"xgboost\"", "TRUE"), + row.names = c(".runInitialTime", ".runInterval", ".saveInitialTime", ".saveInterval", ".studyAreaName", ".useCache", "modelAlgorithm", "rescaleVars") ) expect_identical(paramTable(md), expected) From 426b3c06d530d511fab038ee74d603d6596a011c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 20:10:44 +0000 Subject: [PATCH 14/25] Re-build fireSense_IgnitionPredict.Rmd --- fireSense_IgnitionPredict.html | 22 +++++++++++++++++++++- fireSense_IgnitionPredict.md | 10 +++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/fireSense_IgnitionPredict.html b/fireSense_IgnitionPredict.html index b13d702..0b3fd19 100644 --- a/fireSense_IgnitionPredict.html +++ b/fireSense_IgnitionPredict.html @@ -2994,7 +2994,7 @@

fireSense_IgnitionPredict Manual

-

v.1.0.0.9001

+

v.1.0.0.9002

Last updated: 2026-09-24

@@ -3300,6 +3300,26 @@

Module inputs and parameters

+.studyAreaName + + +character + + +NA + + +NA + + +NA + + +Human-readable name for the study area used. + + + + .useCache diff --git a/fireSense_IgnitionPredict.md b/fireSense_IgnitionPredict.md index ee267d4..4c0b01b 100644 --- a/fireSense_IgnitionPredict.md +++ b/fireSense_IgnitionPredict.md @@ -1,6 +1,6 @@ --- title: "fireSense_IgnitionPredict Manual" -subtitle: "v.1.0.0.9001" +subtitle: "v.1.0.0.9002" date: "Last updated: 2026-09-24" output: bookdown::html_document2: @@ -173,6 +173,14 @@ Summary of user-visible parameters (Table \@ref(tab:moduleParams-fireSense-Ignit NA If not `NA`, the ignition probability raster is plotted each year. + + .studyAreaName + character + NA + NA + NA + Human-readable name for the study area used. + .useCache logical From 5a41274cf03194eefd4ea1790eee8babfe579806 Mon Sep 17 00:00:00 2001 From: Eliot McIntire Date: Thu, 24 Sep 2026 17:06:50 -0700 Subject: [PATCH 15/25] ignitionsAndEscapes: mark which ignitions escaped `escapes` is the coarse pixel's count, repeated on each of its ignition rows, and fireSense spread `escapes` fires from every row: 4 ignitions with 2 escapes gave 8 escaped fires. New column `escaped` marks exactly `escapes` of each coarse pixel's ignitions, drawn after the placements so they are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CwcjqqK59FmTJscyi7xUqv --- NEWS.md | 5 +++++ fireSense_IgnitionPredict.R | 15 +++++++++++--- tests/testthat/test-escaped.R | 34 ++++++++++++++++++++++++++++++++ tests/testthat/test-run-values.R | 2 +- 4 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 tests/testthat/test-escaped.R diff --git a/NEWS.md b/NEWS.md index d26cc44..b8a76a0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,10 @@ # fireSense_IgnitionPredict (development version) +- `ignitionsAndEscapes` gains `escaped`: whether each ignition escaped, with exactly `escapes` of a coarse + pixel's ignitions TRUE. `escapes` is the coarse pixel's count, repeated on each of its ignitions, and fireSense + spread `escapes` fires from every one of them, so a coarse pixel with 4 ignitions and 2 escapes gave 8 escaped + fires instead of 2. Version 1.0.0.9003. + - New parameter `.studyAreaName` (default `NA`), the name PredictiveEcology modules use for the study area. This module does not use it yet. - Several fitted ELFs in one study area: with `fireSense_IgnitionFittedList` and `fireSense_EscapeFittedList` (one fit per ELF, named by `ELFind`) and `rasterToMatchLargeELF`, each ELF's models predict the coarse pixels of that ELF, with its own `scaleData`. The Poisson and binomial draws are still made once over all pixels, so one ELF gives exactly the previous result. diff --git a/fireSense_IgnitionPredict.R b/fireSense_IgnitionPredict.R index 8c0fcfa..71ba896 100644 --- a/fireSense_IgnitionPredict.R +++ b/fireSense_IgnitionPredict.R @@ -11,7 +11,7 @@ defineModule(sim, list( person("Alex M", "Chubaty", email = "achubaty@for-cast.ca", role = "ctb") ), childModules = character(), - version = list(SpaDES.core = "0.1.0", fireSense_IgnitionPredict = "1.0.0.9002"), + version = list(SpaDES.core = "0.1.0", fireSense_IgnitionPredict = "1.0.0.9003"), timeframe = as.POSIXlt(c(NA, NA)), timeunit = "year", citation = list("citation.bib"), @@ -86,7 +86,8 @@ defineModule(sim, list( createsOutput( "ignitionsAndEscapes", "data.table", paste("One row per ignited pixel, in random order: `pixelID` (cell index of `flammableRTM`),", - "and `igProb`, `ignitions`, `escapeProb`, `escapes` of the coarse pixel it was drawn from.") + "`igProb`, `ignitions`, `escapeProb`, `escapes` of the coarse pixel it was drawn from, and", + "`escaped`, whether this ignition escaped: exactly `escapes` of a coarse pixel's rows are TRUE.") ) ) )) @@ -201,7 +202,8 @@ IgnitionPredictRun <- function(sim) { sampledChunk <- draw[chunkyPixels == n] pixelID_pop <- igs[chunkyPixels == sampledChunk$chunkyPixels] drawn <- sample(pixelID_pop$pixelID, size = sampledChunk$ignitions) - igPixels <- try(ig[pixelID %in% drawn, .(pixelID, igProb, ignitions, escapeProb, escapes)]) + igPixels <- try(ig[pixelID %in% drawn, .(pixelID, igProb, ignitions, escapeProb, escapes, + chunkyPixels)]) return(igPixels) } ) |> @@ -219,6 +221,13 @@ IgnitionPredictRun <- function(sim) { #TODO: implement proper plotting control Plots(igProbRas, types = Par$.plots, filename = paste0("IgnitionProbability_yr", time(sim))) } + ## `escapes` counts the coarse pixel's escapes and is repeated on each of its ignitions, so which of + ## them escaped is drawn here: exactly `escapes` of the coarse pixel's rows. Drawn after every pixel + ## is placed, so the placements do not change. + if (NROW(pixelID_igs)) { + pixelID_igs[, escaped := seq_len(.N) %in% sample.int(.N, min(escapes[1], .N)), by = "chunkyPixels"] + set(pixelID_igs, NULL, "chunkyPixels", NULL) + } randomOrder <- sample(1:nrow(pixelID_igs)) sim$ignitionsAndEscapes <- pixelID_igs[randomOrder,] #Randomize order diff --git a/tests/testthat/test-escaped.R b/tests/testthat/test-escaped.R new file mode 100644 index 0000000..9b96644 --- /dev/null +++ b/tests/testthat/test-escaped.R @@ -0,0 +1,34 @@ +## `escapes` is the coarse pixel's number of escapes, repeated on each of its ignitions. fireSense spread +## `escapes` fires from EVERY ignition row, so a coarse pixel with 4 ignitions and 2 escapes gave 8 escaped +## fires. `escaped` marks which ignitions escaped: exactly `escapes` of each coarse pixel's rows. + +## the toy of test-run-values.R: two ignition folds, coarse cells 2 and 3 can ignite +twoFolds <- function(escFuns = list(constant(1)), ...) { + toyIgInputs(list(byPixel(c(0, 2, 3, 0)), byPixel(c(0, 4, 3, 0))), escFuns, ...) +} + +test_that("exactly `escapes` of each coarse pixel's ignitions escaped", { + for (seed in 1:5) { + ig <- igTable(toyIgRun(twoFolds(list(constant(0.5))), seed = seed)) + expect_type(ig$escaped, "logical") + coarse <- coarseOfFine(ig$pixelID) + per <- tapply(ig$escaped, coarse, sum) + esc <- tapply(ig$escapes, coarse, `[`, 1) + expect_identical(as.integer(per), as.integer(esc)) + expect_identical(sum(ig$escaped), sum(tapply(ig$escapes, coarse, `[`, 1))) + } +}) + +test_that("every ignition escapes with probability 1, none with probability 0", { + ig1 <- igTable(toyIgRun(twoFolds(list(constant(1))))) + ig0 <- igTable(toyIgRun(twoFolds(list(constant(0))))) + expect_identical(ig1$escaped, rep(TRUE, nrow(ig1))) + expect_identical(ig0$escaped, rep(FALSE, nrow(ig0))) +}) + +test_that("the seed-42 placements are unchanged, and 5 of the 8 ignitions escape (2 + 3), not 20", { + ig <- igTable(toyIgRun(twoFolds(list(constant(0.5))), seed = 42)) + ig <- ig[order(ig$pixelID), ] + expect_identical(ig$pixelID, c(14L, 15L, 21L, 23L, 34L, 36L, 50L, 58L)) + expect_identical(sum(ig$escaped), 5L) +}) diff --git a/tests/testthat/test-run-values.R b/tests/testthat/test-run-values.R index d53f763..5e3f32a 100644 --- a/tests/testthat/test-run-values.R +++ b/tests/testthat/test-run-values.R @@ -47,7 +47,7 @@ test_that("each ignition is a distinct flammable fine cell inside its own coarse for (seed in 1:3) { sim <- toyIgRun(twoFolds(), seed = seed) ig <- igTable(sim) - expect_identical(names(ig), c("pixelID", "igProb", "ignitions", "escapeProb", "escapes")) + expect_identical(names(ig), c("pixelID", "igProb", "ignitions", "escapeProb", "escapes", "escaped")) expect_gt(nrow(ig), 0) expect_false(anyDuplicated(ig$pixelID) > 0) coarse <- coarseOfFine(ig$pixelID) From 67ac77c2ce5465920e4ef0e0766ba54d2a5453c9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 02:14:13 +0000 Subject: [PATCH 16/25] Re-build fireSense_IgnitionPredict.Rmd --- fireSense_IgnitionPredict.html | 6 +++--- fireSense_IgnitionPredict.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/fireSense_IgnitionPredict.html b/fireSense_IgnitionPredict.html index 0b3fd19..bc01f37 100644 --- a/fireSense_IgnitionPredict.html +++ b/fireSense_IgnitionPredict.html @@ -2994,8 +2994,8 @@

fireSense_IgnitionPredict Manual

-

v.1.0.0.9002

-

Last updated: 2026-09-24

+

v.1.0.0.9003

+

Last updated: 2026-09-25

@@ -3393,7 +3393,7 @@

Module outputs

data.table -One row per ignited pixel, in random order: pixelID (cell index of flammableRTM), and igProb, ignitions, escapeProb, escapes of the coarse pixel it was drawn from. +One row per ignited pixel, in random order: pixelID (cell index of flammableRTM), igProb, ignitions, escapeProb, escapes of the coarse pixel it was drawn from, and escaped, whether this ignition escaped: exactly escapes of a coarse pixel’s rows are TRUE. diff --git a/fireSense_IgnitionPredict.md b/fireSense_IgnitionPredict.md index 4c0b01b..0cd974d 100644 --- a/fireSense_IgnitionPredict.md +++ b/fireSense_IgnitionPredict.md @@ -1,7 +1,7 @@ --- title: "fireSense_IgnitionPredict Manual" -subtitle: "v.1.0.0.9002" -date: "Last updated: 2026-09-24" +subtitle: "v.1.0.0.9003" +date: "Last updated: 2026-09-25" output: bookdown::html_document2: toc: true @@ -224,7 +224,7 @@ Description of the module outputs (Table \@ref(tab:moduleOutputs-fireSense-Ignit ignitionsAndEscapes data.table - One row per ignited pixel, in random order: `pixelID` (cell index of `flammableRTM`), and `igProb`, `ignitions`, `escapeProb`, `escapes` of the coarse pixel it was drawn from. + One row per ignited pixel, in random order: `pixelID` (cell index of `flammableRTM`), `igProb`, `ignitions`, `escapeProb`, `escapes` of the coarse pixel it was drawn from, and `escaped`, whether this ignition escaped: exactly `escapes` of a coarse pixel's rows are TRUE. From 2971762ac9af2b251892180711c6644b2b717959 Mon Sep 17 00:00:00 2001 From: Eliot McIntire Date: Tue, 29 Sep 2026 11:09:57 -0700 Subject: [PATCH 17/25] Rename module to fireSense_ignitionPredict Renamed from fireSense_IgnitionPredict; sibling module names updated; version 1.1.0. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01CwcjqqK59FmTJscyi7xUqv --- .github/workflows/pkgdown-module.yaml | 4 +- .github/workflows/render-module-rmd.yaml | 6 +-- .github/workflows/testthat-module.yaml | 6 +-- .gitignore | 4 +- NEWS.md | 4 +- citation.bib | 2 +- ... references_fireSense_ignitionPredict.bib} | 0 ...onPredict.R => fireSense_ignitionPredict.R | 20 ++++----- ...edict.Rmd => fireSense_ignitionPredict.Rmd | 42 +++++++++---------- ...t.Rproj => fireSense_ignitionPredict.Rproj | 0 ...ict.html => fireSense_ignitionPredict.html | 24 +++++------ ...Predict.md => fireSense_ignitionPredict.md | 26 ++++++------ tests/Test_FrequencyPredict.R | 6 +-- tests/testthat/setup-toyIgnition.R | 4 +- tests/testthat/test-covariate-scaling.R | 4 +- tests/testthat/test-fit-scaling.R | 2 +- 16 files changed, 78 insertions(+), 76 deletions(-) rename citations/{references_fireSense_IgnitionPredict.bib => references_fireSense_ignitionPredict.bib} (100%) rename fireSense_IgnitionPredict.R => fireSense_ignitionPredict.R (95%) rename fireSense_IgnitionPredict.Rmd => fireSense_ignitionPredict.Rmd (76%) rename fireSense_IgnitionPredict.Rproj => fireSense_ignitionPredict.Rproj (100%) rename fireSense_IgnitionPredict.html => fireSense_ignitionPredict.html (99%) rename fireSense_IgnitionPredict.md => fireSense_ignitionPredict.md (92%) diff --git a/.github/workflows/pkgdown-module.yaml b/.github/workflows/pkgdown-module.yaml index 79e472c..35a93e9 100644 --- a/.github/workflows/pkgdown-module.yaml +++ b/.github/workflows/pkgdown-module.yaml @@ -5,7 +5,7 @@ ## ## Publishes from `development` (the pilot's choice; which branch should publish ## is an open question on #40). Pull requests build the site without deploying. -## The site is at https://predictiveecology.github.io/fireSense_IgnitionPredict/ until a +## The site is at https://predictiveecology.github.io/fireSense_ignitionPredict/ until a ## custom domain is set with the `url` input. name: pkgdown-module @@ -26,5 +26,5 @@ jobs: contents: write uses: PredictiveEcology/actions/.github/workflows/pkgdown-module.yaml@main with: - module: fireSense_IgnitionPredict + module: fireSense_ignitionPredict diff --git a/.github/workflows/render-module-rmd.yaml b/.github/workflows/render-module-rmd.yaml index 88a3af9..44100fb 100644 --- a/.github/workflows/render-module-rmd.yaml +++ b/.github/workflows/render-module-rmd.yaml @@ -20,8 +20,8 @@ on: - development paths: - .github/workflows/render-module-rmd.yaml - - fireSense_IgnitionPredict.Rmd - - fireSense_IgnitionPredict.R + - fireSense_ignitionPredict.Rmd + - fireSense_ignitionPredict.R workflow_dispatch: jobs: @@ -35,4 +35,4 @@ jobs: contents: write uses: PredictiveEcology/actions/.github/workflows/render-module-rmd.yaml@main with: - module: fireSense_IgnitionPredict + module: fireSense_ignitionPredict diff --git a/.github/workflows/testthat-module.yaml b/.github/workflows/testthat-module.yaml index 0ef537b..e7dda1c 100644 --- a/.github/workflows/testthat-module.yaml +++ b/.github/workflows/testthat-module.yaml @@ -1,5 +1,5 @@ ## Example caller for the testthat-module reusable workflow. -## Replace fireSense_IgnitionPredict with the module name (which must match the repository name). +## Replace fireSense_ignitionPredict with the module name (which must match the repository name). name: testthat-module @@ -10,7 +10,7 @@ on: branches: [main, master, development] paths: - .github/workflows/testthat-module.yaml - - fireSense_IgnitionPredict.R + - fireSense_ignitionPredict.R - R/** - tests/** workflow_dispatch: @@ -22,4 +22,4 @@ jobs: ## itself is all it needs. uses: PredictiveEcology/actions/.github/workflows/testthat-module.yaml@main with: - module: fireSense_IgnitionPredict + module: fireSense_ignitionPredict diff --git a/.gitignore b/.gitignore index 5d5f4db..2b23413 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,5 @@ .DS_Store .quarto -fireSense_IgnitionPredict_cache/ -fireSense_IgnitionPredict2.Rmd +fireSense_ignitionPredict_cache/ +fireSense_ignitionPredict2.Rmd diff --git a/NEWS.md b/NEWS.md index b8a76a0..c7b76c6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,6 @@ -# fireSense_IgnitionPredict (development version) +# fireSense_ignitionPredict 1.1.0 + +- Renamed from `fireSense_IgnitionPredict` to `fireSense_ignitionPredict` (module naming convention `_`); projects must rename the module and its `params` key. The class `fireSense_IgnitionFit` and the objects `fireSense_IgnitionFitted`, `fireSense_IgnitionFittedList` and `fireSense_IgnitionPredicted` keep their names. Version 1.1.0. - `ignitionsAndEscapes` gains `escaped`: whether each ignition escaped, with exactly `escapes` of a coarse pixel's ignitions TRUE. `escapes` is the coarse pixel's count, repeated on each of its ignitions, and fireSense diff --git a/citation.bib b/citation.bib index 1441840..6f71453 100644 --- a/citation.bib +++ b/citation.bib @@ -1,6 +1,6 @@ @Manual{, - title = {fireSense_IgnitionPredict}, + title = {fireSense_ignitionPredict}, author = {{Authors}}, organization = {Organization}, address = {Somewhere, Someplace}, diff --git a/citations/references_fireSense_IgnitionPredict.bib b/citations/references_fireSense_ignitionPredict.bib similarity index 100% rename from citations/references_fireSense_IgnitionPredict.bib rename to citations/references_fireSense_ignitionPredict.bib diff --git a/fireSense_IgnitionPredict.R b/fireSense_ignitionPredict.R similarity index 95% rename from fireSense_IgnitionPredict.R rename to fireSense_ignitionPredict.R index 71ba896..3df481d 100644 --- a/fireSense_IgnitionPredict.R +++ b/fireSense_ignitionPredict.R @@ -1,8 +1,8 @@ defineModule(sim, list( - name = "fireSense_IgnitionPredict", + name = "fireSense_ignitionPredict", description = paste( "Predicts annual ignition and escape probabilities from the models fitted by", - "fireSense_IgnitionFit and fireSense_EscapeFit, and draws the pixels that ignite and escape."), + "fireSense_ignitionFit and fireSense_EscapeFit, and draws the pixels that ignite and escape."), keywords = c("fire frequency", "additive property", "poisson", "negative binomial", "fireSense"), authors = c( person("Eliot", "McIntire", email = "eliot.mcintire@nrcan-rncan.gc.ca", role = c("aut", "cre")), @@ -11,11 +11,11 @@ defineModule(sim, list( person("Alex M", "Chubaty", email = "achubaty@for-cast.ca", role = "ctb") ), childModules = character(), - version = list(SpaDES.core = "0.1.0", fireSense_IgnitionPredict = "1.0.0.9003"), + version = list(SpaDES.core = "0.1.0", fireSense_ignitionPredict = "1.1.0"), timeframe = as.POSIXlt(c(NA, NA)), timeunit = "year", citation = list("citation.bib"), - documentation = list("README.txt", "fireSense_IgnitionPredict.Rmd"), + documentation = list("README.txt", "fireSense_ignitionPredict.Rmd"), reqdPkgs = list( "magrittr", "terra", "PredictiveEcology/fireSenseUtils@development (>=0.1.0)" @@ -64,7 +64,7 @@ defineModule(sim, list( desc = "Only with several fitted ELFs: each pixel's ELF (`ELFind`), from `fireSense_ELFs` with a `studyAreaLarge`."), expectsInput("fireSense_IgnitionFitted", "fireSense_IgnitionFit", desc = paste("Fitted ignition models (`$modelList$model`, one per fold) and `$modelList$fittingRes`,", - "from `fireSense_IgnitionFit`."), + "from `fireSense_ignitionFit`."), sourceURL = NA ), expectsInput("fireSense_igAndEscapePred_Covariates", "data.table", @@ -102,7 +102,7 @@ defineModule(sim, list( #' @param debug Not used. #' #' @return The `simList`, invisibly. -doEvent.fireSense_IgnitionPredict <- function(sim, eventTime, eventType, debug = FALSE) { +doEvent.fireSense_ignitionPredict <- function(sim, eventTime, eventType, debug = FALSE) { moduleName <- currentModule(sim) switch(eventType, @@ -120,7 +120,7 @@ doEvent.fireSense_IgnitionPredict <- function(sim, eventTime, eventType, debug = } }, save = { - message("fireSense_IgnitionPredict: the `save` event does nothing.") + message("fireSense_ignitionPredict: the `save` event does nothing.") }, warning(paste("Undefined event type: '", current(sim)[1, "eventType", with = FALSE], "' in module '", current(sim)[1, "moduleName", with = FALSE], "'", @@ -277,10 +277,10 @@ ignitionFitsByELF <- function(sim, pixelID) { if (length(ignL) > 1L) { escL <- sim$fireSense_EscapeFittedList if (!setequal(names(ignL), names(escL))) - stop("fireSense_IgnitionPredict: fireSense_IgnitionFittedList and fireSense_EscapeFittedList must name ", + stop("fireSense_ignitionPredict: fireSense_IgnitionFittedList and fireSense_EscapeFittedList must name ", "the same ELFs") if (is.null(sim$rasterToMatchLargeELF)) - stop("fireSense_IgnitionPredict: several ignition fits need sim$rasterToMatchLargeELF to say which ELF ", + stop("fireSense_ignitionPredict: several ignition fits need sim$rasterToMatchLargeELF to say which ELF ", "each pixel is in") elfCoarse <- postProcess(sim$rasterToMatchLargeELF, to = sim$ignitionFitRTM, method = "near") v <- terra::values(elfCoarse[[1]], mat = FALSE)[pixelID] @@ -288,7 +288,7 @@ ignitionFitsByELF <- function(sim, pixelID) { elf <- if (is.data.frame(lv) && NCOL(lv) >= 2) as.character(lv[[2]][match(v, lv[[1]])]) else as.character(v) noELF <- sum(!elf %in% names(ignL)) if (noELF > 0) - warning("fireSense_IgnitionPredict: ", noELF, " coarse pixels are in no ELF with an ignition fit; ", + warning("fireSense_ignitionPredict: ", noELF, " coarse pixels are in no ELF with an ignition fit; ", "they get no ignitions", call. = FALSE) return(lapply(names(ignL), function(id) list(ign = ignL[[id]], esc = escL[[id]], rows = which(elf == id)))) diff --git a/fireSense_IgnitionPredict.Rmd b/fireSense_ignitionPredict.Rmd similarity index 76% rename from fireSense_IgnitionPredict.Rmd rename to fireSense_ignitionPredict.Rmd index ec76109..2f559b1 100644 --- a/fireSense_IgnitionPredict.Rmd +++ b/fireSense_ignitionPredict.Rmd @@ -1,6 +1,6 @@ --- -title: "fireSense_IgnitionPredict Manual" -subtitle: "v.`r SpaDES.core::moduleMetadata(module = 'fireSense_IgnitionPredict', path = '..')$version`" +title: "fireSense_ignitionPredict Manual" +subtitle: "v.`r SpaDES.core::moduleMetadata(module = 'fireSense_ignitionPredict', path = '..')$version`" date: "Last updated: `r Sys.Date()`" output: bookdown::html_document2: @@ -12,17 +12,17 @@ output: keep_md: yes editor_options: chunk_output_type: console -bibliography: citations/references_fireSense_IgnitionPredict.bib +bibliography: citations/references_fireSense_ignitionPredict.bib link-citations: true always_allow_html: true --- -# fireSense_IgnitionPredict Module +# fireSense_ignitionPredict Module -(ref:fireSense-IgnitionPredict) *fireSense_IgnitionPredict* +(ref:fireSense-ignitionPredict) *fireSense_ignitionPredict* -```{r setup-fireSense-IgnitionPredict, include=FALSE} +```{r setup-fireSense-ignitionPredict, include=FALSE} knitr::opts_chunk$set(echo = TRUE, eval = FALSE, results = "hold") ## change to eval = TRUE if all chunks are to be evaluated dir.create("figures", showWarnings = FALSE) @@ -38,14 +38,14 @@ download.file(url = "https://img.shields.io/badge/Made%20with-Markdown-1f425f.pn #### Authors: -`r paste(as.character(SpaDES.core::moduleMetadata(module = "fireSense_IgnitionPredict", path = '..')$authors), sep = ', ')` +`r paste(as.character(SpaDES.core::moduleMetadata(module = "fireSense_ignitionPredict", path = '..')$authors), sep = ', ')` ## Module Overview ### Module summary -Each year, predicts ignitions and escapes from the models fitted by *fireSense_IgnitionFit* and *fireSense_EscapeFit*, for the ignition component of fireSense [@Marchal:2017a; @Marchal:2017b; @Marchal:2019]. +Each year, predicts ignitions and escapes from the models fitted by *fireSense_ignitionFit* and *fireSense_EscapeFit*, for the ignition component of fireSense [@Marchal:2017a; @Marchal:2017b; @Marchal:2019]. 1. The covariates in `fireSense_igAndEscapePred_Covariates` are rescaled with `fireSenseUtils::prepareCovariatesOuter()`, the function used for fitting (`rescaleVars`, `modelAlgorithm`). 2. Expected ignitions per coarse pixel are the mean of the predictions of the per-fold ignition models; the number of ignitions is drawn from a Poisson. @@ -59,21 +59,21 @@ Only `xgboost` models are supported. `ignitionFitRTM` (the coarse raster used for fitting, from *fireSense_dataPrepFit*) is also read from the `simList`, though it is not declared as an input. `modelAlgorithm` and `rescaleVars` must have the same value in every module that defines them. -Table \@ref(tab:moduleInputs-fireSense-IgnitionPredict) shows the full list of module inputs. +Table \@ref(tab:moduleInputs-fireSense-ignitionPredict) shows the full list of module inputs. -```{r moduleInputs-fireSense-IgnitionPredict, echo = FALSE, eval = TRUE, message = FALSE, warning = FALSE} -df_inputs <- SpaDES.core::moduleInputs("fireSense_IgnitionPredict", "..") +```{r moduleInputs-fireSense-ignitionPredict, echo = FALSE, eval = TRUE, message = FALSE, warning = FALSE} +df_inputs <- SpaDES.core::moduleInputs("fireSense_ignitionPredict", "..") knitr::kable(df_inputs, - caption = "List of (ref:fireSense-IgnitionPredict) input objects and their description.") |> + caption = "List of (ref:fireSense-ignitionPredict) input objects and their description.") |> kableExtra::kable_styling(latex_options = "scale_down", full_width = TRUE) ``` -Summary of user-visible parameters (Table \@ref(tab:moduleParams-fireSense-IgnitionPredict)) +Summary of user-visible parameters (Table \@ref(tab:moduleParams-fireSense-ignitionPredict)) -```{r moduleParams-fireSense-IgnitionPredict, echo = FALSE, eval = TRUE, message = FALSE, warning = FALSE} -df_params <- SpaDES.core::moduleParams("fireSense_IgnitionPredict", "..") -knitr::kable(df_params, caption = "List of (ref:fireSense-IgnitionPredict) parameters and their description.") |> +```{r moduleParams-fireSense-ignitionPredict, echo = FALSE, eval = TRUE, message = FALSE, warning = FALSE} +df_params <- SpaDES.core::moduleParams("fireSense_ignitionPredict", "..") +knitr::kable(df_params, caption = "List of (ref:fireSense-ignitionPredict) parameters and their description.") |> kableExtra::kable_styling(latex_options = "scale_down", full_width = TRUE) ``` @@ -89,11 +89,11 @@ If `.saveInterval` is not `NA`, the ignition probability raster is plotted each ### Module outputs -Description of the module outputs (Table \@ref(tab:moduleOutputs-fireSense-IgnitionPredict)). +Description of the module outputs (Table \@ref(tab:moduleOutputs-fireSense-ignitionPredict)). -```{r moduleOutputs-fireSense-IgnitionPredict, echo = FALSE, eval = TRUE, message = FALSE, warning = FALSE} -df_outputs <- SpaDES.core::moduleOutputs("fireSense_IgnitionPredict", "..") -knitr::kable(df_outputs, caption = "List of (ref:fireSense-IgnitionPredict) outputs and their description.") |> +```{r moduleOutputs-fireSense-ignitionPredict, echo = FALSE, eval = TRUE, message = FALSE, warning = FALSE} +df_outputs <- SpaDES.core::moduleOutputs("fireSense_ignitionPredict", "..") +knitr::kable(df_outputs, caption = "List of (ref:fireSense-ignitionPredict) outputs and their description.") |> kableExtra::kable_styling(latex_options = "scale_down", full_width = TRUE) ``` @@ -104,7 +104,7 @@ It is normally run as part of the [fireSense](https://github.com/PredictiveEcolo ### Getting help -- +- ## References diff --git a/fireSense_IgnitionPredict.Rproj b/fireSense_ignitionPredict.Rproj similarity index 100% rename from fireSense_IgnitionPredict.Rproj rename to fireSense_ignitionPredict.Rproj diff --git a/fireSense_IgnitionPredict.html b/fireSense_ignitionPredict.html similarity index 99% rename from fireSense_IgnitionPredict.html rename to fireSense_ignitionPredict.html index bc01f37..434e161 100644 --- a/fireSense_IgnitionPredict.html +++ b/fireSense_ignitionPredict.html @@ -11,7 +11,7 @@ -fireSense_IgnitionPredict Manual +fireSense_ignitionPredict Manual