API Reference
The public API is the two accessors plus the function behind them.
import xcorduroy registers .dem on DataArray and Dataset; in most cases
that is all you need, and compute_terrain is the escape hatch for calling the
machinery directly.
Terrain modes (Slope, Aspect, Hillshade) are frozen dataclasses carrying the
output's name, units and long_name, and in the hillshade case the light
position. They are what compute_terrain dispatches on.
The kernel
All three methods come from one function, _terrain_kernel in
src/xcorduroy/DEM.py. It takes a 1-pixel-padded 2D elevation array and returns
an array of the same shape as the unpadded input.
Gradients use the Horn (1981) 3x3 kernel — a weighted finite difference over the eight neighbours, with the centre-row and centre-column neighbours weighted 2x:
and the transpose for dz/drow. This is what gdaldem uses, which is why the
outputs match it.
Array orientation
The kernel works in array index space, but slope and aspect are defined in world
space. compute_terrain takes the signed median spacing of each coordinate axis
and passes x_sign / y_sign into the kernel, which flips the gradients so they
are positive eastward and northward regardless of how the raster is stored. A
north-up raster (descending y) and a south-up one give the same answer.
Longitude is differenced with a dateline guard: a step larger than 180 degrees is wrapped, so an array crossing the antimeridian does not read as one enormous cell.
Conventions
| Output | Range | Notes |
|---|---|---|
slope |
0–90 degrees | atan(hypot(dz_dx, dz_dy)) |
aspect |
0–360 degrees | Downslope direction, clockwise from north. Flat cells are NaN. |
hillshade |
0–1 | Clipped, float32 |
Hillshade is the standard Lambertian term:
with zenith = 90 - altitude, and azimuth converted from compass degrees to the
counter-clockwise-from-east convention that arctan2 returns.
Why flat cells are NaN
A perfectly flat cell has no downslope direction. arctan2(0, 0) returns 0,
which would report every flat cell as facing due north — visible as a false wall
of colour across hydro-flattened lakes and plateaus. xcorduroy returns NaN
there instead, matching gdaldem. Slope and hillshade are unaffected: a flat cell
has zero slope and takes the illumination of a horizontal surface.
Degrees to metres
Elevation is in metres, but a geographic CRS gives coordinate spacing in degrees. Dividing one by the other is meaningless, so for a geographic CRS the spacing is scaled to metres before the gradient is taken:
111320 is the WGS84 mean metres per degree of latitude. Using the mean
latitude of the array is the approximation: the true longitude scale varies across
the array, and the error grows with its north–south extent. For a tile this is
negligible; for a continental extent it is not, and the right answer is to
reproject to an equal-area or local projected CRS first.
Near a pole cos(lat) collapses toward zero, which would make every cell read as
a cliff. The scaling is floored at its 89.9-degree value and a RuntimeWarning is
raised.
An explicit resolution= bypasses all of this and is taken in elevation units.
Chunked arrays
A 3x3 kernel needs one cell of context beyond each chunk boundary. For dask-backed
input the kernel is applied through map_overlap with depth=1,
boundary="nearest" and trim=False: dask grows each chunk by one cell on every
side, the kernel consumes that halo, and the output chunk comes back the same
shape as the input chunk. Seam values are therefore identical to the in-memory
result — tests/test_DEM.py::test_dask_chunk_seams asserts exactly that.
The in-memory path does the same thing with np.pad(..., mode="edge"), so the two
code paths agree at array edges as well as at chunk seams.
NaN handling
NaN is not treated as nodata; it propagates. A single NaN cell in the input
produces a 3x3 block of NaN in the output: every neighbour of that cell had it
in its stencil, and the centre cell is masked explicitly, since the Horn kernel
gives it weight 0 and would otherwise return a finite, meaningless value. Mask or fill nodata before calling if that is not what you
want.
Testing
Beyond unit tests against hand-derived analytic expectations, tests/test_hypothesis.py
asserts properties that must hold for any input: a flat surface has zero slope, a
linear ramp has constant slope, slope is invariant under translation and rotation
of the elevation field, aspect stays in 0–360, hillshade stays in 0–1, and halving
the resolution doubles the gradient.
Signatures
xcorduroy.DEMDataArrayAccessor
Xarray accessor for terrain analysis on DataArrays.
Provides methods to compute slope, aspect, and hillshade from elevation data. Access via da.dem.slope(), da.dem.aspect(), or da.dem.hillshade().
crs
property
Get coordinate reference system from DataArray.
Returns:
-
Any–CRS object from xproj accessor
Raises:
-
ValueError–If no CRS is found
aspect
aspect(
x: str | None = None,
y: str | None = None,
resolution: Resolution = None,
**kwargs: Any,
) -> DataArray
Compute aspect: downslope direction in degrees clockwise from north (0-360). Flat cells are NaN.
Parameters:
-
x(str | None, default:None) –X dimension name (auto-detected if None)
-
y(str | None, default:None) –Y dimension name (auto-detected if None)
-
resolution(Resolution, default:None) –Cell size or (y_res, x_res) in elevation units. Derived from coordinates if None (degrees converted to metres for geographic CRSs)
-
**kwargs(Any, default:{}) –Additional arguments (e.g., z_factor vertical exaggeration)
Returns:
-
DataArray–DataArray with aspect values in degrees
hillshade
hillshade(
x: str | None = None,
y: str | None = None,
resolution: Resolution = None,
azimuth: float = 315.0,
altitude: float = 45.0,
**kwargs: Any,
) -> DataArray
Compute hillshade (0-1 shaded relief).
Parameters:
-
x(str | None, default:None) –X dimension name (auto-detected if None)
-
y(str | None, default:None) –Y dimension name (auto-detected if None)
-
resolution(Resolution, default:None) –Cell size or (y_res, x_res) in elevation units. Derived from coordinates if None (degrees converted to metres for geographic CRSs)
-
azimuth(float, default:315.0) –Light source direction in degrees (0-360)
-
altitude(float, default:45.0) –Light source angle above horizon in degrees (0-90)
-
**kwargs(Any, default:{}) –Additional arguments (e.g., z_factor vertical exaggeration)
Returns:
-
DataArray–DataArray with hillshade values (0-1)
slope
slope(
x: str | None = None,
y: str | None = None,
resolution: Resolution = None,
**kwargs: Any,
) -> DataArray
Compute slope in degrees.
Parameters:
-
x(str | None, default:None) –X dimension name (auto-detected if None)
-
y(str | None, default:None) –Y dimension name (auto-detected if None)
-
resolution(Resolution, default:None) –Cell size or (y_res, x_res) in elevation units. Derived from coordinates if None (degrees converted to metres for geographic CRSs)
-
**kwargs(Any, default:{}) –Additional arguments (e.g., z_factor vertical exaggeration)
Returns:
-
DataArray–DataArray with slope values in degrees
xcorduroy.DEMDatasetAccessor
Xarray accessor for terrain analysis on Datasets.
Auto-detects elevation variable or access specific variables via ds.dem('var_name'). Provides methods to compute slope, aspect, and hillshade.
__call__
__call__(name: str | None = None) -> DEMDataArrayAccessor
Get DEM accessor for a specific variable or auto-detect elevation variable.
Parameters:
-
name(str | None, default:None) –Variable name or None to auto-detect
Returns:
-
DEMDataArrayAccessor–DEMDataArrayAccessor for the selected variable
Raises:
-
ValueError–If multiple candidate variables found
-
AttributeError–If no suitable elevation variable found
aspect
Compute aspect. See DEMDataArrayAccessor.aspect for details.
hillshade
Compute hillshade. See DEMDataArrayAccessor.hillshade for details.
xcorduroy.compute_terrain
compute_terrain(
da: DataArray,
mode: ModeType,
resolution: float | int | tuple[float, float] | None = None,
crs: Any = None,
x_dim: str = "x",
y_dim: str = "y",
**kwargs: Any,
) -> DataArray
Compute terrain analysis from a 2D elevation DataArray.
Parameters:
-
da(DataArray) –Input elevation DataArray with dims
y_dimandx_dim -
mode(ModeType) –Terrain mode (Slope, Aspect, or Hillshade instance)
-
resolution(float | int | tuple[float, float] | None, default:None) –Cell size as scalar or (y_res, x_res) in elevation units. Derived from coordinates if None. For geographic CRSs the degree spacing is converted to metres using the mean latitude; within 0.1 degrees of a pole that scaling is clamped and a
RuntimeWarningis raised. -
crs(Any, default:None) –pyproj-like CRS with
is_geographic. Used for degree-to-metre scaling -
x_dim(str, default:'x') –Name of x dimension
-
y_dim(str, default:'y') –Name of y dimension
-
**kwargs(Any, default:{}) –Passed to the kernel (e.g.
z_factorvertical exaggeration)
Returns:
-
DataArray–DataArray with computed terrain values
xcorduroy.types.TerrainMode
dataclass
Bases: ABC
Base class for all terrain analysis methods.
xcorduroy.types.Slope
dataclass
Bases: TerrainMode
xcorduroy.types.Aspect
dataclass
Bases: TerrainMode
xcorduroy.types.Hillshade
dataclass
Hillshade(
azimuth: float = 315.0,
altitude: float = 45.0,
name: str = "hillshade",
units: str = "0-1",
long_name: str = "hillshade",
)
Bases: TerrainMode