Skip to content

Reject source grids that don't share a horizontal lattice (#547) - #548

Merged
amarcozzi merged 1 commit into
mainfrom
reject-mismatched-source-grid-lattices
Aug 26, 2026
Merged

Reject source grids that don't share a horizontal lattice (#547)#548
amarcozzi merged 1 commit into
mainfrom
reject-mismatched-source-grid-lattices

Conversation

@amarcozzi

Copy link
Copy Markdown
Contributor

Summary

Closes #547. irradiance.surface.relative (LeafLux) and fuel_moisture.dead.1hr (Fosberg) each derive an output by combining two source grids cell-for-cell inside a processing service (treevox, griddle). Both endpoints assumed the two grids already shared a horizontal lattice and passed the raw arrays straight through — so a mismatch (the common case: a fine 2 m LAD grid paired with a coarse 30 m terrain grid) surfaced only as an async worker crash:

ValueError: Terrain dimensions must match leaf area grid dimensions.
Leaf area is (715, 703) and terrain is (173, 141)

The job failed with UNEXPECTED_FAILURE and no actionable message. Confirmed against treevox-v2-prod logs (multiple occurrences). This was blocking end-to-end validation of the Fosberg 1-hr dead fuel moisture grid (#540).

Approach

Catch the mismatch synchronously at the API boundary with a clear 422, and keep the processing services' existing pre-aligned-input contract intact (no resampling pushed into treevox/griddle).

  • New shared validator validate_grids_share_horizontal_lattice in grids/utils.py: compares two grids' CRS (via lib.crs.crs_equal), trailing (y, x) shape, and the six affine-transform coefficients (abs tol 1e-9). A 3D grid may pair with a 2D grid — only the horizontal lattice must match. Returns 422 with resample remediation guidance.
  • LeafLux router — validates the terrain grid against the LAD grid; also scopes both source lookups to the domain (a cross-domain source grid now 404s, where the LAD lookup previously wasn't domain-scoped).
  • Fosberg router — validates the irradiance grid against the topography grid (whose georeference the output inherits).
  • Compose router — already enforced this inline; now delegates to the shared validator. Behavior-preserving: compose grids are always 2D (_shape_rank), so shape[-2:] equals the full shape.
  • OpenAPI field descriptions for both endpoints now state the same-lattice requirement and how to resample.

Validation runs before the output document is written or a task dispatched, so a rejected request creates no grid and dispatches no worker.

Tests

  • New validate_grids_share_horizontal_lattice unit tests (matching 3D/2D, equivalent CRS spellings, and CRS / shape / transform mismatches).
  • LeafLux + Fosberg router regression tests: aligned success, 3D-irradiance-with-matching-y/x success, cross-domain 404, and shape/transform/CRS mismatch → 422.
  • Compose validate-alignment tests extended for shape and transform.

Verified locally against a live API server: unit tests 28 passed; router tests (LeafLux + Fosberg + Compose) 86 passed; ruff check / ruff format --check clean.

Contract / downstream notes (Beta)

  1. API contract — two new synchronous rejections: mismatched-lattice terrain/irradiance now 422s (instead of a downstream async failure), and a cross-domain LeafLux source grid now 404s. Correct hardening; flagging as a conscious Beta break.
  2. SDKsilvxlabs/fastfuels-sdk-python#197 is held pending this fix; the SDK/docs should note that terrain/irradiance must share the consumer grid's lattice (422 otherwise). OpenAPI descriptions updated here.

LeafLux surface irradiance and Fosberg dead fuel moisture each derive an
output by combining two source grids cell-for-cell in a processing service
(treevox, griddle). Both assumed the grids were already on the same
horizontal lattice and passed the raw arrays straight through, so a mismatch
(e.g. a 2 m LAD grid paired with a 30 m terrain grid) surfaced only as an
async worker crash with an UNEXPECTED_FAILURE and no actionable message —
this was blocking end-to-end validation of the Fosberg grid (#540).

Add a shared `validate_grids_share_horizontal_lattice` in grids/utils that
compares two grids' CRS (semantically), trailing (y, x) shape, and affine
transform, returning 422 with resample guidance. A 3D grid may pair with a
2D grid; only the horizontal lattice must match. Apply it in the LeafLux and
Fosberg routers before the output document is created or a task dispatched,
and also scope both LeafLux source lookups to the domain (cross-domain
source now 404s). Compose already enforced this inline; it now delegates to
the shared validator (behavior-preserving — compose grids are always 2D).

The processing services keep their pre-aligned-input contract; the mismatch
is caught synchronously at the API boundary instead.

Fixes #547
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.

Leaflux surface irradiance fails when terrain grid dimensions differ from the LAD grid

1 participant