Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
59 changes: 42 additions & 17 deletions fireSense_dataPrepFit.R
Original file line number Diff line number Diff line change
Expand Up @@ -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`",
Expand Down Expand Up @@ -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<dataYear>`),
#' 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<dataYear>`),
#' 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]])
}

Expand All @@ -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.
#'
Expand All @@ -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`
Expand Down Expand Up @@ -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 = ", "))
Expand Down
2 changes: 1 addition & 1 deletion fireSense_dataPrepFit.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -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`).

Expand Down
6 changes: 3 additions & 3 deletions fireSense_dataPrepFit.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).

Expand Down Expand Up @@ -340,7 +340,7 @@ Parameters are in Table \@ref(tab:moduleParams-fireSense-dataPrepFit).
<td style="text-align:left;"> 1985, 19.... </td>
<td style="text-align:left;"> NA </td>
<td style="text-align:left;"> NA </td>
<td style="text-align:left;"> 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. </td>
<td style="text-align:left;"> 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. </td>
</tr>
<tr>
<td style="text-align:left;"> estimateFuelClasses </td>
Expand All @@ -356,7 +356,7 @@ Parameters are in Table \@ref(tab:moduleParams-fireSense-dataPrepFit).
<td style="text-align:left;"> 1985, 19.... </td>
<td style="text-align:left;"> NA </td>
<td style="text-align:left;"> NA </td>
<td style="text-align:left;"> 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. </td>
<td style="text-align:left;"> 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. </td>
</tr>
<tr>
<td style="text-align:left;"> flammabilityThreshold </td>
Expand Down
11 changes: 6 additions & 5 deletions tests/testthat/test-fireYearsDefault.R
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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]))
})
2 changes: 1 addition & 1 deletion tests/testthat/test-joinFireBuffersToVeg.R
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Loading
Loading