Repository navigation
Validate classified input and support arbitrary factor rasters in transitions #19
Description
Status (parked 2026-09-30)
Branch 19-validate-classified-input-and-support-ar is pushed (WIP commit c84dde8). All tests pass, but there is no PR yet.
Decided at the plan gate:
sourcedefaults toNULLin bothdft_rast_transition()anddft_rast_break_class().- Labels resolve in this order:
class_table, then an explicitsource, then the union of the rasters' own factor levels (layer 1, active category). Otherwise it errors, namingdft_rast_classify()andterra::set.cats(). dft_rast_break_class()shares the one internal helper,transition_class_table(), so it stays identical to the two-epoch transition.- Class codes must be whole numbers in 0-999.
Measured:
- On the bundled tile,
mainlabels a remap groupVegetationasTreesand labels non-IO codesNA. The branch fixes both. - The BULK published pair gives an identical 63-row summary on
mainand on the branch, whether labels come from the RAT or fromsource. The label path reads only metadata.
Open before a PR (details in planning/active/review-plan.md and review-round1.md on the branch):
- Cell codes that have no label still give silent
NArows. Error on any observed from/to code that has no label, in both functions and in the removed raster. - Regression:
dft_rast_consensus()copies only the first input's levels, so a code that wins the vote but is absent from year 1 now labelsNA. Build the levels from the union of all inputs. - One label on two different codes across rasters is accepted, so
from_class != to_classreads a real change as stable. Refuse it. - Regression: a character
class_table$codefails with an opaqueround()error. - The unlabelled-raster message says "not a factor" for a zero-level factor, and its plural wording is wrong. Also: drop
NAlabels, test a resampled factor inbreak_class, and document thatbreak_classreads every year's levels.
Follow-up: #95 (map colours still come from source).
Problem
dft_rast_transition() silently produces wrong results when passed raw integer rasters that weren't classified via dft_rast_classify(). The code_lookup maps integer codes to class names using the default class table, so:
- If codes happen to match (e.g., IO LULC codes), results look correct but the user never explicitly classified
- If codes don't match (custom rasters, climate data, habitat types), class names are
NAwith no error
As drift generalizes beyond land cover (climate zones, habitat classification, soil types, any categorical raster), this becomes more likely. A user with a pre-classified raster from another source has no reason to run dft_rast_classify() first.
Proposed Solution
Two complementary changes:
-
Validate input is factor: If the input SpatRasters are not factor rasters, warn or error with a message pointing to
dft_rast_classify()or explaining how to set factors manually viaterra::set.cats(). -
Read labels from the raster itself: Factor rasters already carry their class labels. Instead of requiring a
class_tableto decode integers, read the factor levels directly when available. Fall back toclass_tableonly for raw integer rasters. This makes the function work with any pre-classified categorical raster regardless of source.
Option 2 is the bigger unlock — it means dft_rast_transition() works out of the box with any factor raster, not just drift-classified ones. A user with climate zone rasters from another package could compute transitions directly.
What is already true (revised 2026-09-05)
- The
class_tablepath already works for arbitrary codes.dft_rast_transition(x, class_table = <tibble with code, class_name, color>)decodes any integer coding; drift#44's test fixtures run it against a synthetic 4-class table with no IO LULC anywhere. Option 2's "read labels from the raster itself" is therefore about convenience for factor inputs, not a capability gap — the remaining gap is that a factor raster's own levels are ignored and aclass_tablemust be supplied even when the raster already carries the labels. - Downstream functions read the transition raster's levels by contract.
dft_transition_vectors()anddft_transition_artifact()readterra::cats()— the id in the first column (terra's own contract) and the label in a column namedtransition(drift's). Whatever this issue does to how labels get in must keep that shape. dft_check_crs()already refuses lon/lat; the factor check proposed here belongs next to it.
Scope, revised
- Validate: non-factor input without a
class_tableis an error namingdft_rast_classify()andterra::set.cats(). - Read labels from factor levels when present and
class_tableisNULL;class_tablestill wins when supplied. - Unblocks Label index-trajectory breaks with from-to land-cover classes #31.
Context
Discovered during code review of #14. The current code works because the test data and typical pipeline always use IO LULC codes with the matching default class table. The gap only surfaces with non-standard inputs.
Activity