Skip to content

API reference

High-level entry points are exported from the top-level geohalo package. Each workflow comes in a convenience form (builds the operator for you) and a precomputed form (you pass a cached object).

Convenience Precomputed Builds
reduce reduce_with_stencil / reduce_with_operator Stencil / ReduceOperator
resample_grid resample_grid_with_matrix Resampler
aggregate_bias aggregate_bias_with_tree BiasTree

Reduction

geohalo.reduce

reduce(
    grid: T,
    geoms: GeoSeries,
    *,
    target_resolution: float | None = None,
    resample_iterations: int = 1,
    spherical_correction: bool = True,
    lat_dim: str = "latitude",
    lon_dim: str = "longitude",
    geom_dim: str = "geom",
    weight_key: str | None = None,
    how: Literal["mean", "sum"] = "mean",
    preserve_dtype: bool = False,
    period: float | None = None,
) -> T

Build a float64 stencil and reduce polygons in caller order.

preserve_dtype=True retains each floating input variable's result dtype; integer means remain floating. For float32 coefficients, prebuild a stencil or operator with dtype=np.float32 and use its corresponding reducer. period=360 enables cyclic longitude resampling and generates a full cycle when target_resolution is set. Geometries themselves do not wrap.

geohalo.reduce_with_stencil

reduce_with_stencil(
    grid: T,
    stencil: Stencil,
    *,
    resample_iterations: int = 1,
    lat_dim: str = "latitude",
    lon_dim: str = "longitude",
    geom_dim: str = "geom",
    weight_key: str | None = None,
    how: Literal["mean", "sum"] = "mean",
    preserve_dtype: bool = False,
    period: float | None = None,
) -> T

Reduce in stencil row order, automatically handling NaNs and cell weights.

preserve_dtype=True retains each floating input variable's result dtype; integer means remain floating. Clean fusion uses the stencil's coefficient dtype. Masked/weighted normalization retains its existing arithmetic precision. period=360 wraps resampling in longitude; stencil coordinates and polygon geometries are not wrapped or changed.

geohalo.reduce_with_operator

reduce_with_operator(
    grid: T,
    operator: ReduceOperator,
    *,
    how: Literal["mean", "sum"] = "mean",
    skipna: bool = False,
    preserve_dtype: bool = False,
    lat_dim: str = "latitude",
    lon_dim: str = "longitude",
    geom_dim: str = "geom",
) -> T

Reduce grid to per-polygon values using the fused source-grid operator.

By default, contributing NaNs propagate. skipna=True omits missing source cells and renormalizes means over their surviving signed weights; nonpositive denominators return NaN. Sums omit missing contributions, returning zero when all are missing. With fused resampling, source masking differs from resample-then-mask and negative weights can cause overshoots. For target-cell masking or per-cell weights, use reduce_with_stencil. Large grids gather contributing cells per batch slice, retaining the operator's precision without copying or upcasting the entire batch. preserve_dtype=True retains each floating input variable's dtype in the result, without lowering matrix precision. Integer means remain floating.

geohalo.reduce_with_restricted_operator

reduce_with_restricted_operator(
    grid: T,
    operator: RestrictedOperator,
    *,
    how: Literal["mean", "sum"] = "mean",
    skipna: bool = False,
    preserve_dtype: bool = False,
    lat_dim: str = "latitude",
    lon_dim: str = "longitude",
    geom_dim: str = "geom",
) -> T

Reduce data, reading only the plan's contributing spatial chunks.

Returns an eager result, preserving batch coordinates, names, and attrs. Each batch chunk is processed separately; only one spatial window and its gathered cells are held at a time. Without batch chunk metadata, blocks target 8 MiB of working data (at least one slice). Native chunks may exceed this.

Coordinates must match the plan's stored latitude order and longitude grid. Known spatial chunk layouts must also match; rebuild the plan after changing either. Dataset variables without both spatial dimensions pass through. NaNs propagate by default. skipna=True omits missing source cells, renormalizing means over surviving signed weights (NaN if nonpositive). Sums omit missing contributions, returning zero if all are missing. With fused resampling this is not resample-then-mask. Per-cell weights and target-cell masking still require reduce_with_stencil. preserve_dtype=True retains each floating input variable's result dtype; integer inputs keep normal promotion, including floating-point means.


Resampling

geohalo.resample_grid

resample_grid(
    source: T,
    target_resolution: float,
    *,
    lat_dim: str = "latitude",
    lon_dim: str = "longitude",
    iterations: int = 1,
    period: float | None = None,
    method: ResampleMethod = "meanpreserving",
    normalization: Normalization = "destination",
    source_bounds: GridBounds | None = None,
    skipna: bool = False,
    preserve_dtype: bool = False,
) -> T

Resample at a target spacing; period makes longitude a full cycle.

period=360 wraps interpolation and parent assignment at the seam. The generated longitude centres start at the source minimum and exclude its repeated endpoint. period=None retains clamped, min/max-based targets. method='conservative' uses spherical area overlaps, with optional normalization='covered' or apply-time skipna=True. Target generation still uses source centres, not their outer cell bounds; use an explicitly built Resampler for matched source/target footprints. source_bounds is an optional pair of latitude/longitude edge arrays (conservative only). preserve_dtype=True retains each real floating variable's result dtype for either method, with existing computation and normalization precision. Integer, boolean, and complex inputs retain normal promotion.

geohalo.resample_grid_with_matrix

resample_grid_with_matrix(
    source: T,
    resampler: Resampler,
    *,
    lat_dim: str = "latitude",
    lon_dim: str = "longitude",
    skipna: bool = False,
    preserve_dtype: bool = False,
) -> T

Resample a matching source grid, accepting either latitude orientation.

Raises ValueError if the source coordinates differ from the resampler's source grid. Output coordinates follow the resampler's target order. For conservative resamplers, skipna=True averages only valid covered area; by default contributing NaNs propagate. Unmapped cells return NaN. preserve_dtype=True retains each real floating variable's result dtype without lowering computation or normalization precision. Integer, boolean, and complex inputs retain normal promotion. Applies to both methods.


Hierarchical rollups

geohalo.aggregate_bias

aggregate_bias(
    reduced: T,
    edges: DataFrame,
    *,
    geom_dim: str = "geom",
    parent_col: str = "parent",
    weight_col: str | None = None,
    how: Literal["mean", "sum"] = "mean",
) -> T

geohalo.aggregate_bias_with_tree

aggregate_bias_with_tree(
    reduced: T, tree: BiasTree, *, geom_dim: str = "geom"
) -> T

Precomputed operators

All five classes support obj.to_npz() -> bytes and Class.from_npz(blob: bytes) -> Class. These versioned, non-pickle artifacts can be stored independently of the built-in caches. See portable operator artifacts for supported keys, the array schema, validation, and migration from older caches.

geohalo.Stencil dataclass

Bases: NPZSerializable

compute classmethod

compute(
    lats: ndarray,
    lons: ndarray,
    geoms: GeoSeries,
    *,
    spherical_correction: bool = True,
    dtype: DTypeLike = np.float64,
) -> Stencil

Build caller-ordered rows with float64 (default) or float32 coefficients.

Geometry calculations use float64 before casting stored coefficients. Row sums accumulate those stored weights in float64. Coordinates remain float64; dtype is part of the canonical digest, with old float64 keys intact.

geohalo.ReduceOperator dataclass

Bases: NPZSerializable

apply_grid

apply_grid(
    values: ndarray,
    *,
    descending: bool = False,
    how: Literal["mean", "sum"] = "sum",
    skipna: bool = False,
    preserve_dtype: bool = False,
) -> np.ndarray

Project (..., latitude, longitude) values; default to an unnormalized sum.

Large grids gather only referenced cells, one batch slice at a time. descending=True interprets source rows in descending latitude order. The canonical matrix and its arithmetic precision are unchanged.

how="mean" divides by row_sums. With skipna=True, sums omit missing source cells and means divide by the surviving signed weight (NaN if nonpositive). This is source-cell masking, not resample-then-mask.

preserve_dtype=True returns floating inputs in their own dtype. Multiplication and normalization keep their existing precision; integer inputs retain normal promotion, so means are never truncated to integers.

compute classmethod

compute(
    stencil: Stencil,
    source_lat: ndarray,
    source_lon: ndarray,
    *,
    iterations: int = 1,
    dtype: DTypeLike = np.float64,
    period: float | None = None,
) -> ReduceOperator

Fuse with float64 (default) or float32 stored coefficients.

Fusion uses the existing float64 resampling math before casting. The stencil's float64 row sums are retained; casting does not sort entries. Request float32 explicitly even if the stencil already uses float32. period=360 wraps resampling in longitude, not polygon geometries.

geohalo.RestrictedOperator dataclass

Bases: NPZSerializable

Reusable chunk-aligned read plan for a :class:ReduceOperator.

Latitude coordinates and windows are in stored order, not canonical order. A plan is specific to that orientation and the spatial chunk layout. The compact matrix keeps the original coefficient accumulation order.

gather

gather(arrays: Iterable[ndarray]) -> np.ndarray

Gather already-read windows into (..., contributing_cells).

Supply exactly one NumPy array per entry in windows, in that order. Each array must have trailing (rows, columns) dimensions matching its window and the same leading batch shape. Coordinates cannot be checked: the caller is responsible for reading in the plan's stored grid order.

Iterators are consumed one window at a time, without retaining earlier arrays or flattening whole noncontiguous windows. A common input dtype is preserved; mixed dtypes are promoted with numpy.result_type. No I/O, xarray objects, or Dask scheduling is involved.

With no windows, gather([]) returns an empty float64 vector. For a batched empty plan, pass np.empty((*batch_shape, 0), dtype=...) directly to apply instead.

apply

apply(
    gathered: ndarray,
    *,
    how: Literal["mean", "sum"] = "mean",
    skipna: bool = False,
    preserve_dtype: bool = False,
) -> np.ndarray

Reduce (..., contributing_cells) to (..., zones) in keys order.

gathered must be in the column order produced by gather. Sum returns the sparse projection; mean additionally divides by row_sums. Matrix precision and coefficient accumulation order are preserved, with per-batch-slice products to avoid upcasting or copying an entire batch. preserve_dtype=True stores results in a floating input's dtype; integer inputs keep normal promotion, including floating-point means.

By default, contributing NaNs propagate. skipna=True omits missing source cells: sums return the remaining weighted contributions (zero if all are missing), and means divide by the surviving signed weight (NaN if nonpositive). With fused resampling this differs from masking after resampling; negative coefficients can still produce overshoots. This method does not read data or require xarray objects or Dask.

compute classmethod

compute(
    operator: ReduceOperator,
    source_lat: ndarray,
    lat_chunks: ChunkSizes,
    lon_chunks: ChunkSizes,
) -> RestrictedOperator

Build from stored latitudes and chunks, inheriting the operator's dtype.

from_grid classmethod

from_grid(
    operator: ReduceOperator,
    grid: DataArray,
    *,
    lat_dim: str = "latitude",
    lon_dim: str = "longitude",
) -> RestrictedOperator

Infer layout from Dask chunks or backend encoding, without reading values.

For a Dataset, pass one spatial variable; reuse the plan for variables with the same layout. For missing/stale metadata, use compute with explicit chunk sizes instead. Dask and Zarr are not required dependencies.

geohalo.Resampler dataclass

Bases: NPZSerializable

Grid transform whose source columns use ascending latitude, then longitude.

Target rows retain the supplied coordinate order. Direct matrix callers must flatten source values in (source_lat, source_lon) order; the xarray API handles descending source latitudes automatically.

The default mean-preserving method stores transform_matrix. Conservative resampling instead stores axis_weights=(latitude, longitude) and leaves transform_matrix=None; use apply_grid for either representation.

coverage property

coverage: ndarray

Source-covered fraction of each target cell (conservative only).

apply_grid

apply_grid(
    values: ndarray,
    *,
    descending: bool = False,
    skipna: bool = False,
    preserve_dtype: bool = False,
) -> np.ndarray

Transform (..., latitude, longitude) values into (..., n_target).

descending=True interprets source rows in descending latitude order. Temporary dense allocations are bounded per slice or small batch block. Conservative resamplers use two 1-D contractions per slice. Uncovered cells return NaN. For this method only, skipna=True renormalizes over valid covered area, overriding destination-area normalization; all missing cells return NaN. The default propagates contributing NaNs.

preserve_dtype=True returns real floating inputs in their own dtype for either method. Computation and normalization retain their existing precision; only result storage changes. Integer, boolean, and complex inputs retain normal promotion. Defaults are unchanged.

compute classmethod

compute(
    source_lat: ndarray,
    source_lon: ndarray,
    target_lat: ndarray,
    target_lon: ndarray,
    *,
    iterations: int = 1,
    period: float | None = None,
    method: ResampleMethod = "meanpreserving",
    normalization: Normalization = "destination",
    source_bounds: GridBounds | None = None,
    target_bounds: GridBounds | None = None,
) -> Resampler

Build a mean-preserving or separable conservative grid transform.

The default clamps both axes. Set period=360 for cyclic longitude, with no repeated source endpoint. Explicit target coordinates retain their values and order, including targets outside the source cycle.

method='conservative' averages spherical cell overlaps using two 1-D axis_weights; transform_matrix is None. normalization is 'destination' (full target area) or 'covered' (source-covered area). Optional source_bounds/target_bounds are (latitude, longitude) edge arrays, each one longer than its coordinate axis, in that axis's order. Otherwise midpoint edges are inferred (latitude clipped at the poles, cyclic source longitude when periodic). Singleton noncyclic axes require bounds. Conservative axes must be strictly monotonic, and iterations must remain 1. Geometries and existing reducers are not changed by this grid-resampling option.

geohalo.BiasTree dataclass

Bases: NPZSerializable


Caching

geohalo.LocalCache

Bases: _Cache

NPZ files under path/<namespace>/<key>.npz; legacy entries are ignored.

geohalo.RedisCache

Bases: _Cache

Values under <prefix>:<key> in Redis.


Geometry areas

These helpers live in geohalo.geometry. See latitude correction for the coordinate convention and the distinction between polygon areas and stencil weights.

geohalo.geometry.polygon_areas

polygon_areas(
    geoms: GeoSeries | Sequence[Geometry | None] | ndarray,
) -> np.ndarray

Spherical areas in m², using the same radius as :func:cell_areas.

Accept a GeoSeries or a one-dimensional sequence of Shapely geometries in longitude/latitude degrees. Return a float64 array in input order, without index labels. Missing geometries return NaN; empty geometries and non-area geometries return zero. Polygon holes are subtracted and multipart or collection areas are added (not unioned). Z coordinates are ignored.

Edges are straight segments in the supplied lon/lat coordinates, not geodesics. Integration is analytic, including sloped edges. Longitudes are NOT wrapped: a box from -170 to 170 spans 340 degrees, while a narrow seam-crossing box can use 170 to 190, or be split at the antimeridian. Each polygon must span at most 360 degrees. Full-globe boxes are supported.

Coordinates must be finite, latitudes within [-90, 90], and geometries topologically valid. Invalid inputs raise ValueError; non-geometries raise TypeError. A GeoSeries CRS, when set, must be EPSG:4326 (or equivalent). No reprojection, repair, clipping, or longitude unwrapping is performed.

Stencil weights use planar cell-coverage fractions times spherical cell areas. They share these units and radius, but only approximate this area in partially covered cells. For planar areas, use Shapely's area.

geohalo.geometry.cell_areas

cell_areas(
    lats: ndarray, lons: ndarray, *, spherical: bool = True
) -> np.ndarray

Per-cell area. spherical=True → m²; spherical=False → all 1.0.


Exceptions

geohalo.EmptyOverlapError

Bases: Exception