Skip to content

Add from_template(): empty study-area DataArrays for common regions (#3484)#3487

Merged
brendancol merged 10 commits into
mainfrom
issue-3484
Jun 25, 2026
Merged

Add from_template(): empty study-area DataArrays for common regions (#3484)#3487
brendancol merged 10 commits into
mainfrom
issue-3484

Conversation

@brendancol

Copy link
Copy Markdown
Contributor

Closes #3484

Adds a templates module with a from_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), and attrs carrying res and crs. The grid covers the area's rectangular bounding box and is a starting canvas for a new analysis.

  • Curated regions resolve to a projected CRS: conus (Albers EPSG:5070), nyc, alaska, hawaii, california, europe, world.
  • ISO-3166 / GADM alpha-3 country codes (240 of them) resolve to an EPSG:4326 bounding-box grid, e.g. 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.
  • resolution sets 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_coords helper 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

  • Contract: dims, coords, res tuple, crs int, units
  • Curated regions (conus Albers, nyc, case-insensitive) and country codes (EPSG:4326)
  • Resolution control, fill/dtype, error cases (unknown name, non-positive resolution, over-fine resolution, non-string, bad backend)
  • All four backends with coord/attr parity
  • Downstream smoke: slope accepts a template
  • User guide notebook executes end to end
  • flake8 clean on new files

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 brendancol left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_data dask+cupy branch: da.full(...).map_blocks(cupy.asarray) has no meta=, so dask infers meta by running cupy.asarray on a probe block while building the graph (an eager GPU call at graph-build time). Pass meta=cupy.empty((0, 0), dtype="float32") to skip the probe.

Nits

  • xrspatial/templates.py, _MAX_CELLS guard: 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: running slope on an all-NaN grid prints All-NaN slice RuntimeWarnings. Harmless (it is slope's own behavior on empty input), but wrapping the call in np.errstate/warnings.catch_warnings would 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_coords rather 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 brendancol left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up review (commit f1e7deb)

All three findings from the previous pass are addressed:

  • Suggestion (dask+cupy meta): _make_data now passes meta=cupy.empty((0, 0), dtype="float32") to map_blocks, so the graph builds without the eager probe call. Verified agg.data._meta is 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_template now wraps the slope call in np.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 brendancol left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 shape or a Greenwich-centered Equal Earth for area, 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 every preserve result, consistent with the base feature and the array contract.
  • Reuses _edge_samples + _transform_boundary to project the lon/lat bbox and pyproj.database.query_utm_crs_info for 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/direction are 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 preserve set; 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

@brendancol brendancol merged commit e6e05d6 into main Jun 25, 2026
12 checks passed
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.

Add from_template(): empty study-area DataArrays for common regions

1 participant