Implement modeler with grid domain support only#21
Conversation
emmanuelmathot
left a comment
There was a problem hiding this comment.
Nice work, Chuck — clean, well-documented, and the test coverage is excellent. Approving. Left two minor notes: one half-pixel CompactAxis question worth a tracking issue, and a small nit. Neither blocks merge.
|
|
||
| # CompactAxis describes a regular axis by its endpoints and cell count. | ||
| # x runs west->east; y runs north->south to match raster row order | ||
| # (row 0 is the north edge). Endpoints use the bounds edges; pixel-center |
There was a problem hiding this comment.
🟨 Reviewer-found (not from the original design notes).
CompactAxis uses the bounds edges as start/stop, not cell centers.
For num > 1 the axis is built as start=west, stop=east, num=width, so the generated coordinates are the outer bounds edges. In CoverageJSON a CompactAxis describes the coordinate values at which cells are defined (the cell centers), so this is off by half a pixel — e.g. 2 columns over -10..10 yields coords -10 and 10, but the actual cell centers are -5 and +5.
It is also inconsistent with the num == 1 path in _compact_axis, which does collapse to the midpoint (a true center).
The inline comment already flags this ("pixel-center offsets are a possible later refinement"), so I am fine deferring the fix in this PR — but could you please open a tracking issue so the half-pixel offset is not lost? Suggested scope: shift start/stop to cell centers (west + dx/2 … east - dx/2) and add a roundtrip test asserting cell-center coordinates. Reference this thread from the issue.
There was a problem hiding this comment.
Good catch. I thought I had caught that, but I must be think of something else I was working on with a compact axis (perhaps my own experimenting elsewhere).
| # No data axis may be empty: a zero-size height/width/sample axis is a | ||
| # degenerate coverage and would otherwise surface as an opaque CompactAxis | ||
| # error (num must be a positive cell count) deep in the modeler. | ||
| if 0 in self.data.shape: |
There was a problem hiding this comment.
🟨 Reviewer-found.
nit: 0 in self.data.shape also matches shape[0] == 0, already rejected above with a different message — so a (0, h, w) array reports "must be 2-D or 3-D" instead of "non-empty". Harmless; could narrow to data.shape[-2:] for a consistent message.
There was a problem hiding this comment.
This proposed change does not change the error a user sees, and actually makes the code harder to reason about because the slicing causes the reader to pause to grok the reason for the slicing, which is merely to avoid a potentially redundant check of shape[0] against 0. That's additional cognitive load that's not worth merely skipping an unnecessary check of a single element in the shape tuple for no change in behavior.
A CovJSON CompactAxis describes the coordinates at which cells are defined -- the cell centers -- but the Grid modeler passed the raster bounds edges straight through, leaving x/y coordinates off by half a cell. It was also inconsistent with the num == 1 path, which already collapsed to a true center. _compact_axis now insets the bounds edges by half a cell so the axis runs first + dx/2 .. last - dx/2. The unified formula naturally yields start == stop for a single cell, subsuming the former special case. Addresses reviewer feedback on PR #21. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @emmanuelmathot. I skipped the nit, but addressed the cell-centered compact axis suggestion. I preferred to keep it in this PR (not much to change) rather than splitting the logic to a separate tracking PR. |
Story 3 (PR 1): CoverageJSON modeler — Grid domain
Implements the modeler layer that converts a
CoverageInputinto a covjson-pydanticCoverage, scoped to the Grid domain (gridded rasters). Story 3 is split by domain type so each PR stays relatively small and independently reviewable; this is the first part. Part of #8; closes #17.What's included
modeler.py:to_coverage(coverage_input)and helpers, implemented as stateless module-level functions (no class). Builds the GridDomain(x/yCompactAxis), spatial referencing, oneParameterper band, and rangeNdArrays, reusing the existinghelpers.pyutilities. Non-gridgeometryand non-3-D data raiseNotImplementedErroruntil later PRs.input.py:CoverageInputnow resolvesbandsat construction (synthesizingb1, b2, ...when none are supplied), so consumers always read a populated tuple and the default-band convention lives in one place. Added domain-independent guards: reject empty data axes and duplicate band names (both become CoverageJSON keys and would otherwise fail opaquely downstream).tests/test_modeler.py(new) covers every Grid path (single/multi-band, float/int/string dtypes, nodata→null, UCUM unit resolution, synthesized bands, projected CRS, single-cell axis midpoint, and the two guards), all validated against the vendored OGC CoverageJSON schema; plus newCoverageInputvalidation/synthesis tests intests/test_input.py.docs/01,04,05,06updated to reflect the function-based modeler, the construction-time band resolution, and the Polygon-blocker correction (covjson-pydantic0.8.0 ships the Polygon domain types).Key design decisions
bandsresolved once, at construction:CoverageInputis the post-resolution representation; precedence/enrichment stays upstream in the converters. Rationale captured indocs/04section 3.xwest-east,ynorth-south (raster row 0 is north); a single-cell axis collapses to its bounds midpoint (CompactAxisrequiresstart == stopwhennum == 1).docs/04section 7), introduced when the second domain type lands.