Skip to content

Validate classified input and support arbitrary factor rasters in transitions #19

Open

Description

@NewGraphEnvironment

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:

  • source defaults to NULL in both dft_rast_transition() and dft_rast_break_class().
  • Labels resolve in this order: class_table, then an explicit source, then the union of the rasters' own factor levels (layer 1, active category). Otherwise it errors, naming dft_rast_classify() and terra::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, main labels a remap group Vegetation as Trees and labels non-IO codes NA. The branch fixes both.
  • The BULK published pair gives an identical 63-row summary on main and on the branch, whether labels come from the RAT or from source. The label path reads only metadata.

Open before a PR (details in planning/active/review-plan.md and review-round1.md on the branch):

  1. Cell codes that have no label still give silent NA rows. Error on any observed from/to code that has no label, in both functions and in the removed raster.
  2. 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 labels NA. Build the levels from the union of all inputs.
  3. One label on two different codes across rasters is accepted, so from_class != to_class reads a real change as stable. Refuse it.
  4. Regression: a character class_table$code fails with an opaque round() error.
  5. The unlabelled-raster message says "not a factor" for a zero-level factor, and its plural wording is wrong. Also: drop NA labels, test a resampled factor in break_class, and document that break_class reads 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 NA with 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:

  1. 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 via terra::set.cats().

  2. Read labels from the raster itself: Factor rasters already carry their class labels. Instead of requiring a class_table to decode integers, read the factor levels directly when available. Fall back to class_table only 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_table path 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 a class_table must be supplied even when the raster already carries the labels.
  • Downstream functions read the transition raster's levels by contract. dft_transition_vectors() and dft_transition_artifact() read terra::cats() — the id in the first column (terra's own contract) and the label in a column named transition (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

  1. Validate: non-factor input without a class_table is an error naming dft_rast_classify() and terra::set.cats().
  2. Read labels from factor levels when present and class_table is NULL; class_table still wins when supplied.
  3. 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions