Operator System¶
DisSCube implements operators as self-registering Python classes. Adding a new operator requires no change to the pipeline.
How it works¶
Every subclass of Operator that defines name is automatically inserted into OPERATOR_REGISTRY via __init_subclass__:
# operators/base.py
class Operator:
name: ClassVar[str]
requires_class_code: ClassVar[bool] = False
_resampling: ClassVar[Resampling] = Resampling.nearest
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if hasattr(cls, "name"):
OPERATOR_REGISTRY[cls.name] = cls # self-registration
@classmethod
def resampling(cls) -> Resampling:
return cls._resampling
def compute(self, data, var, grid) -> xr.DataArray:
raise NotImplementedError
GridAligner queries op_cls.resampling() to choose the resampling method before reprojecting the raster. Aggregator calls op_cls().compute(data, var, grid) to compute the result. Neither of them contains a list of operators.
Available operators¶
Zonal, with direct resampling¶
These operators receive from GridAligner a DataArray already reprojected to the target resolution. _resampling determines the reprojection method.
| Operator | _resampling |
Raster | Vector | requires_class_code |
|---|---|---|---|---|
mean |
average |
mean of the pixels on upscale | — | no |
sum |
sum |
sum of the pixels on upscale | — | no |
min |
min |
minimum on upscale | — | no |
max |
max |
maximum on upscale | — | no |
attribute |
nearest |
passthrough | rasterizes with the value of column var.name |
no |
presence |
nearest |
passthrough | rasterizes binary with class_code (or 1) |
no |
Zonal, with fine alignment (needs_fine_alignment = True)¶
These operators receive a high-resolution array snapped to the target grid origin. GridAligner reprojects with Resampling.nearest at a fine resolution (an integer sub-multiple of the cell size); the operator then reduces over real windows of the raw pixels.
| Operator | Raster | Vector | requires_class_code |
|---|---|---|---|
std |
true per-cell standard deviation (ddof 0 or 1) |
— | no |
median |
median of the valid pixels | — | no |
majority |
dominant class by count | rasterizes with class_code (or 1) |
no |
minority |
least-frequent class by count | rasterizes with class_code (or 1) |
no |
percentage |
fraction of pixels of the target class | rasterizes with class_code |
yes |
sum also accepts a vector source: params = {area = true} shares each polygon's value among the cells in proportion to the intersected area (total conserved); without it every feature adds its whole value to each cell it touches. The column is the one named like the target, or params = {column = "…"}.
Each of the operators above accepts params = {"subcells": n}, a cap on the fine pixels per cell
along each axis (see GridAligner._align_fine).
majority and minority accept params = {groups = {label = [codes]}}: the map is read at a coarser level, and the
result is the label of the group that covers most (least) of the cell. The labels are integers (TOML keys are written as
strings, "1"), a code cannot be in two groups, and codes in no group, and nodata, are ignored. With it one derivation
turns a map of leaf classes into the MapBiomas level-1 classes; see MapBiomas.
The last three also produce coverage_purity and dominance_purity as coordinates of the output DataArray (persisted in the Zarr alongside the variable).
Proximity — vector (and passthrough for raster)¶
| Operator | Description | requires_class_code |
|---|---|---|
distance |
Exact Euclidean distance (CRS units) from each cell centre to the nearest feature; the source is not clipped to the grid. params = {"crs": …} measures in another CRS (metres in a projected one) |
no |
min_distance |
Raster approximation of the distance to the nearest feature inside the grid (NaN, with a warning, if none is inside) | no |
count |
Number of features whose centroid falls in each cell | no |
network_cost (alias gpm_network) |
Least transport cost over a road network (exact multi-source Dijkstra) from each cell to the nearest destination: targets (file or [x, y] list), cost_column or status_column + inside_paved / inside_unpaved, outside (off-road factor), entrance (segment or vertex), unit_scale, crs. NaN where no destination is reachable |
no |
area |
Share (0..1) of each cell covered by polygons (intersection area / cell area; overlaps count once) — TerraME's area |
no |
The compute() contract¶
def compute(
self,
data: xr.DataArray | gpd.GeoDataFrame,
var: Variable,
grid: GridSpec,
) -> xr.DataArray:
data: for raster sources, theDataArrayalready reprojected and resampled byGridAligner; for vectors, theGeoDataFramereprojected and clipped to the bbox.- Return value: an
xr.DataArraywithdims=("y", "x")andcoordsaligned togrid.ys/grid.xs.
Adding a new operator¶
Create the file disscube/operators/my_operator.py:
from rasterio.warp import Resampling
import xarray as xr
import numpy as np
from disscube.operators.base import Operator
class WeightedMeanOperator(Operator):
"""Mean weighted by intersection area (illustrative example)."""
name = "weighted_mean"
_resampling = Resampling.average # used by GridAligner
def compute(self, data, var, grid) -> xr.DataArray:
if isinstance(data, xr.DataArray):
if "band" in data.dims:
data = data.isel(band=0)
return data.transpose("y", "x")
raise TypeError(f"'weighted_mean' requires a raster source")
Import the module so that __init_subclass__ runs — just add it to disscube/operators/__init__.py:
Done. The operator shows up in OPERATOR_REGISTRY["weighted_mean"] and is accepted by Derivation(operator="weighted_mean") and by Variable(operator="weighted_mean").
attribute — implicit contract¶
The attribute operator rasterizes a vector using the value of a numeric column as the pixel value. The column must have the same name as the variable (Variable.name):
# Vector source with columns "f" and "d"
Variable(name="f", operator="attribute") # uses gdf["f"]
Variable(name="d", operator="attribute") # uses gdf["d"]
If the column does not exist in the GeoDataFrame, the result is a raster of zeros with no explicit error. Make sure the variable name matches the column name in the source.
Construction-time validation (Derivation)¶
Derivation validates the operator name and required fields at creation time:
# Immediate error — unknown operator
Derivation(target="x", source_id="s", operator="bogus")
# ValueError: Unknown operator 'bogus'. Available: ['attribute', 'count', ...]
# Immediate error — percentage without class_code
Derivation(target="x", source_id="s", operator="percentage")
# ValueError: Operator 'percentage' requires class_code to be set.
SpatialDerivation and Variable do not validate — the error surfaces in Aggregator at run time. Use Derivation for early validation.