Add from_template(): empty study-area DataArrays for common regions (#3484)#3487
Conversation
New xrspatial.templates module with from_template(name), exported at the top level. Resolves curated region names (conus -> Albers EPSG:5070, nyc, europe, world, ...) and ISO-3166 / GADM alpha-3 country codes (EPSG:4326) to an empty NaN-filled DataArray that obeys the array contract. Supports numpy, dask+numpy, cupy, and dask+cupy backends. Country bounding boxes derived from Natural Earth 50m admin_0 (public domain).
brendancol
left a comment
There was a problem hiding this comment.
PR Review: Add from_template() study-area grids
Reviewed from the PR head worktree. The change is additive (new module, data table, export, tests, docs, notebook) and does not touch existing code paths.
Blockers
None.
Suggestions
-
xrspatial/templates.py,_make_datadask+cupy branch:da.full(...).map_blocks(cupy.asarray)has nometa=, so dask infers meta by runningcupy.asarrayon a probe block while building the graph (an eager GPU call at graph-build time). Passmeta=cupy.empty((0, 0), dtype="float32")to skip the probe.
Nits
-
xrspatial/templates.py,_MAX_CELLSguard: the cap is right for numpy and cupy, which allocate eagerly, but it also rejects large lazy dask grids that would never materialize all at once. Fine as a conservative default; a one-line note on the constant saying it is deliberately backend-agnostic would save the next reader a question. -
xrspatial/tests/test_templates.py,test_downstream_slope_accepts_template: runningslopeon an all-NaN grid printsAll-NaN sliceRuntimeWarnings. Harmless (it is slope's own behavior on empty input), but wrapping the call innp.errstate/warnings.catch_warningswould keep the test output clean.
What looks good
- Antimeridian handling: country boxes for the wrap cases (USA, Russia, Fiji) stay contiguous instead of collapsing to a full -180..180 span. Verified against Natural Earth.
- Coordinate construction reuses
_make_output_coordsrather than re-deriving the linspace math, so pixel-center/north-up behavior matches the reproject path. - Error coverage is thorough: unknown name, non-positive resolution, over-fine resolution, non-string name, and bad backend each have a test and a specific message.
- Cross-backend parity (numpy/dask/cupy/dask+cupy) checks coords and attrs, not just that the call returns.
- The notebook executes end to end and the country grids are honestly documented as full-territory bounding boxes, not masks.
Checklist
- Output obeys the array contract (dims, coords, res, crs)
- All four backends produce consistent coords/attrs
- NaN fill is correct; dtype is float32
- Edge cases covered (errors, resolution bounds, fill)
- No premature materialization; data stays lazy on dask
- No benchmark added (acceptable: the helper is a single allocation)
- README feature matrix updated
- Docstrings present and accurate
brendancol
left a comment
There was a problem hiding this comment.
Follow-up review (commit f1e7deb)
All three findings from the previous pass are addressed:
- Suggestion (dask+cupy meta):
_make_datanow passesmeta=cupy.empty((0, 0), dtype="float32")tomap_blocks, so the graph builds without the eager probe call. Verifiedagg.data._metais a cupy array. - Nit (_MAX_CELLS): added a comment noting the cap is applied uniformly across backends, dask included, on purpose.
- Nit (test warnings):
test_downstream_slope_accepts_templatenow wraps theslopecall innp.errstate+catch_warnings, so the suite runs clean (22 passed, no warnings).
No new issues. flake8 clean on the changed files. Nothing else outstanding.
) preserve='area' returns an equal-area EPSG (curated national code, e.g. 5070/3035, or EPSG:8857 Equal Earth fallback); preserve='shape' returns a conformal EPSG (the centroid's UTM/UPS zone, with curated overrides for europe/world). attrs['crs'] stays an int EPSG. Reuses _edge_samples/ _transform_boundary to project the lon/lat bbox and query_utm_crs_info for the zone; requires pyproj. distance/direction are not offered (no EPSG-coded equidistant/azimuthal projections exist).
brendancol
left a comment
There was a problem hiding this comment.
Review: preserve= extension (EPSG-coded projection by property)
Reviewed the preserve commits on top of the base feature. Additive: a new keyword-only arg plus supporting data, no change to the default code path.
Blockers
None.
Suggestions
None outstanding. One issue found during review and fixed in 7362493: from_template("world", preserve="shape") produced a degenerate 80x970 grid at ~500 km cells, because World Mercator's y-extent diverges toward the poles and inflated the default resolution. Clamping the world template's preserve-path latitude band to +/-85 (the conventional Web Mercator limit) yields a sensible 797x802 grid; the default (non-preserve) world grid still spans the full +/-90 in EPSG:4326.
Nits
xrspatial/templates.py_default_preserve_resolution: snaps up to the next {1,2,5}x10^k, so a 5.86 Mm extent lands on 10 km cells (~586 cells) rather than ~1000. Intentional and documented as "~1000 cells"; fine to leave.- Wide or overseas-spanning country codes (USA, RUS, FRA-with-overseas) get a single UTM zone for
shapeor a Greenwich-centered Equal Earth forarea, both of which distort across the extent. This is inherent to EPSG-coded global/zonal projections and is called out in the notebook's info box; curated regions remain the better canvas.
What looks good
attrs['crs']stays an int EPSG for everypreserveresult, consistent with the base feature and the array contract.- Reuses
_edge_samples+_transform_boundaryto project the lon/lat bbox andpyproj.database.query_utm_crs_infofor the zone, rather than reimplementing projection math. - Property correctness is asserted, not assumed: tests check
pyproj.CRS(...).to_dict()['proj']is the equal-area / conformal family, plus exact codes (conus 5070, JPN UTM 32653, Europe LCC 3034, Equal Earth 8857 fallback). - Hemisphere and polar handling verified: southern centroids -> 327xx,
|lat|>84-> UPS 5041/5042. distance/directionare rejected with a message explaining no EPSG-coded equidistant/azimuthal projection exists, rather than silently synthesizing one.- Antimeridian country codes (USA, RUS, FJI) build under both properties.
- Backends (numpy/dask/cupy/dask+cupy) keep coord/attr parity with
preserveset; notebook re-executes end to end.
Checklist
- EPSG resolution correct per property (area/shape), verified by projection family + exact codes
- CRS stored as int EPSG; units "m"
- Hemisphere / polar / antimeridian paths exercised
- Invalid values (distance/direction/bogus) raise a clear error
- Backends consistent with preserve set
- No benchmark (acceptable: a single allocation + one pyproj bbox transform, not a hot loop)
- README + docstring + notebook updated
Closes #3484
Adds a
templatesmodule with afrom_template(name)helper, exported at the top level (from xrspatial import from_template). It returns an empty (NaN-filled, float32) DataArray for a named study area that follows the xarray-spatial array contract: 2-D['y', 'x'], pixel-center coordinates (north-up), andattrscarryingresandcrs. The grid covers the area's rectangular bounding box and is a starting canvas for a new analysis.conus(Albers EPSG:5070),nyc,alaska,hawaii,california,europe,world.from_template("FRA"). The country bounding boxes come from Natural Earth 50m admin_0 (public domain) and are committed as a static table, so there is no runtime network access or geopandas dependency. Countries that cross the antimeridian (Russia, Fiji) keep contiguous bounding boxes.resolutionsets cell size in the template's CRS units, with a per-template default. An over-fine resolution is rejected before anything is allocated.Coordinate construction reuses the existing
_make_output_coordshelper from the reproject module instead of re-deriving the math.Backends: numpy, dask+numpy, cupy, dask+cupy. The data is a NaN fill, so each backend allocates its own array type and coords/attrs match across all four.
Test plan
slopeaccepts a template