Skip to content

Commit 40fa429

Browse files
authored
Add northness and eastness functions (#1039) (#1041)
* Add northness and eastness functions (#1039) cos(aspect) and sin(aspect) wrappers for encoding aspect as linear variables suitable for regression and clustering. Flat cells map to NaN. * Add tests for northness and eastness (#1039) Covers correctness, cardinal directions, flat-to-NaN, NaN propagation, range checks, and backend parity (numpy, dask, cupy, dask+cupy). Also fixes CuPy compat by using np.where on raw data instead of xarray .where() method. * Add northness/eastness to docs and README feature matrix (#1039) * Add user guide notebook for northness and eastness (#1039)
1 parent c70cae4 commit 40fa429

File tree

7 files changed

+742
-0
lines changed

7 files changed

+742
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ write_vrt('mosaic.vrt', ['tile1.tif', 'tile2.tif']) # generate VRT
232232
| Name | Description | Source | NumPy xr.DataArray | Dask xr.DataArray | CuPy GPU xr.DataArray | Dask GPU xr.DataArray |
233233
|:----------:|:------------|:------:|:----------------------:|:--------------------:|:-------------------:|:------:|
234234
| [Aspect](xrspatial/aspect.py) | Computes downslope direction of each cell in degrees | Horn 1981 | ✅️ | ✅️ | ✅️ | ✅️ |
235+
| [Northness](xrspatial/aspect.py) | North-south component of aspect: cos(aspect) for linear models | Stage 1976 | ✅️ | ✅️ | ✅️ | ✅️ |
236+
| [Eastness](xrspatial/aspect.py) | East-west component of aspect: sin(aspect) for linear models | Stage 1976 | ✅️ | ✅️ | ✅️ | ✅️ |
235237
| [Curvature](xrspatial/curvature.py) | Measures rate of slope change (concavity/convexity) at each cell | Zevenbergen & Thorne 1987 | ✅️ |✅️ |✅️ | ✅️ |
236238
| [Hillshade](xrspatial/hillshade.py) | Simulates terrain illumination from a given sun angle and azimuth | GDAL gdaldem | ✅️ | ✅️ | ✅️ | ✅️ |
237239
| [Roughness](xrspatial/terrain_metrics.py) | Computes local relief as max minus min elevation in a 3×3 window | GDAL gdaldem | ✅️ | ✅️ | ✅️ | ✅️ |

docs/source/reference/surface.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,20 @@ Aspect
1111

1212
xrspatial.aspect.aspect
1313

14+
Northness
15+
=========
16+
.. autosummary::
17+
:toctree: _autosummary
18+
19+
xrspatial.aspect.northness
20+
21+
Eastness
22+
========
23+
.. autosummary::
24+
:toctree: _autosummary
25+
26+
xrspatial.aspect.eastness
27+
1428
Curvature
1529
=========
1630
.. autosummary::

examples/user_guide/35_Northness_Eastness.ipynb

Lines changed: 362 additions & 0 deletions
Large diffs are not rendered by default.
1.79 MB
Loading

xrspatial/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from xrspatial.aspect import aspect # noqa
2+
from xrspatial.aspect import eastness # noqa
3+
from xrspatial.aspect import northness # noqa
24
from xrspatial.balanced_allocation import balanced_allocation # noqa
35
from xrspatial.bilateral import bilateral # noqa
46
from xrspatial.contour import contours # noqa

xrspatial/aspect.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,3 +437,155 @@ def aspect(agg: xr.DataArray,
437437
coords=agg.coords,
438438
dims=agg.dims,
439439
attrs=agg.attrs)
440+
441+
442+
@supports_dataset
443+
def northness(agg: xr.DataArray,
444+
name: Optional[str] = 'northness',
445+
method: str = 'planar',
446+
z_unit: str = 'meter',
447+
boundary: str = 'nan') -> xr.DataArray:
448+
"""
449+
Computes the north-south component of aspect.
450+
451+
Returns ``cos(aspect)`` for each cell, ranging from +1 (due north)
452+
to -1 (due south). Flat cells (where ``aspect()`` returns -1) are
453+
set to NaN.
454+
455+
This is the standard way to encode aspect for use in regression,
456+
clustering, and other models that assume linear inputs. Raw aspect
457+
in degrees is circular (0 and 360 are the same direction), so
458+
feeding it directly into a linear model gives wrong results.
459+
460+
Parameters
461+
----------
462+
agg : xarray.DataArray or xr.Dataset
463+
2D elevation raster (NumPy, CuPy, Dask, or Dask+CuPy backed).
464+
If a Dataset is passed, the operation is applied to each
465+
data variable independently.
466+
name : str, default='northness'
467+
Name of output DataArray.
468+
method : str, default='planar'
469+
Passed to :func:`aspect`. ``'planar'`` or ``'geodesic'``.
470+
z_unit : str, default='meter'
471+
Passed to :func:`aspect`. Only used when ``method='geodesic'``.
472+
boundary : str, default='nan'
473+
Passed to :func:`aspect`. ``'nan'``, ``'nearest'``,
474+
``'reflect'``, or ``'wrap'``.
475+
476+
Returns
477+
-------
478+
northness_agg : xarray.DataArray or xr.Dataset
479+
Values in [-1, +1]. NaN where the input has NaN or where
480+
the surface is flat.
481+
482+
References
483+
----------
484+
Stage, A.R. (1976). "An Expression for the Effect of Aspect, Slope,
485+
and Habitat Type on Tree Growth." *Forest Science* 22(4): 457-460.
486+
487+
Examples
488+
--------
489+
.. sourcecode:: python
490+
491+
>>> import numpy as np
492+
>>> import xarray as xr
493+
>>> from xrspatial import northness
494+
495+
>>> data = np.array([
496+
[1, 1, 1, 1, 1],
497+
[1, 1, 1, 2, 0],
498+
[1, 1, 1, 0, 0],
499+
[4, 4, 9, 2, 4],
500+
[1, 5, 0, 1, 4],
501+
[1, 5, 0, 5, 5]
502+
], dtype=np.float32)
503+
>>> raster = xr.DataArray(data, dims=['y', 'x'])
504+
>>> north = northness(raster)
505+
"""
506+
asp = aspect(agg, name='_aspect', method=method, z_unit=z_unit,
507+
boundary=boundary)
508+
asp_data = asp.data
509+
trig = np.cos(np.deg2rad(asp_data))
510+
out = np.where(asp_data == -1, np.nan, trig)
511+
return xr.DataArray(out,
512+
name=name,
513+
coords=agg.coords,
514+
dims=agg.dims,
515+
attrs=agg.attrs)
516+
517+
518+
@supports_dataset
519+
def eastness(agg: xr.DataArray,
520+
name: Optional[str] = 'eastness',
521+
method: str = 'planar',
522+
z_unit: str = 'meter',
523+
boundary: str = 'nan') -> xr.DataArray:
524+
"""
525+
Computes the east-west component of aspect.
526+
527+
Returns ``sin(aspect)`` for each cell, ranging from +1 (due east)
528+
to -1 (due west). Flat cells (where ``aspect()`` returns -1) are
529+
set to NaN.
530+
531+
This is the standard way to encode aspect for use in regression,
532+
clustering, and other models that assume linear inputs. Raw aspect
533+
in degrees is circular (0 and 360 are the same direction), so
534+
feeding it directly into a linear model gives wrong results.
535+
536+
Parameters
537+
----------
538+
agg : xarray.DataArray or xr.Dataset
539+
2D elevation raster (NumPy, CuPy, Dask, or Dask+CuPy backed).
540+
If a Dataset is passed, the operation is applied to each
541+
data variable independently.
542+
name : str, default='eastness'
543+
Name of output DataArray.
544+
method : str, default='planar'
545+
Passed to :func:`aspect`. ``'planar'`` or ``'geodesic'``.
546+
z_unit : str, default='meter'
547+
Passed to :func:`aspect`. Only used when ``method='geodesic'``.
548+
boundary : str, default='nan'
549+
Passed to :func:`aspect`. ``'nan'``, ``'nearest'``,
550+
``'reflect'``, or ``'wrap'``.
551+
552+
Returns
553+
-------
554+
eastness_agg : xarray.DataArray or xr.Dataset
555+
Values in [-1, +1]. NaN where the input has NaN or where
556+
the surface is flat.
557+
558+
References
559+
----------
560+
Stage, A.R. (1976). "An Expression for the Effect of Aspect, Slope,
561+
and Habitat Type on Tree Growth." *Forest Science* 22(4): 457-460.
562+
563+
Examples
564+
--------
565+
.. sourcecode:: python
566+
567+
>>> import numpy as np
568+
>>> import xarray as xr
569+
>>> from xrspatial import eastness
570+
571+
>>> data = np.array([
572+
[1, 1, 1, 1, 1],
573+
[1, 1, 1, 2, 0],
574+
[1, 1, 1, 0, 0],
575+
[4, 4, 9, 2, 4],
576+
[1, 5, 0, 1, 4],
577+
[1, 5, 0, 5, 5]
578+
], dtype=np.float32)
579+
>>> raster = xr.DataArray(data, dims=['y', 'x'])
580+
>>> east = eastness(raster)
581+
"""
582+
asp = aspect(agg, name='_aspect', method=method, z_unit=z_unit,
583+
boundary=boundary)
584+
asp_data = asp.data
585+
trig = np.sin(np.deg2rad(asp_data))
586+
out = np.where(asp_data == -1, np.nan, trig)
587+
return xr.DataArray(out,
588+
name=name,
589+
coords=agg.coords,
590+
dims=agg.dims,
591+
attrs=agg.attrs)

0 commit comments

Comments
 (0)