Skip to content

Fix large-floodplain OOM in LULC transition + vectorize (terra-native, changes_only) — v0.4.0#35

Merged
NewGraphEnvironment merged 10 commits into
mainfrom
34-lulc-transition-classify-ooms-on-large-f
Jul 9, 2026
Merged

Fix large-floodplain OOM in LULC transition + vectorize (terra-native, changes_only) — v0.4.0#35
NewGraphEnvironment merged 10 commits into
mainfrom
34-lulc-transition-classify-ooms-on-large-f

Conversation

@NewGraphEnvironment

Copy link
Copy Markdown
Owner

Summary

Fixes the remaining OOM class in the categorical land-cover change pipeline for large-floodplain AOIs (#34, closing #28). #27 fixed the vectorizer's per-class loop; this fixes the two drivers that remained. Both fixes are behavior-preserving:

  • dft_rast_transition() rewritten to stream entirely through terra — transitions are encoded/filtered with raster arithmetic, terra::subst(), patches(), and a single terra::freq(), with no terra::values() pull and no full-grid R vectors (previously 6+, incl. two full-grid character vectors). Peak memory is now O(#codes + #patches), not O(ncell). Byte-identical output, verified by a golden snapshot across the full parameter matrix plus an independent old-vs-new comparison (incl. ESA WorldCover 3-digit codes). Producer-only peak at 16M cells: 2.66 GB → 1.63 GB.
  • dft_transition_vectors() gains opt-in changes_only (default FALSE): drop stable (from == to) patches at the raster level before as.polygons, so only actual change patches are polygonized. Plus a small-patch raster pre-filter when patch_area_min is set (trailing st_area filter kept → identical output). On a fragmented NECR-like synthetic (9M cells / 415k patches, where as.polygons dominates): 3.83 GB → 1.71 GB (55% cut). changes_only=TRUE equals the default output filtered to change patches (proven by test).

Released as v0.4.0.

Two terra gotchas handled

  • SpatRaster %in% is not dispatched when terra is imported (not attached) → use terra::subst().
  • terra::freq() errors on an all-NA raster (does not return 0 rows) → guarded with tryCatch.

Follow-up (not in this PR — drift-only decision)

NECR completes end-to-end only once the floodplains caller opts in: pass changes_only = TRUE and drop the post-hoc from != to filter in scripts/floodplain_lcc/03_lulc_classify.R. Left as a separate floodplains change.

Related Issues

Test plan

  • devtools::test() — 314 pass / 4 skip (network/bfast tests skip)
  • R CMD check — 0 errors / 0 warnings / 0 notes
  • lintr::lint_package() — clean
  • Golden snapshot proves dft_rast_transition byte-identical across the parameter matrix
  • Pinned vectorizer regression (185 / 123.11 ha / 57 at patch_area_min=1000) unchanged
  • /code-check round 1 clean (one pre-existing empty-return fragility found + fixed)

🤖 Generated with Claude Code

https://claude.ai/code/session_01DKqnW6Fni3tc44Lo5D5fJz

NewGraphEnvironment and others added 10 commits July 9, 2026 10:43
data-raw/benchmark_transition_oom.R: self-checking assertions for the terra ops
the rewrite rests on (factor-strip via *1L with no input mutation, NA propagation,
%in% FALSE-at-NA, freq value==code, empty-RHS %in% guard, classify/subst, set.cats
on arithmetic raster) -- all pass on terra 1.9.11. Synthetic profiling confirms the
two-driver diagnosis: on a 4.0M-cell coherent field, 89% of transition patches are
stable X->X (polygonized then discarded by the caller), motivating changes_only.

Relates to NewGraphEnvironment/sred#16

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DKqnW6Fni3tc44Lo5D5fJz
terra writes a <raster>.tif.aux.xml stats sidecar whenever the transition tests or
the OOM benchmark read the bundled fixtures; ignore so it stops surfacing as an
untracked stray.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DKqnW6Fni3tc44Lo5D5fJz
Snapshot the full behavior (summary tibble + raster factor levels/frequencies +
removed raster) across 8 parameter combos as a canonicalized, order-independent
digest via expect_snapshot_value(style=serialize). This freezes the pre-rewrite
output so the terra-native rewrite in Phase 3 can be proven byte-identical. Empty
case (impossible filter) is normalized: cats()[[1]] is NULL and freq() errors on
an all-NA raster.

Relates to NewGraphEnvironment/sred#16

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DKqnW6Fni3tc44Lo5D5fJz
…ctors)

Rewrite the producer to eliminate the OOM class (#28): strip factor to code
rasters via `* 1L`, encode from*1000+to by raster arithmetic, filter from/to
classes by integer code-set membership (terra::subst), remove small patches with
patches/subst/ifel, and derive codes + total_valid + summary from a single native
terra::freq. No terra::values(), no rep(NA, ncell) -- peak R memory is now
O(#codes + #patches), not O(ncell).

Behavior-identical: the Phase-2 golden snapshot stays byte-for-byte; full suite
303 pass / 4 skip; /code-check round 1 clean (independent old-vs-new comparison
byte-identical across 14 param combos incl. ESA WorldCover 3-digit codes).
Producer-only peak RSS at 16M cells (patch_area_min=500): 2.66 GB -> 1.63 GB.

Two terra gotchas handled: SpatRaster `%in%` is not dispatched when terra is
imported (not attached) -> use terra::subst; terra::freq() errors on an all-NA
raster -> guard with tryCatch.

Relates to NewGraphEnvironment/sred#16

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DKqnW6Fni3tc44Lo5D5fJz
…tors (#34)

Cap the vectorizer working set for large floodplains. changes_only=TRUE drops
stable (from==to) patches at the raster level (subst+mask) before as.polygons,
so only actual change patches are polygonized -- the field OOM lever, since the
producer's transition raster is non-NA over the whole floodplain (stable mosaic)
and the caller discards stable patches anyway. NA-ing stable cells can't merge or
split any change patch (stable neighbours were already boundaries), so the result
equals the default filtered to non-stable rows -- proven by test.

Also pre-filter small patches (freq+subst small pids -> NA) before as.polygons
when patch_area_min is set, keeping the trailing st_area filter so output stays
byte-identical (pinned 185 / 123.11 ha / 57 unchanged).

Fixed a pre-existing empty-return schema gap surfaced by /code-check: the early
return dropped the zone column, which changes_only makes reachable (an all-stable
sub-basin) -> per-sub-basin bind_rows mismatch. Empty return now carries zone_col.

Fragmented 9M-cell benchmark (as.polygons-dominated, NECR-like geometry): default
415,705 patches / 3.83 GB -> changes_only 44,537 patches / 1.71 GB (55% peak cut).
Full suite 314 pass / 4 skip; lint clean.

Relates to NewGraphEnvironment/sred#16

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DKqnW6Fni3tc44Lo5D5fJz
NEWS entry for the OOM fixes (producer terra-native rewrite; vectorizer
changes_only + small-patch pre-filter). Document the drift-only decision's
follow-up: the floodplains caller passes changes_only=TRUE and drops its
post-hoc from!=to filter so NECR completes end-to-end (separate floodplains PR).

Relates to NewGraphEnvironment/sred#16

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DKqnW6Fni3tc44Lo5D5fJz
Memory-scalable categorical land-cover change detection (#34, #28): terra-native
dft_rast_transition (no full-grid R vectors) + dft_transition_vectors changes_only
and small-patch pre-filter. Behavior-preserving; R CMD check clean (0/0/0).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DKqnW6Fni3tc44Lo5D5fJz
@NewGraphEnvironment NewGraphEnvironment merged commit 073da37 into main Jul 9, 2026
1 check passed
@NewGraphEnvironment NewGraphEnvironment deleted the 34-lulc-transition-classify-ooms-on-large-f branch July 9, 2026 18:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant