From 1b4bdcc52adca3746b7533fc2a4a1ab2c19dfc11 Mon Sep 17 00:00:00 2001 From: Eliot McIntire Date: Thu, 15 Oct 2026 13:54:15 -0700 Subject: [PATCH] Fit each fire year against the latest vegetation year strictly before it A year's satellite vegetation already shows part of that year's fires. yearGroups(), lccShareForYear() and fitDecisionYearName() now look up the data year through one helper, vegetationYear(); fireYears must be after the first dataYear. Co-Authored-By: Claude Opus 5.5 --- NEWS.md | 2 + fireSense_dataPrepFit.R | 59 +++++++++++++++------- fireSense_dataPrepFit.Rmd | 2 +- fireSense_dataPrepFit.md | 6 +-- tests/testthat/test-fireYearsDefault.R | 11 ++-- tests/testthat/test-joinFireBuffersToVeg.R | 2 +- tests/testthat/test-lccShareForYear.R | 22 ++++---- tests/testthat/test-yearGroups.R | 35 ++++++++----- 8 files changed, 90 insertions(+), 49 deletions(-) diff --git a/NEWS.md b/NEWS.md index dd33483..023376a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # fireSense_dataPrepFit (development version) +- Fixed: each fire year is fitted against vegetation from an earlier year. A year's satellite vegetation already shows part of that year's fires (ELF 6.6.2: pixels burned in 2010 have mean biomass 64 in the 2005 vegetation and 26 in the 2010 vegetation, while unburned pixels are near 79 in both), so the fits learned that fire burns where the vegetation already looks burned. A fire year now uses the latest data year strictly before it (1986-1990 use 1985, 1991-2000 use 1990, ...; `yearGroups()`, `lccShareForYear()`, `fitDecisionYear()`'s data year), the decision year of the structure of the model included, and every `fireYears` must be after the first of `dataYears`, with an error otherwise. The `fireYears` default therefore starts in 1986. Fits, ignition and escape alike, must be redone. Prediction is unchanged: it still uses the data year at or before the year. + - `spreadFitGoogleDriveFolder` defaults to `fireSenseUtils::spreadFitLedgerFolder()`, which follows `options(fireSenseUtils.spreadFitLedgerFolder = )`. `fireSenseUtils` floor raised to 0.2.3.9097, the first version with `spreadFitLedgerFolder()`. - Fixed: `init` is declared in the new `.neverCache` parameter, so it is never cached, even when `.useCache` (the module's or `.globals`) lists it. It reads the spread-fit ledger on Google Drive, which is not part of the event's cache key, so a cached `init` replayed an old "fit exists" or "no fit yet" answer. Needs SpaDES.core >= 3.2.1.9037. diff --git a/fireSense_dataPrepFit.R b/fireSense_dataPrepFit.R index 4a0d25e..7fddcb0 100644 --- a/fireSense_dataPrepFit.R +++ b/fireSense_dataPrepFit.R @@ -46,17 +46,18 @@ defineModule(sim, list( defineParameter("dataYears", "integer", c(1985L, 1990L, 2000L, 2010L, 2020L), NA_integer_, NA_integer_, paste("Two or more increasing years for which vegetation, land cover and stand age are built", "(`cohortDatas`, `rstLCCs`, `standAgeMaps`, ...).", - "Each fire year uses the data year at or before it, so no `fireYears` may precede the first,", + "Each fire year uses the latest data year strictly before it (a year's satellite vegetation already", + "shows part of that year's fires), so every `fireYears` must be after the first,", "and every data year needs at least one fire year before the next data year.")), defineParameter("estimateFuelClasses", "logical", TRUE, NA, NA, paste("Estimate fuel classes with `fireSenseUtils::assessFuelClasses`? Skipped if the user supplies", "`nonForestedLCCGroups`, `fuelClassTable` or a `FuelClass` column that differs from LandR's,", "or if a previous SpreadFit exists for the study area.")), - defineParameter("fireYears", "integer", fireSenseUtils::defaultFireYears(), NA, NA, - paste("Years of fire records to use for fitting. None may precede the first of `dataYears`,", - "and `historicalClimateRasters` must cover all of them. The default runs from 1985, the", - "first SCANFI V2 year, to the latest year with historical climate for every tile", - "(`fireSenseUtils::defaultFireYears()`); climate is the last of the inputs to reach a year.", + defineParameter("fireYears", "integer", fireSenseUtils::defaultFireYears()[-1L], NA, NA, + paste("Years of fire records to use for fitting. All must be after the first of `dataYears`,", + "and `historicalClimateRasters` must cover all of them. The default runs from 1986, the", + "year after the first SCANFI V2 year, to the latest year with historical climate for every tile", + "(`fireSenseUtils::defaultFireYears()` without its first year); climate is the last of the inputs to reach a year.", "`fireSense_ELFs` takes its `fireYears` default from the same function.")), defineParameter("flammabilityThreshold", "numeric", fireSenseUtils::fireSenseFlammabilityThreshold, 0, 1, paste("Minimum proportion of flammable fine-resolution land cover for a `rasterToMatch`", @@ -974,18 +975,17 @@ prepare_SpreadFit <- function(sim) { #' Land-cover shares of flammable pixels, for one year #' -#' The land cover is the data year `year`'s vegetation comes from: the data year closest to, and not -#' after, `year` (`fireSenseUtils::dataYearFor()`, the rule [yearGroups()] assigns a vegetation year to -#' each fire year with). Read from the per-year lists `sim$rstLCCs` and `sim$flammableRTMs` (named `year`), +#' The land cover is the data year `year`'s vegetation comes from: the latest data year strictly before +#' `year` ([vegetationYear()], the rule [yearGroups()] assigns a vegetation year to each fire year with). Read from the per-year lists `sim$rstLCCs` and `sim$flammableRTMs` (named `year`), #' never from `sim$rstLCC_RTM` and `sim$flammableRTM`: `fireSense_dataPrepPredict` overwrites those two #' with the year it predicts (a hindcast of 1985 sets 1985), and what is a spread covariate of a fit #' must not change with the year predicted. #' #' @param sim a `simList` with `rstLCCs` and `flammableRTMs`. -#' @param year integer, the year the shares are for (see [fitDecisionYear()] for a fit). +#' @param year integer, the fire year the shares are for (see [fitDecisionYear()] for a fit). #' @return as `fireSenseUtils::lccFlammableShare()`. lccShareForYear <- function(sim, year) { - el <- fireSenseUtils::dataYearElement(sim$rstLCCs, year) + el <- fireSenseUtils::dataYearElement(sim$rstLCCs, vegetationYear(year)) fireSenseUtils::lccFlammableShare(sim$rstLCCs[[el]], sim$flammableRTMs[[el]]) } @@ -995,7 +995,7 @@ lccShareForYear <- function(sim, year) { #' non-forest classes are grouped (`assessFuelClasses()`), the dominant and secondary fuel classes #' (`chooseDomSecFuelClasses()`) -- is decided once per ELF, at fit time, for all of `fireYears`, and a #' prediction takes it from the fit's ledger row. It is decided for the last fire year, -#' `max(P(sim)$fireYears)`, whose vegetation data year (`fireSenseUtils::dataYearFor()`) is the latest the fit uses and +#' `max(P(sim)$fireYears)`, whose vegetation data year ([vegetationYear()]) is the latest the fit uses and #' the one a prediction starts from. A hindcast's `fireSense_dataPrepFit` run (same `fireYears`, the #' ledger row already there) makes the same decision, whatever its `dataYears` or the year it predicts. #' @@ -1010,7 +1010,7 @@ fitDecisionYear <- function(sim) { #' lists (`cohortDatas`, `rstLCCs`, ...; they share `rstLCCs`' names), `fireSenseUtils::dataYearElement()`. #' @rdname fitDecisionYear fitDecisionYearName <- function(sim) { - fireSenseUtils::dataYearElement(sim$rstLCCs, fitDecisionYear(sim)) + fireSenseUtils::dataYearElement(sim$rstLCCs, vegetationYear(fitDecisionYear(sim))) } #' Fire buffers and ignition points from `historicalFireRaster` @@ -1779,18 +1779,43 @@ cohDat <- "cohortData" pixGM <- "pixelGroupMap" saMap <- "standAgeMap" +#' The latest year whose vegetation a fire year can use +#' +#' A satellite vegetation year already shows part of that year's fires, so a fit pairs a fire year with +#' vegetation of an earlier year: the year before it, for the data year lookup ([vegetationDataYear()], +#' `fireSenseUtils::dataYearElement()`). This is the one place the module states that rule; prediction +#' keeps `fireSenseUtils::dataYearFor()`'s "not after". +#' +#' @param fireYears integer fire years. +#' @return integer, the year to look up the data year of. +vegetationYear <- function(fireYears) fireYears - 1L + +#' The data year whose vegetation each fire year uses +#' +#' @param dataYears increasing integer years with vegetation data. +#' @param fireYears integer years of fire records. +#' @return integer, per fire year the latest of `dataYears` strictly before it (`NA` if none). +vegetationDataYear <- function(dataYears, fireYears) { + fireSenseUtils::dataYearFor(dataYears, vegetationYear(fireYears)) +} + #' Group fire years by the data year whose vegetation they use #' -#' Each fire year belongs to the latest data year at or before it (`fireSenseUtils::dataYearFor()`, the -#' rule every match of a year to a data year uses). Stops if a fire year precedes the first data year, -#' or if a data year gets no fire years. +#' Each fire year belongs to the latest data year strictly before it ([vegetationDataYear()], the +#' rule every match of a fire year to a data year uses). Stops if a fire year is not after the first +#' data year, or if a data year gets no fire years. #' #' @param dataYears increasing integer years with vegetation data. #' @param fireYears integer years of fire records. #' @param minmaxOnly if `TRUE`, return only the first and last fire year of each group. #' @return list with one integer vector per data year, named by it. yearGroups <- function(dataYears, fireYears, minmaxOnly = TRUE) { - ageGroups <- split(fireYears, factor(fireSenseUtils::dataYearFor(dataYears, fireYears), levels = dataYears)) + tooEarly <- fireYears <= dataYears[1] + if (any(tooEarly)) { + stop("fireYears must all be after the first of dataYears (", dataYears[1], "), whose vegetation already ", + "shows that year's fires; remove ", paste(fireYears[tooEarly], collapse = ", "), " from fireYears") + } + ageGroups <- split(fireYears, factor(vegetationDataYear(dataYears, fireYears), levels = dataYears)) empty <- lengths(ageGroups) == 0 if (any(empty)) { stop("dataYears with no fireYears before the next dataYear: ", paste(dataYears[empty], collapse = ", ")) diff --git a/fireSense_dataPrepFit.Rmd b/fireSense_dataPrepFit.Rmd index 78a23e1..61333b4 100644 --- a/fireSense_dataPrepFit.Rmd +++ b/fireSense_dataPrepFit.Rmd @@ -51,7 +51,7 @@ Prepares the data needed to fit `fireSense_ignitionFit` (ignition and escape) an The module combines historical fire records, vegetation, land cover and climate into the covariate tables used to fit the three `fireSense` processes [@Marchal:2017a; @Marchal:2017b; @Marchal: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. +Each year in `P(sim)$fireYears` uses the latest snapshot strictly before it (a year's satellite vegetation already shows part of that year's fires), so every fire year must be after 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. Ignition and escape share climate variables (`sim$climateVariablesForFire$ignition`); spread can use others (`$spread`). diff --git a/fireSense_dataPrepFit.md b/fireSense_dataPrepFit.md index 2f2b861..42645bb 100644 --- a/fireSense_dataPrepFit.md +++ b/fireSense_dataPrepFit.md @@ -42,7 +42,7 @@ Prepares the data needed to fit `fireSense_ignitionFit` (ignition and escape) an The module combines historical fire records, vegetation, land cover and climate into the covariate tables used to fit the three `fireSense` processes [@Marchal:2017a; @Marchal:2017b; @Marchal: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. +Each year in `P(sim)$fireYears` uses the latest snapshot strictly before it (a year's satellite vegetation already shows part of that year's fires), so every fire year must be after 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. Ignition and escape share climate variables (`sim$climateVariablesForFire$ignition`); spread can use others (`$spread`). @@ -340,7 +340,7 @@ Parameters are in Table \@ref(tab:moduleParams-fireSense-dataPrepFit). 1985, 19.... NA NA - Two or more increasing years for which vegetation, land cover and stand age are built (`cohortDatas`, `rstLCCs`, `standAgeMaps`, ...). Each fire year uses the data year at or before it, so no `fireYears` may precede the first, and every data year needs at least one fire year before the next data year. + Two or more increasing years for which vegetation, land cover and stand age are built (`cohortDatas`, `rstLCCs`, `standAgeMaps`, ...). Each fire year uses the latest data year strictly before it (a year's satellite vegetation already shows part of that year's fires), so every `fireYears` must be after the first, and every data year needs at least one fire year before the next data year. estimateFuelClasses @@ -356,7 +356,7 @@ Parameters are in Table \@ref(tab:moduleParams-fireSense-dataPrepFit). 1985, 19.... NA NA - Years of fire records to use for fitting. None may precede the first of `dataYears`, and `historicalClimateRasters` must cover all of them. The default runs from 1985, the first SCANFI V2 year, to the latest year with historical climate for every tile (`fireSenseUtils::defaultFireYears()`); climate is the last of the inputs to reach a year. `fireSense_ELFs` takes its `fireYears` default from the same function. + Years of fire records to use for fitting. All must be after the first of `dataYears`, and `historicalClimateRasters` must cover all of them. The default runs from 1986, the year after the first SCANFI V2 year, to the latest year with historical climate for every tile (`fireSenseUtils::defaultFireYears()` without its first year); climate is the last of the inputs to reach a year. `fireSense_ELFs` takes its `fireYears` default from the same function. flammabilityThreshold diff --git a/tests/testthat/test-fireYearsDefault.R b/tests/testthat/test-fireYearsDefault.R index e74ac21..d8916bc 100644 --- a/tests/testthat/test-fireYearsDefault.R +++ b/tests/testthat/test-fireYearsDefault.R @@ -1,23 +1,24 @@ ## The fitting window is 1985 to the latest year every input can supply, and climate is the last to ## reach a year. A hard-coded end (2002:2025 before 1.2.0.9010) ran past climate, so every project had ## to set `fireYears` itself; the first dataYear postdated 1985, so a 1985 start could not be the default. +## A fire year needs vegetation from an earlier year, so the default starts the year after the first data year. -test_that("the default fire years run from the first data year to the latest climate year", { +test_that("the default fire years run from the year after the first data year to the latest climate year", { md <- SpaDES.core::moduleMetadata(module = moduleName, path = modulePath) defaults <- stats::setNames(md$parameters$default, md$parameters$paramName) fireYears <- defaults[["fireYears"]] dataYears <- defaults[["dataYears"]] - expect_identical(min(fireYears), 1985L) + expect_identical(min(fireYears), 1986L) expect_identical(max(fireYears), climateData::latestHistoricalYear()) expect_identical(fireYears, min(fireYears):max(fireYears)) expect_identical(dataYears, c(1985L, 1990L, 2000L, 2010L, 2020L)) - ## the defaults satisfy the module's own constraints: no fire year before the first data year, + ## the defaults satisfy the module's own constraints: every fire year after the first data year, ## and every data year has fire years expect_no_error(yearGroups(dataYears, fireYears)) }) -test_that("the default is fireSenseUtils::defaultFireYears(), which fireSense_ELFs shares", { +test_that("the default is fireSenseUtils::defaultFireYears() less its first year, which fireSense_ELFs shares", { ## the default is defined once, in fireSenseUtils, so the ELF merging and the fit count the same years exprs <- parse(file.path(modulePath, moduleName, paste0(moduleName, ".R")), keep.source = FALSE) found <- list() @@ -30,5 +31,5 @@ test_that("the default is fireSenseUtils::defaultFireYears(), which fireSense_EL } for (e in exprs) walk(e) expect_length(found, 1L) - expect_identical(found[[1]][[4]], quote(fireSenseUtils::defaultFireYears())) + expect_identical(found[[1]][[4]], quote(fireSenseUtils::defaultFireYears()[-1L])) }) diff --git a/tests/testthat/test-joinFireBuffersToVeg.R b/tests/testthat/test-joinFireBuffersToVeg.R index 03dc793..70b9e9a 100644 --- a/tests/testthat/test-joinFireBuffersToVeg.R +++ b/tests/testthat/test-joinFireBuffersToVeg.R @@ -25,7 +25,7 @@ test_that("fire buffers join only to their own data year's vegetation", { expect_identical(nrow(out), length(fireYears) * length(pixels)) expect_false(anyDuplicated(out, by = c("fireYear", "pixelID")) > 0) fireYearNum <- as.integer(gsub(fireSenseUtils::yearTxt, "", out$fireYear)) - expectedDataYear <- max.col(outer(fireYearNum, dataYears, ">=") * 1, ties.method = "last") + expectedDataYear <- max.col(outer(fireYearNum, dataYears, ">") * 1, ties.method = "last") expect_identical(out$year, dataYears[expectedDataYear]) expect_identical(out$fuel, out$year) }) diff --git a/tests/testthat/test-lccShareForYear.R b/tests/testthat/test-lccShareForYear.R index 50d556a..adc9648 100644 --- a/tests/testthat/test-lccShareForYear.R +++ b/tests/testthat/test-lccShareForYear.R @@ -1,8 +1,8 @@ ## A hindcast (fireSense_dataPrepPredict, vegetationYear = 1985) overwrites sim$rstLCC_RTM and ## sim$flammableRTM with 1985 before prepare_SpreadFit() runs. Whether treed wetland is a spread ## covariate (minCovariateProp) and the fuel-class land-cover shares must not follow that: they come -## from the data year the decision year's vegetation uses -- the data year closest to and not after -## it, the rule yearGroups() assigns vegetation to fire years with -- read from the per-year lists. +## from the data year the decision year's vegetation uses -- the latest data year strictly before it, the +## rule yearGroups() assigns vegetation to fire years with -- read from the per-year lists. lccRaster <- function(vals) terra::rast(nrows = 2, ncols = 5, vals = vals) @@ -14,23 +14,24 @@ lccSim <- function() { flammableRTMs = list(year2010 = flam, year2020 = flam)) } -test_that("yearGroups assigns each fire year the data year fireSenseUtils::dataYearFor() gives", { +test_that("yearGroups assigns each fire year the data year vegetationDataYear() gives", { dataYears <- c(1985L, 1990L, 2000L, 2010L, 2020L) - groups <- yearGroups(dataYears, 1985:2024, minmaxOnly = FALSE) + groups <- yearGroups(dataYears, 1986:2024, minmaxOnly = FALSE) expect_identical(unlist(Map(function(dy, ys) rep(dy, length(ys)), as.integer(names(groups)), groups), use.names = FALSE), - fireSenseUtils::dataYearFor(dataYears, 1985:2024)) + vegetationDataYear(dataYears, 1986:2024)) }) test_that("lccShareForYear reads the list element of the data year the year's vegetation uses", { sim <- lccSim() share2010 <- c(`1` = 0.8, `2` = 0.2) share2020 <- c(`1` = 0.9, `2` = 0.1) - expect_equal(lccShareForYear(sim, 2010L), share2010) + expect_equal(lccShareForYear(sim, 2011L), share2010) expect_equal(lccShareForYear(sim, 2015L), share2010) # 2015 uses 2010, not the latest year - expect_equal(lccShareForYear(sim, 2020L), share2020) + expect_equal(lccShareForYear(sim, 2020L), share2010) # 2020 burns are already in the 2020 vegetation + expect_equal(lccShareForYear(sim, 2021L), share2020) expect_equal(lccShareForYear(sim, 2024L), share2020) - expect_error(lccShareForYear(sim, 2005L), "before the first dataYear") + expect_error(lccShareForYear(sim, 2010L), "before the first dataYear") # no vegetation before 2010 ## the shared objects a hindcast overwrites with another year change nothing sim$rstLCC_RTM <- sim$rstLCCs$year2010 @@ -54,7 +55,8 @@ test_that("the fit's structure decisions use the fit's decision year, not the sh test_that("fitDecisionYearName is the list element of the decision year's data year", { ## P(sim)$fireYears is not available outside a simList: the element rule alone sim <- lccSim() - expect_identical(fireSenseUtils::dataYearElement(sim$rstLCCs, 2024L), "year2020") - expect_true(grepl("dataYearElement(sim$rstLCCs, fitDecisionYear(sim))", + expect_identical(fireSenseUtils::dataYearElement(sim$rstLCCs, vegetationYear(2024L)), "year2020") + expect_identical(fireSenseUtils::dataYearElement(sim$rstLCCs, vegetationYear(2020L)), "year2010") + expect_true(grepl("dataYearElement(sim$rstLCCs, vegetationYear(fitDecisionYear(sim)))", paste(deparse(body(fitDecisionYearName)), collapse = " "), fixed = TRUE)) }) diff --git a/tests/testthat/test-yearGroups.R b/tests/testthat/test-yearGroups.R index 7a33837..dd90312 100644 --- a/tests/testthat/test-yearGroups.R +++ b/tests/testthat/test-yearGroups.R @@ -1,26 +1,37 @@ -## yearGroups() built the groups with rep(mm[1:2], diff(mm)), which only works for exactly three -## dataYears: four or more stopped with "invalid 'times' argument", so phase 1 could not use -## dataYears = c(1985, 1990, 2000, 2010, 2020). -test_that("yearGroups gives each fire year to the latest data year at or before it", { +## A satellite vegetation year already shows part of that year's burns (6.6.2: pixels burned in 2010 +## have mean biomass 64 in SCANFI 2005 and 26 in SCANFI 2010, unburned pixels ~79 in both). A fire year +## therefore uses the latest data year STRICTLY BEFORE it, so the fit does not learn that fire burns where +## the vegetation already looks burned. +test_that("yearGroups gives each fire year the latest data year strictly before it", { dataYears <- c(1985L, 1990L, 2000L, 2010L, 2020L) - fireYears <- 1985:2025 + fireYears <- 1986:2025 groups <- yearGroups(dataYears, fireYears, minmaxOnly = FALSE) expect_identical(names(groups), as.character(dataYears)) - expect_identical(groups[["1985"]], 1985:1989) - expect_identical(groups[["1990"]], 1990:1999) - expect_identical(groups[["2020"]], 2020:2025) + expect_identical(groups[["1985"]], 1986:1990) + expect_identical(groups[["1990"]], 1991:2000) + expect_identical(groups[["2000"]], 2001:2010) + expect_identical(groups[["2010"]], 2011:2020) + expect_identical(groups[["2020"]], 2021:2025) expect_identical(unname(unlist(groups)), fireYears) - expect_identical(yearGroups(dataYears, fireYears)[["2000"]], c(2000L, 2009L)) + expect_identical(yearGroups(dataYears, fireYears)[["2000"]], c(2001L, 2010L)) +}) + +test_that("a fire year pairs with the data year before it: 1990 with 1985, 1991 with 1990, 2020 with 2010, 2024 with 2020", { + dataYears <- c(1985L, 1990L, 2000L, 2010L, 2020L) + fireYears <- c(1990L, 1991L, 2020L, 2024L) + expect_identical(vegetationDataYear(dataYears, fireYears), c(1985L, 1990L, 2010L, 2020L)) }) test_that("yearGroups keeps the grouping for three data years", { groups <- yearGroups(c(2000L, 2010L, 2020L), 2002:2025, minmaxOnly = FALSE) - expect_identical(groups, list(`2000` = 2002:2009, `2010` = 2010:2019, `2020` = 2020:2025)) + expect_identical(groups, list(`2000` = 2002:2010, `2010` = 2011:2020, `2020` = 2021:2025)) }) -test_that("yearGroups stops on fire years without vegetation data and on data years without fire years", { - expect_error(yearGroups(c(1985L, 1990L), 1984:1995), "before the first dataYear") +test_that("yearGroups stops on fire years not after the first data year and on data years without fire years", { + expect_error(yearGroups(c(1985L, 1990L), 1984:1995), "after the first of dataYears") + ## a fire year equal to the first data year is not allowed: that vegetation already shows its fires + expect_error(yearGroups(c(1985L, 1990L), 1985:1995), "after the first of dataYears.*1985") expect_error(yearGroups(c(2000L, 2010L, 2020L), 2012:2025), "no fireYears") })