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_spreadPredict (development version)

- A single fitted ELF now rescales its covariates with the `covMinMax_spread` of its ledger row, as several ELFs already did, not with `sim$covMinMax_spread`, which `fireSense_spreadFit` rebuilds in every run from that run's tables (in a hindcast, from a formula rebuilt for another year, which can lack a fitted term). A row without ranges (an older ledger) still uses `sim$covMinMax_spread`. A fitted covariate with no range now stops, naming it; before, it silently stayed on its raw scale.

- A single fitted ELF now predicts with the covariates its fitted parameters name, as several ELFs already did, not the terms of `sim$fireSense_spreadFormula`. A hindcast whose formula had a term the fit did not (e.g. `treedWetland_agb`) stopped with "not found in data objects". Predictions where the formula matched the fit do not change. A fitted coefficient with no covariate still stops, naming it.

# fireSense_spreadPredict 1.2.0
Expand Down
32 changes: 29 additions & 3 deletions fireSense_spreadPredict.R
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ doEvent.fireSense_spreadPredict <- function(sim, eventTime, eventType, debug = F
#' `paramSetForRep()`), and writes it to `sim$fireSense_SpreadPredicted`. Parameter sets are not averaged.
#'
#' With one fitted ELF (one row of `sim$studyAreaWithSpreadParams`) every pixel uses its parameters and
#' `sim$covMinMax_spread`. With several, each ELF's model (its parameter set, its `covMinMax_spread`, and
#' its `covMinMax_spread` (`ledgerCovMinMax()`; `sim$covMinMax_spread` only when the row has none). With several, each ELF's model (its parameter set, its `covMinMax_spread`, and
#' only the covariates it was fitted with) predicts its own pixels, from `sim$rasterToMatchLargeELF`, and
#' those within `ELFblendWidth` of them; overlapping predictions are averaged with the weights of
#' `ELFblendWeights()`.
Expand All @@ -151,7 +151,7 @@ spreadPredictRun <- function(sim) {

if (NROW(sa) == 1L) {
params1 <- paramSetForRep(sa$params[[1]], P(sim)$.rep)
pred <- spreadProbOneELF(covs, params = params1, covMinMax = sim$covMinMax_spread,
pred <- spreadProbOneELF(covs, params = params1, covMinMax = ledgerCovMinMax(sa, 1L, sim$covMinMax_spread),
yr = time(sim),
maxFireSpread = P(sim)$maxFireSpread, lowerSpreadProb = P(sim)$lowerSpreadProb,
covCentre = ledgerCovCentre(sa, 1L))
Expand Down Expand Up @@ -239,7 +239,8 @@ ELFblendWeights <- function(elfRas, template, pixelID, ids, width) {
#' makes them; fuel biomass logged). It can hold more than the fit used (every ELF's covariates, or a term the
#' spread formula has but the fit does not); only the covariates named in `params` are used.
#' @param params one-row `data.frame`: the ELF's parameter set for this replicate (`paramSetForRep()`).
#' @param covMinMax the ELF's `covMinMax_spread`.
#' @param covMinMax the ELF's `covMinMax_spread` (`ledgerCovMinMax()`): the ranges the fit rescaled its
#' covariates with. Every fitted covariate must be a column of it, or it is left unscaled.
#' @param yr,maxFireSpread,lowerSpreadProb as for `fireSenseUtils::spreadProbFromIntegerCovs()` and
#' `fireSenseUtils::logisticAll()`.
#' @param covCentre the ELF's `covCentre_spread` (`ledgerCovCentre()`): the covariate means the fit subtracted
Expand Down Expand Up @@ -287,6 +288,13 @@ spreadProbOneELF <- function(covs, params, covMinMax, yr, maxFireSpread, lowerSp
## the covariates this ELF was fitted with, not the spread formula's terms: they can differ (a hindcast
## rebuilds the formula from another year's land cover), and `covs` can hold more than this ELF's
covs <- covs[, c("pixelID", fitted), with = FALSE]
## spreadProbFromIntegerCovs() rescales only the columns of covMinMax: a fitted covariate it lacks would
## silently stay on its raw scale (x1000)
noRange <- setdiff(fitted, names(covMinMax))
if (length(noRange))
stop(moduleName, "> covMinMax_spread has no range for the fitted covariate(s) ",
paste(noRange, collapse = ", "), "; it has ", paste(names(covMinMax), collapse = ", "),
". The ranges must be the fit's (its ledger row).")

# integers x 1000, the form `spreadProbFromIntegerCovs` expects
shortAnnDTx1000 <- toX1000(list(covs))[[1]] |> setDT()
Expand Down Expand Up @@ -331,6 +339,24 @@ spreadProbOneELF <- function(covs, params, covMinMax, yr, maxFireSpread, lowerSp

yearSpreadSDTxt <- "yearSpreadSD"

#' The covariate ranges of one ledger row
#'
#' A fit rescales its covariates with fixed ranges and stores them in its ledger row (`covMinMax_spread`).
#' Prediction rescales with the same values, so it never depends on `sim$covMinMax_spread`, which
#' `fireSense_spreadFit` rebuilds in every run from that run's own covariate tables (in a hindcast, from a
#' formula rebuilt for another year, which can lack a fitted term).
#'
#' @param sa `sim$studyAreaWithSpreadParams`, one row per ELF.
#' @param i integer; the row.
#' @param default what to return for a row without ranges (an older ledger row): `sim$covMinMax_spread`.
#' @return The row's `covMinMax_spread` (a `data.table`, 2 rows: min, max), else `default`.
ledgerCovMinMax <- function(sa, i, default = NULL) {
if (!"covMinMax_spread" %in% names(sa)) return(default)
cmm <- sa[["covMinMax_spread"]][[i]]
if (is.null(cmm) || !NROW(cmm)) return(default)
cmm
}

#' The covariate centres of one ledger row
#'
#' A fit with an intercept centres its covariates and stores the centres in the ledger row
Expand Down
33 changes: 33 additions & 0 deletions tests/testthat/test-ledgerCovMinMax.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
## A single fitted ELF rescales its covariates with the ranges its ledger row stores
## (`covMinMax_spread`), not with `sim$covMinMax_spread`: fireSense_spreadFit rebuilds that object in
## every run from the run's own tables, and in a hindcast those come from a formula rebuilt for another
## year, so it need not have the fit's columns. The row's ranges are the fit's.

test_that("one ELF rescales with its ledger row's covMinMax_spread, not sim$covMinMax_spread", {
ins <- toyInputs()
ledger <- data.table::copy(ins$covMinMax_spread)
ledger$MDC <- c(100, 300)
ins$studyAreaWithSpreadParams$covMinMax_spread <- list(ledger)
## as in test-spreadProb-values.R "shifts as well as scales", but the range is in the row only:
## cell 4: MDC 200 -> (200 - 100) / 200 = 0.5 -> x = 0.75 + 1 = 1.75; cell 7: 150 -> 0.25 -> 0.375
v <- predVals(toyRun(ins))
expect_equal(v[c(4, 7)], handLogistic3(c(1.75, 0.375)), tolerance = 1e-7)
expect_false(isTRUE(all.equal(v, predVals(toyRun(toyInputs())))))
})

test_that("a row without ranges (an older ledger) falls back to sim$covMinMax_spread", {
expect_identical(predVals(toyRun(toyInputs())), predVals(toyRun(toyInputs())))
sa <- data.frame(polygonID = c("A", "B"))
cmm <- data.table::data.table(MDC = c(0, 200))
sa$covMinMax_spread <- list(cmm, NULL)
expect_identical(ledgerCovMinMax(sa, 1L, "default"), cmm)
expect_identical(ledgerCovMinMax(sa, 2L, "default"), "default")
expect_identical(ledgerCovMinMax(data.frame(polygonID = "A"), 1L, "default"), "default")
expect_null(ledgerCovMinMax(data.frame(polygonID = "A"), 1L))
})

test_that("a fitted covariate with no range stops instead of staying unscaled", {
ins <- toyInputs()
ins$covMinMax_spread$fuelA <- NULL
expect_error(toyRun(ins), "no range for the fitted covariate\\(s\\) fuelA")
})
Loading