Source code for stilt.config.footprint

"""Footprint settings."""

from __future__ import annotations

from typing import Any

from pydantic import (
    BaseModel,
    Field,
    TypeAdapter,
    field_serializer,
    field_validator,
    model_validator,
)

from stilt.transforms import dump_transform, load_transform

from .geometry import GeometrySpec
from .spatial import Grid

_GEOMETRY_ADAPTER: TypeAdapter[Any] = TypeAdapter(GeometrySpec)


[docs] class FootprintConfig(BaseModel): """ Footprint settings: the grid, smoothing, and particle transforms. The config and each variant hold these as their defaults and overrides, and every footprint keeps the settings it was calculated with. ``grid`` is the raster the footprint is computed on. Leave both ``grid`` and ``geometry`` unset for a variant that only produces trajectories. Give ``geometry`` to name the polygons the footprint will be aggregated to, and the grid is derived from them with :meth:`stilt.Grid.from_geometry`. When both are given, ``grid`` is used as is and ``geometry`` is kept with the footprint. """ grid: Grid | None = Field( None, description=( "Domain and resolution of the footprint. Leaving it unset with no " "``geometry`` gives a run that produces trajectories only." ), ) geometry: GeometrySpec | None = Field( None, description=( "Polygons the footprint will be aggregated to (``kind`` of " "``file``, ``h3``, or ``windows``). Used to derive ``grid`` when it " "is unset, and stored with the footprint." ), ) cells_per_target: float = Field( default=4.0, description="Grid cells across the smallest ``geometry`` cell when ``grid`` is derived.", gt=0, ) geometry_hash: str | None = Field( None, description=( "Hash of the built ``geometry`` (``Mesh.hash``), used to tell whether " "the geometry changed after a footprint was made. Filled in " "automatically when ``geometry`` is set." ), ) smooth_factor: float = Field( 1.0, description=( "Factor on the width of the Gaussian smoothing kernel. 0 turns smoothing off." ), ) time_integrate: bool = Field( False, description="Sum the footprint over time into a single layer instead of hourly layers.", ) transforms: list[Any] = Field( description=( "Particle transforms applied in order before the footprint is " "computed. Each entry's ``kind`` is a built-in name " "(``averaging_kernel``, ``pressure_weighting``, " "``first_order_lifetime``) or the import path of your own class." ), default_factory=list, ) @model_validator(mode="before") @classmethod def _derive_from_geometry(cls, data: Any) -> Any: """ Fill ``grid`` and ``geometry_hash`` from ``geometry`` when they are missing. The geometry is built only when one of them is missing, so reloading a stored config never reads the geometry source. """ if not isinstance(data, dict): return data spec_raw = data.get("geometry") if spec_raw is None: return data need_grid = data.get("grid") is None need_hash = data.get("geometry_hash") is None if not (need_grid or need_hash): return data spec = ( spec_raw if hasattr(spec_raw, "build") else _GEOMETRY_ADAPTER.validate_python(spec_raw) ) mesh = spec.build() out = dict(data) if need_grid: cells = float(data.get("cells_per_target", 4.0)) out["grid"] = Grid.from_geometry(mesh, cells_per_target=cells) if need_hash: out["geometry_hash"] = mesh.hash return out @field_validator("transforms", mode="before") @classmethod def _load_transforms(cls, value: Any) -> list[Any]: """Build transform objects from their configured mappings.""" if value is None: return [] return [load_transform(item) for item in value] @field_serializer("transforms") def _dump_transforms(self, value: list[Any]) -> list[dict[str, Any]]: """Serialize the transforms to plain mappings.""" return [dump_transform(item) for item in value] @property def footprint(self) -> FootprintConfig | None: """ The footprint settings alone, or ``None`` without a grid. On a :class:`~stilt.config.VariantConfig` this drops the transport settings, leaving what a footprint is calculated and stored with. """ if self.grid is None: return None return FootprintConfig( **{name: getattr(self, name) for name in FootprintConfig.model_fields} )
__all__ = ["FootprintConfig"]