Source code for hydromodpy.spatial.catchment_zones_field
"""Catchment zonation object (3 classes: domain / buffer / core).
This object stores only semantic zone information and class values. It
intentionally does not duplicate georeferencing metadata already carried by
``Domain.surface_topo.support``.
"""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
import numpy as np
[docs]
@dataclass(frozen=True)
class CatchmentZonesField:
"""Compact 3-level zonation stored in ``Domain.zones``.
Attributes
----------
identifier:
Stable field identifier used by runtime consumers.
encoded_codes:
2D code matrix aligned with domain support.
encoded_to_zone:
Mapping of class codes to semantic labels.
nodata_code:
Background/nodata class code.
source_meta:
Optional provenance metadata (paths, pipeline tag...).
"""
identifier: str
encoded_codes: np.ndarray
encoded_to_zone: Mapping[int, str]
nodata_code: int = 0
source_meta: Mapping[str, str] | None = None
def __post_init__(self) -> None:
codes = np.asarray(self.encoded_codes, dtype=np.uint8)
if codes.ndim != 2:
raise ValueError("encoded_codes must be a 2D array")
if codes.size == 0:
raise ValueError("encoded_codes cannot be empty")
mapping = {int(k): str(v).strip() for k, v in dict(self.encoded_to_zone).items()}
if len(mapping) == 0:
raise ValueError("encoded_to_zone cannot be empty")
if any(code <= 0 for code in mapping):
raise ValueError("encoded_to_zone keys must be positive integers")
if any(name == "" for name in mapping.values()):
raise ValueError("encoded_to_zone labels cannot be empty")
nodata = int(self.nodata_code)
allowed = set(mapping.keys()) | {nodata}
present = set(np.unique(codes).astype(int).tolist())
if not present.issubset(allowed):
unknown = sorted(present.difference(allowed))
raise ValueError(
f"encoded_codes contains unknown class codes: {unknown}; allowed={sorted(allowed)}"
)
object.__setattr__(self, "encoded_codes", codes)
object.__setattr__(self, "encoded_to_zone", mapping)
object.__setattr__(self, "nodata_code", nodata)
if self.source_meta is not None:
object.__setattr__(
self,
"source_meta",
{str(k): str(v) for k, v in dict(self.source_meta).items()},
)
@property
def zone_keys(self) -> tuple[str, ...]:
"""Ordered tuple of zone labels (excluding nodata)."""
return tuple(self.encoded_to_zone[k] for k in sorted(self.encoded_to_zone))
@property
def shape(self) -> tuple[int, int]:
"""Zone matrix shape."""
return tuple(int(v) for v in self.encoded_codes.shape)
[docs]
def as_dict(self) -> dict[str, object]:
"""Return a lightweight metadata snapshot."""
return {
"id": str(self.identifier),
"shape": self.shape,
"nodata_code": int(self.nodata_code),
"encoded_to_zone": dict(self.encoded_to_zone),
"zone_keys": self.zone_keys,
"source_meta": dict(self.source_meta) if self.source_meta is not None else None,
}