diff --git a/NEWS.md b/NEWS.md index a942740..82732db 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,14 @@ # fireSense_dataPrepFit (development version) -- A cached `prepSpreadFitData` event, or cached `harmonizeFireData()` call, now re-runs when a fireSenseUtils function it calls changes: they are keyed on those functions (`.useCacheArgs`, `fireSenseUtils::harmonizeFireDataDeps()`). Requires fireSenseUtils >= 0.2.3.9053. +- New parameter `fuelCovariates` (default `"domSecOther"`): `prepare_SpreadFit()` now builds the + spread covariates as `dom_agb_`/`sec_agb_` (the ELF's two fuel classes with the + most total treed AGB), `other_agb` and `treedWetland_agb`, chosen once per ELF by the new + `fireSenseUtils::chooseDomSecFuelClasses()` and recorded in `sim$fuelClassRoles`. `rstLCC` is + now passed to `fireSenseCovariatesCreate()` (previously never passed, so `treedWetland` never + appeared). `fuelCovariates = "species"` keeps the previous one-column-per-fuel-class behaviour. + `chooseDomSecFuelClasses()` is added to the `prepSpreadFitData` cache key (`.useCacheArgs`). + Needs `fireSenseUtils@development (>= 0.2.3.9057)`. Version 1.2.0.9016. +- A cached `prepSpreadFitData` event, or cached `harmonizeFireData()` call, now re-runs when a fireSenseUtils function it calls changes: they are keyed on those functions (`.useCacheArgs`, `fireSenseUtils::harmonizeFireDataDeps()`). Requires fireSenseUtils >= 0.2.3.9053. Version 1.2.0.9015. - `spreadFitFilename` now defaults to `"latest"`: each polygon's fit comes from the most recent ledger file in `spreadFitGoogleDriveFolder` that has it (`fireSenseUtils::latestSpreadFits()`, which reads only the current model's files, `fireSenseParams_*.rds`). So "this ELF has a diff --git a/fireSense_dataPrepFit.R b/fireSense_dataPrepFit.R index 29e16fe..fbda1bc 100644 --- a/fireSense_dataPrepFit.R +++ b/fireSense_dataPrepFit.R @@ -8,7 +8,7 @@ defineModule(sim, list( person(c("Alex", "M"), "Chubaty", role = "ctb", email = "achubaty@for-cast.ca") ), childModules = character(0), - version = list(fireSense_dataPrepFit = "1.2.0.9015"), + version = list(fireSense_dataPrepFit = "1.2.0.9016"), timeframe = as.POSIXlt(c(NA, NA)), timeunit = "year", citation = list("citation.bib"), @@ -17,7 +17,7 @@ defineModule(sim, list( reqdPkgs = list("data.table", "fastDummies", "Require", "PredictiveEcology/reproducible@development (>= 3.2.1.9042)", # CacheGeo re-reads a changed local file "PredictiveEcology/climateData@development (>= 2.2.3.9006)", - "PredictiveEcology/fireSenseUtils@development (>= 0.2.3.9053)", + "PredictiveEcology/fireSenseUtils@development (>= 0.2.3.9057)", "FOR-CAST/fireregimetools@main (>= 0.1.0.9008)", "ggplot2", "parallel", "purrr", "raster", "sf", "sp", "PredictiveEcology/LandR@development (>= 1.2.0.9015)", @@ -67,6 +67,15 @@ defineModule(sim, list( "named `FuelClass` exists in the `LandR::sppEquivalencies_CA` and will be used ", "by default. To change the `FuelClass` classifications, add a column to that table, ", "or to `sim$sppEquiv` and then modify this `fuelClassCol` parameter"), + defineParameter("fuelCovariates", "character", c("domSecOther", "species"), NA, NA, + paste("How the spread-fit fuel covariates are represented. `\"domSecOther\"` (default):", + "exactly four AGB columns per ELF, `dom_agb_` and `sec_agb_` (the", + "two fuel classes with the most total treed AGB over the fit study area),", + "`other_agb` (the rest, pooled) and `treedWetland_agb` (all tree AGB on treed-wetland", + "pixels, removed from the other three there); see", + "`fireSenseUtils::fireSenseCovariatesCreate()`. `\"species\"`: the previous one", + "column per fuel class. `fireSense_dataPrepPredict` follows whichever a fit used;", + "this is not a parameter there.")), defineParameter("minBufferSize", "numeric", 5000, NA, NA, paste("Minimum number of cells in each fire's burned-plus-buffer sample, applied after `areaMultiplier`.")), defineParameter("nonflammableLCC", "numeric", c(0, 20, 31, 32, 33), NA, NA, @@ -103,7 +112,8 @@ defineModule(sim, list( fireSenseUtils::assessFuelClasses, fireSenseUtils::fuelClassPrep, fireSenseUtils::makeLandcoverDT, fireSenseUtils::makeTSD))), prepSpreadFitData = list(.cacheExtra = quote(c(list( - fireSenseUtils::bufferToArea, fireSenseUtils::climateRasterToDataTable, + fireSenseUtils::bufferToArea, fireSenseUtils::chooseDomSecFuelClasses, + fireSenseUtils::climateRasterToDataTable, fireSenseUtils::fireSenseCovariatesCreate, fireSenseUtils::harmonizeFireData, fireSenseUtils::makeMutuallyExclusive, fireSenseUtils::rasterFireBufferDT, fireSenseUtils::rasterFireSpreadPoints), fireSenseUtils::harmonizeFireDataDeps())))), @@ -231,6 +241,10 @@ defineModule(sim, list( "List of data.tables, one per `dataYears`, of `pixelID` and the fuel covariates in the fire buffers."), createsOutput("fireSense_spreadFormula", "character", "formula for spread, using climate and vegetation covariates, as character"), + createsOutput("fuelClassRoles", "list", + paste("Only when `fuelCovariates = \"domSecOther\"`: `list(domClass =, secClass =)`,", + "the fuel classes chosen once for this ELF by `fireSenseUtils::chooseDomSecFuelClasses()`.", + "Both `NA` with `fuelCovariates = \"species\"` or when the ELF has no tree fuel class.")), createsOutput("ignitionFirePoints", "SpatVector", paste("The input, in the CRS of `rasterToMatch` and clipped to `studyArea`.")), createsOutput("ignitionFitRTM", "SpatRaster", @@ -687,10 +701,28 @@ prepare_SpreadFit <- function(sim) { ## when landcoverDT is included, as is the case here, non-forest pixels in cohortData are masked out ## this is necessary when LandR and fireSense have differing concepts of non-forest - dig1 <- .robustDigest(list(sim$landcoverDTs, sim$flammableRTMs)) + dig1 <- .robustDigest(list(sim$landcoverDTs, sim$flammableRTMs, sim$rstLCCs)) dig1a <- .robustDigest(list(sim$cohortDatas, sim$pixelGroupMaps, sim$nonForest_timeSinceDisturbances)) dig2 <- append(dig1, dig1a) - + + fuelCovariates <- match.arg(P(sim)$fuelCovariates, c("domSecOther", "species")) + sim$fuelClassRoles <- list(domClass = NA_character_, secClass = NA_character_) + if (identical(fuelCovariates, "domSecOther")) { + ## chosen once per ELF (the most recent data year, as with sim$rstLCC/sim$rstLCC_RTM elsewhere + ## in this module), not independently for every data year -- a prediction must build the same + ## dom_agb_*/sec_agb_* columns whichever year it is predicting + sim$fuelClassRoles <- Cache(fireSenseUtils::chooseDomSecFuelClasses, + cohortData = tail(sim$cohortDatas, 1)[[1]], + pixelGroupMap = tail(sim$pixelGroupMaps, 1)[[1]], + flammableRTM = tail(sim$flammableRTMs, 1)[[1]], + landcoverDT = tail(sim$landcoverDTs, 1)[[1]], + sppEquiv = sim$sppEquiv, fuelClassCol = P(sim)$fuelClassCol, + sppEquivCol = P(sim)$sppEquivCol, cutoffForYoungAge = P(sim)$cutoffForYoungAge, + .cacheExtra = dig2, omitArgs = c("cohortData", "pixelGroupMap", "flammableRTM", "landcoverDT")) + message("fireSense_dataPrepFit: dominant fuel class = ", sim$fuelClassRoles$domClass, + "; secondary = ", sim$fuelClassRoles$secClass) + } + # This adds youngAge vegData <- Map(f = fireSenseUtils:::fireSenseCovariatesCreate, cohortData = sim$cohortDatas, @@ -698,18 +730,22 @@ prepare_SpreadFit <- function(sim) { flammableRTM = sim$flammableRTMs, landcoverDT = sim$landcoverDTs, nonForest_timeSinceDisturbance = sim$nonForest_timeSinceDisturbances, + rstLCC = sim$rstLCCs, MoreArgs = list(sppEquiv = sim$sppEquiv, sppEquivCol = P(sim)$sppEquivCol, fuelClassCol = P(sim)$fuelClassCol, cutoffForYoungAge = P(sim)$cutoffForYoungAge, missingLCCgroup = sim$missingLCCgroup, nonForestedLCCGroups = sim$nonForestedLCCGroups, + fuelCovariates = fuelCovariates, + domClass = sim$fuelClassRoles$domClass, + secClass = sim$fuelClassRoles$secClass, nonForestCanBeYoungAge = P(sim)$nonForestCanBeYoungAge, studyAreaName = P(sim)$.studyAreaName - ) + ) ) |> - Cache(.cacheExtra = dig2, - omitArgs = c("landcoverDT", "flammableRTM", "cohortData", "pixelGroupMap", "nonForest_timeSinceDisturbance"), + Cache(.cacheExtra = dig2, + omitArgs = c("landcoverDT", "flammableRTM", "cohortData", "pixelGroupMap", "nonForest_timeSinceDisturbance", "rstLCC"), .functionName = "spreadCovariatesCreate") # Add "year" column vegData <- Map(v = vegData, n = names(vegData), function(v, n) { diff --git a/fireSense_dataPrepFit.html b/fireSense_dataPrepFit.html index 78adb13..ca62dd3 100644 --- a/fireSense_dataPrepFit.html +++ b/fireSense_dataPrepFit.html @@ -2994,7 +2994,7 @@

fireSense_dataPrepFit Manual

-

v.1.2.0.9015

+

v.1.2.0.9016

Last updated: 2026-09-28

@@ -3004,7 +3004,7 @@

Last updated: 2026-09-28

fireSense_dataPrepFit Module

-

made-with-Markdown

+

made-with-Markdown

Authors:

@@ -3015,7 +3015,7 @@

Module Overview

Prepares the data needed to fit fireSense_IgnitionFit, fireSense_EscapeFit and fireSense_SpreadFit.

Module summary

-

The module combines historical fire records, vegetation, land cover and climate into the covariate tables used to fit the three fireSense processes (Marchal, Steve G. Cumming, et al. 2017b; Marchal, Steve G. Cumming, et al. 2017a; Marchal et al. 2019): ignition, escape (an ignition that grows beyond one pixel) and spread.

+

The module combines historical fire records, vegetation, land cover and climate into the covariate tables used to fit the three fireSense processes (Marchal, Cumming, and McIntire 2017b; Marchal, Cumming, and McIntire 2017a; Marchal, Cumming, and McIntire 2019): ignition, escape (an ignition that grows beyond one pixel) and spread.

Vegetation, land cover and stand age are snapshots, one per year in P(sim)$dataYears (default 2000, 2010, 2020). Each year in P(sim)$fireYears uses the snapshot at or before it, so no fire year may precede the first data year, and every data year needs at least one fire year. Climate is annual and is matched to the year of each fire. @@ -3757,6 +3757,26 @@

Module inputs and parameters

+fuelCovariates + + +character + + +domSecOt…. + + +NA + + +NA + + +How the spread-fit fuel covariates are represented. "domSecOther" (default): exactly four AGB columns per ELF, dom_agb_&lt;class&gt; and sec_agb_&lt;class&gt; (the two fuel classes with the most total treed AGB over the fit study area), other_agb (the rest, pooled) and treedWetland_agb (all tree AGB on treed-wetland pixels, removed from the other three there); see fireSenseUtils::fireSenseCovariatesCreate(). "species": the previous one column per fuel class. fireSense_dataPrepPredict follows whichever a fit used; this is not a parameter there. + + + + minBufferSize @@ -3991,7 +4011,7 @@

Module inputs and parameters

NA -Extra reproducible::Cache() arguments, by event. A cached event’s digest covers this module’s code but not the package functions it calls, so dataPrepBuild and prepSpreadFitData pass those in .cacheExtra: a changed function then re-runs the event. +Extra reproducible::Cache() arguments, by event. A cached event’s digest covers this module’s code but not the package functions it calls, so dataPrepBuild passes those in .cacheExtra: a changed function then re-runs the event. @@ -4250,6 +4270,17 @@

Module outputs

+fuelClassRoles + + +list + + +Only when fuelCovariates = "domSecOther": list(domClass =, secClass =), the fuel classes chosen once for this ELF by fireSenseUtils::chooseDomSecFuelClasses(). Both NA with fuelCovariates = "species" or when the ELF has no tree fuel class. + + + + ignitionFirePoints @@ -4470,7 +4501,7 @@

References

Marchal, Jean, Steve G Cumming, and Eliot J B McIntire. 2017b. “Land Cover, More Than Monthly Fire Weather, Drives Fire-Size Distribution in Southern Québec Forests: Implications for Fire Risk Management.” PLoS ONE 12 (6): 1–17. https://doi.org/10.1371/journal.pone.0179294.
-Marchal, Jean, Steven G. Cumming, and Eliot J. B. McIntire. 2019. “Turning Down the Heat: Vegetation Feedbacks Limit Fire Regime Responses to Global Warming.” Ecosystems, ahead of print, May. https://doi.org/10.1007/s10021-019-00398-2. +Marchal, Jean, Steven G. Cumming, and Eliot J. B. McIntire. 2019. “Turning Down the Heat: Vegetation Feedbacks Limit Fire Regime Responses to Global Warming.” Ecosystems, May. https://doi.org/10.1007/s10021-019-00398-2.
diff --git a/fireSense_dataPrepFit.md b/fireSense_dataPrepFit.md index c6c77b4..1600a05 100644 --- a/fireSense_dataPrepFit.md +++ b/fireSense_dataPrepFit.md @@ -1,6 +1,6 @@ --- title: "fireSense_dataPrepFit Manual" -subtitle: "v.1.2.0.9015" +subtitle: "v.1.2.0.9016" date: "Last updated: 2026-09-28" output: bookdown::html_document2: @@ -386,6 +386,14 @@ Parameters are in Table \@ref(tab:moduleParams-fireSense-dataPrepFit). NA the column in `sppEquiv` that defines unique fuel classes. A column named `FuelClass` exists in the `LandR::sppEquivalencies_CA` and will be used by default. To change the `FuelClass` classifications, add a column to that table, or to `sim$sppEquiv` and then modify this `fuelClassCol` parameter + + fuelCovariates + character + domSecOt.... + NA + NA + How the spread-fit fuel covariates are represented. `"domSecOther"` (default): exactly four AGB columns per ELF, `dom_agb_<class>` and `sec_agb_<class>` (the two fuel classes with the most total treed AGB over the fit study area), `other_agb` (the rest, pooled) and `treedWetland_agb` (all tree AGB on treed-wetland pixels, removed from the other three there); see `fireSenseUtils::fireSenseCovariatesCreate()`. `"species"`: the previous one column per fuel class. `fireSense_dataPrepPredict` follows whichever a fit used; this is not a parameter there. + minBufferSize numeric @@ -480,7 +488,7 @@ Parameters are in Table \@ref(tab:moduleParams-fireSense-dataPrepFit). list(.ca.... NA NA - Extra `reproducible::Cache()` arguments, by event. A cached event's digest covers this module's code but not the package functions it calls, so `dataPrepBuild` and `prepSpreadFitData` pass those in `.cacheExtra`: a changed function then re-runs the event. + Extra `reproducible::Cache()` arguments, by event. A cached event's digest covers this module's code but not the package functions it calls, so `dataPrepBuild` passes those in `.cacheExtra`: a changed function then re-runs the event. @@ -628,6 +636,11 @@ Description of the module outputs (Table \@ref(tab:moduleOutputs-fireSense-dataP character formula for spread, using climate and vegetation covariates, as character + + fuelClassRoles + list + Only when `fuelCovariates = "domSecOther"`: `list(domClass =, secClass =)`, the fuel classes chosen once for this ELF by `fireSenseUtils::chooseDomSecFuelClasses()`. Both `NA` with `fuelCovariates = "species"` or when the ELF has no tree fuel class. + ignitionFirePoints SpatVector diff --git a/tests/testthat/test-domSecOtherFuels.R b/tests/testthat/test-domSecOtherFuels.R new file mode 100644 index 0000000..775183e --- /dev/null +++ b/tests/testthat/test-domSecOtherFuels.R @@ -0,0 +1,74 @@ +## fireSense_dataPrepFit.R ~690 (prepare_SpreadFit()): fireSenseCovariatesCreate() was never given +## `rstLCC`, so `treedWetland` never appeared, and there was no way to ask for the new +## dom/sec/other AGB fuel representation. `fuelCovariates` (default "domSecOther") now picks the +## representation, and the dominant/secondary classes are chosen once per ELF (not once per data +## year, or every prediction would build a different pair of columns) via +## fireSenseUtils::chooseDomSecFuelClasses(), stored in sim$fuelClassRoles. + +## generic AST walk: apply `f` to every call node in `expr`, recursing into every part +walkCalls <- function(expr, f) { + if (is.call(expr)) { + f(expr) + for (part in as.list(expr)) if (!missing(part) && !is.null(part)) walkCalls(part, f) + } + invisible(NULL) +} + +test_that("fuelCovariates defaults to domSecOther, with species as the only other choice", { + md <- SpaDES.core::moduleMetadata(module = moduleName, path = modulePath) + def <- stats::setNames(md$parameters$default, md$parameters$paramName) + expect_identical(def$fuelCovariates, c("domSecOther", "species")) +}) + +test_that("reqdPkgs floors fireSenseUtils at >= 0.2.3.9057 (chooseDomSecFuelClasses etc.)", { + md <- SpaDES.core::moduleMetadata(module = moduleName, path = modulePath) + fsu <- grep("fireSenseUtils", md$reqdPkgs, value = TRUE) + expect_length(fsu, 1L) + expect_match(fsu, "0\\.2\\.3\\.9057") +}) + +test_that("prepare_SpreadFit() picks fuelClassRoles once per ELF via chooseDomSecFuelClasses()", { + body <- body(prepare_SpreadFit) + found <- FALSE + walkCalls(body, function(e) { + if (identical(deparse(e[[1]]), "<-") && is.call(e[[2]]) && + identical(deparse(e[[2]]), "sim$fuelClassRoles") && + any(grepl("chooseDomSecFuelClasses", deparse(e[[3]]), fixed = TRUE))) + found <<- TRUE + }) + expect_true(found) +}) + +test_that("chooseDomSecFuelClasses() is asked about one representative data year, not every year", { + ## "once per ELF": cohortData/pixelGroupMap/flammableRTM/landcoverDT are each a single tail() + ## element, not the per-data-year lists (sim$cohortDatas etc.) fireSenseCovariatesCreate()'s + ## Map() call uses. + body <- body(prepare_SpreadFit) + callTxt <- NULL + walkCalls(body, function(e) { + if (identical(deparse(e[[1]]), "<-") && is.call(e[[2]]) && + identical(deparse(e[[2]]), "sim$fuelClassRoles") && + any(grepl("chooseDomSecFuelClasses", deparse(e[[3]]), fixed = TRUE))) + callTxt <<- gsub("\\s+", " ", paste(deparse(e[[3]]), collapse = " ")) + }) + expect_match(callTxt, "tail(sim$cohortDatas, 1)", fixed = TRUE) + expect_false(grepl("cohortData = sim$cohortDatas,", callTxt, fixed = TRUE)) +}) + +test_that("the fireSenseCovariatesCreate() Map() call passes rstLCC, fuelCovariates, domClass, secClass", { + body <- body(prepare_SpreadFit) + mapCall <- NULL + walkCalls(body, function(e) { + if (identical(deparse(e[[1]]), "Map") && !is.null(names(e)) && "f" %in% names(e) && + grepl("fireSenseCovariatesCreate", deparse(e[["f"]]), fixed = TRUE)) + mapCall <<- e + }) + expect_false(is.null(mapCall)) + expect_true("rstLCC" %in% names(mapCall)) + expect_identical(deparse(mapCall[["rstLCC"]]), "sim$rstLCCs") + + moreArgsTxt <- gsub("\\s+", " ", paste(deparse(mapCall[["MoreArgs"]]), collapse = " ")) + expect_match(moreArgsTxt, "fuelCovariates = fuelCovariates", fixed = TRUE) + expect_match(moreArgsTxt, "domClass = sim$fuelClassRoles$domClass", fixed = TRUE) + expect_match(moreArgsTxt, "secClass = sim$fuelClassRoles$secClass", fixed = TRUE) +}) diff --git a/tests/testthat/test-metadata.R b/tests/testthat/test-metadata.R index 96fdbf3..3ff5e14 100644 --- a/tests/testthat/test-metadata.R +++ b/tests/testthat/test-metadata.R @@ -59,6 +59,7 @@ test_that("outputs are the expected names and classes", { fireSense_spreadFormula = "character", flammableRTM = "SpatRaster", flammableRTMs = "list", + fuelClassRoles = "list", fuelClassTable = "data.table", ignitionFirePoints = "SpatVector", ignitionFitRTM = "SpatRaster", @@ -93,7 +94,7 @@ test_that("parameters are the expected names", { sort(c(".studyAreaName", ".useCache", ".useCacheArgs", "areaMultiplier", "bufferForFireRaster", "cutoffForYoungAge", "dataYears", "escapeSizeHa", "estimateFuelClasses", "fireYears", "flammabilityThreshold", - "forestedLCC", "fuelClassCol", "igAggFactor", + "forestedLCC", "fuelClassCol", "fuelCovariates", "igAggFactor", "minBufferSize", "nonflammableLCC", "nonForestCanBeYoungAge", "sppEquivCol", "spreadFitFilename", "spreadFitGoogleDriveFolder", "targetFuelClasses",