Skip to content

Fix climateYear shadowing, stale climate rasters, categorical resampling and rstLCC setup - #13

Merged
eliotmcintire merged 6 commits into
developmentfrom
fix/init-and-climate-defects
Sep 21, 2026
Merged

eliotmcintire merged 6 commits into
developmentfrom
fix/init-and-climate-defects

Conversation

@eliotmcintire

@eliotmcintire eliotmcintire commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator

Merge #12 first; this branch already contains it.

Four confirmed behaviour defects, each fixed minimally and each with a regression test that fails on development and passes here. No dead-code removal, no refactoring: this is deliberately separate from #12.

A — climateYear had no effect (variable shadowing)

getCurrentClimate() computed currentYear from sim$climateYear (dev l.309-313), but the inner lapply FUN (dev l.322) declared a formal currentYear = time(sim), which shadowed it. Observed: climateYear = 2003 with start = 2001 returned the year2001 MDC layer (101, 102, 103...).

Fix: dropped the formal so the outer currentYear is used, and moved the computation above the guard. climateYear is compared numerically (currentYear > max(availableYears)), so its declared metadata class changed from "character" to "numeric" (it was never used as "year2009"-style text anywhere in the module), and time(sim)'s "unit" attribute is stripped.

B — covariate tables never advanced with simulation time (most serious)

The whole block was wrapped in if (is.null(sim$currentClimateRasters)) (dev l.296-334). currentClimateRasters is also a declared output (dev l.150), so it persisted across years and the block ran once. Observed: a 2001→2003 run produced an ignition table labelled year = 2003 carrying MDC = 103.5, 105.5, 111.5 — aggregated year2001 values. Silent.

Fix: a supplied currentClimateRasters is left alone. In the project the climateYear module supplies it every year. Only a raster this module built is refreshed, and only when the wanted year changes. The module records that it built the object, and for which year, in mod. A test supplies a different raster each year and checks that the covariate tables carry the supplied values. Another poisons the cached raster and re-runs the event in the same year, to check it is not rebuilt.

C — Init() destroyed categorical landcover on realignment

postProcess(sim$rstLCC_RTM, to = sim$rasterToMatch) (dev l.241-245) with no method= defaults to bilinear, on a categorical raster. Triggers whenever .compareRas() is FALSE. Observed: a misaligned 4x4 LCC produced 210, 114.5, 113.75, 66, 17.75..., and makeLandcoverDT() then returned an all-NA landcoverDT of the correct shape and column names — a silent failure.

Fix: method = "near".

D — both branches of the rstLCC_RTM setup were dead ends

.inputObjects() (dev l.509) branched on suppliedElsewhere("rstLCCs", sim), but rstLCCs was not a declared input: suppliedElsewhere returned TRUE while sim$rstLCCs was NULL, so tail(NULL, 1)[[1]] left rstLCC_RTM NULL and Init() died in LandR::.compareRas(). The other branch called makeFireSenseLCC(studyArea=, rasterToMatch=, ...), which the installed fireSenseUtils 0.2.3.9024 signature rejects (unused arguments). And dev l.546 defineFlammable(rstLCC, ...) referenced a local bound only inside the else branch.

Fix, in three parts:

  • rstLCCs declared as an input, class "list", matching fireSense_dataPrepFit (one SpatRaster per data year; its l.499 does defineFlammable(sim$rstLCCs[[dy]], ...)).
  • makeFireSenseLCC(maskTo = sim$studyArea, to = sim$rasterToMatch, ...), per args(fireSenseUtils::makeFireSenseLCC) and the sibling module's call.
  • the whole setup is now guarded by !suppliedElsewhere("rstLCC_RTM", sim) (the commented-out intent at dev l.508/533), and defineFlammable() derives its raster from sim$rstLCC_RTM using the same postProcess(..., method = "near") construction Init() uses, instead of the unbound local.

Open question for the owner

The rstLCCs interface is now tail(sim$rstLCCs, 1)[[1]] — the last element, as the existing code assumed. fireSense_dataPrepFit names its elements year<yyyy> and indexes them by year. If predict should pick the element matching P(sim)$dataYear rather than the last one, that is a behaviour decision I did not make; say so and I will change it. propFlammable is removed in the merge with #12, because nothing reads it.

Tests

tests/testthat/helper-toyPredict.R (4x4 in-memory terra landscape, MDC stack whose year<yyyy> layer is constant at yyyy - 1900, small data.tables, no network), plus test-getCurrentClimate.R, test-realignCategorical.R, test-inputObjectsRstLCC.R. test-metadata.R updated for the two metadata changes. All 20 assertions pass; each fails on development; each fix was individually reverted to confirm its test goes red.

eliotmcintire and others added 4 commits September 20, 2026 09:44
No behaviour change: only commented-out code, an if (FALSE) block, comments,
desc strings and Rmd text are touched. Several input descs had been passed
positionally into sourceURL; they are now in desc.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds tests for `ageNonForest()`, `getCurrentClimate()`, `Init()`/`init`,
`.inputObjects()` and the two covariate-assembly events, on a 4x4 toy
landscape whose every covariate can be worked out by hand. The toy climate
encodes both the year and the cell in its values, so an assertion on a value
proves which layer of which variable was taken.

99 assertions, no skips. Verified to pass identically on `origin/development`
and on this branch, run the way CI runs them (convertToPackage + test_local).
Every substantive test was mutation-checked: 14 mutations of the module
source, 14 detected.

Five defects are recorded, none of them blessed as correct: each states the
INTENDED assertion wrapped in `expect_failure()` so that fixing the defect
trips the test, plus an assertion pinning the current state.

- `.inputObjects` leaves `rstLCC_RTM` NULL: the `suppliedElsewhere("rstLCCs")`
  branch does `tail(sim$rstLCCs, 1)[[1]]`, but `rstLCCs` is not a declared
  input and so is absent. `Init()` then errors on `.compareRas(rtm, NULL)`,
  which makes the module unusable as shipped.
- `.inputObjects` cannot build `flammableRTM`: `defineFlammable(rstLCC, ...)`
  uses a local from the other branch and a function the module does not import.
- The covariate tables do not advance with simulation time: `getCurrentClimate()`
  only builds `currentClimateRasters` when it is NULL, so from year two on the
  climate is frozen at `start(sim)` while the `year` column still tracks
  `time(sim)` -- the table claims 2003 and carries 2001 climate.
- `climateYear` does not override `time(sim)`: the inner lapply's formal
  `currentYear = time(sim)` shadows the outer `currentYear`.
- `lightningDays` is NaN in the ignition table.
- `Init()` realigns landcover with bilinear `postProcess()`, averaging
  categorical class codes into non-classes and silently yielding an all-NA
  landcoverDT.
They are pinned observations of fireSenseUtils internals, identical on
origin/development and on this branch, not hand-derived truths.
…ing, rstLCC setup

A. getCurrentClimate(): the inner lapply FUN had a `currentYear = time(sim)`
   formal that shadowed the outer binding, so sim$climateYear never had any
   effect. Removed the formal so the outer value is used. `climateYear` is
   compared numerically, so its declared metadata class is now "numeric".

B. getCurrentClimate(): the build was guarded by
   `is.null(sim$currentClimateRasters)`, but currentClimateRasters is also a
   declared OUTPUT, so it persisted and the block ran only once: every year
   after the first silently used year-one climate. The guard is now keyed on
   the year (mod$currentClimateYear), preserving the within-year caching.

C. Init(): postProcess(rstLCC_RTM, to = rasterToMatch) used the bilinear
   default on a CATEGORICAL landcover raster, producing invented classes and
   an all-NA landcoverDT. Now method = "near".

D. .inputObjects(): `rstLCCs` was branched on but not declared as an input, so
   a user-supplied rstLCCs was NULL in the simList; it is now a declared input
   and the branch is guarded by !suppliedElsewhere("rstLCC_RTM"). The
   makeFireSenseLCC() call used studyArea=/rasterToMatch=, which the installed
   signature rejects; now maskTo=/to=. defineFlammable() referenced a local
   `rstLCC` bound only in the other branch; it now derives the raster from
   sim$rstLCC_RTM the same way Init() does.

Regression tests for each: tests/testthat/test-getCurrentClimate.R,
test-realignCategorical.R, test-inputObjectsRstLCC.R, with a shared
helper-toyPredict.R of in-memory terra/data.table inputs (no network).
@eliotmcintire

Copy link
Copy Markdown
Collaborator Author

On the open question about rstLCCs: keep tail(sim$rstLCCs, 1)[[1]], do not index by P(sim)$dataYear.

fireSense_dataPrepFit defines rstLCC_RTM the same way (fireSense_dataPrepFit.R:518), documented as conditions at start(sim), taken from the last layer. Its per-year indexing at :499 and :653 builds fit covariates for each data year, which is a different job. So tail() here reproduces what dataPrepFit would have passed over, and this branch only runs when predict is used without it.

Indexing by dataYear would also fail under the defaults: dataYear is 2011, and rstLCCs is named by dataYears (2000, 2010, 2020), so the lookup returns NULL.

…; add empty save event

Removed, never read: parameters .plotInitialTime, .plotInterval,
.saveInitialTime, .saveInterval; input fireSense_IgnitionFitted and its
check in .inputObjects, which only ran for a Fit module name.

whichModulesToPrepare now defaults to the three Predict modules that
doEvent tests for. It named fireSense_EscapeFit, which nothing tests for.

The save event emits a message and does nothing else.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013B4sgRg9EwyHaAQdDzUaQW
…Rasters alone

Merge #12 first; this branch now contains it.

Conflicts and how each was resolved:
- inputs metadata: #12's text and removals, with #13's climateYear class
  ("numeric") and the new rstLCCs input.
- getCurrentClimate() guard: #13's code. The two dead comments #12 removed
  stay removed.
- getCurrentClimate() assignment: #13's mod$currentClimateYear line, without
  the dead trailing comment.
- .inputObjects rstLCC block, both ends: #13's
  if (!suppliedElsewhere("rstLCC_RTM", sim)) wrapper.
- tests/testthat/test-getCurrentClimate.R: every test from both sides. #12's
  climateYear expect_failure pin is replaced by #13's test of the fix.

Correction to #13's stale-climate fix: the guard was keyed on the year alone,
so it rebuilt currentClimateRasters from projectedClimateRasters even when
another module had supplied it. In the project the climateYear module supplies
it every year, preferring historical rasters. Now a supplied object is left
alone, and only an object this module built is rebuilt when the year changes
(mod$builtCurrentClimate). New test: supplied every year, left untouched, and
the covariate tables carry the supplied values. It fails on #13 as pushed.

#12's expect_failure pins for the defects fixed here are now ordinary
expectations: rstLCC_RTM from rstLCCs, flammableRTM in .inputObjects,
nearest-neighbour landcover, covariates advancing each year. The toy helper's
rstLCC_RTM workaround is gone. The lightningDays pin stays; it is not fixed.

"the climate rasters are rebuilt when the year changes" now runs through
spades(), because mod only persists there.

Removed propFlammable (input and assignment): nothing reads it.
Version 1.0.4.9000.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013B4sgRg9EwyHaAQdDzUaQW
@eliotmcintire
eliotmcintire merged commit 1774a2d into development Sep 21, 2026
6 checks passed
@eliotmcintire
eliotmcintire deleted the fix/init-and-climate-defects branch September 21, 2026 04:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant