Source code for stilt.footprint.config

"""
Footprint config.

A footprint's ``grid`` is the raster it is computed on. Its ``geometry``,
when given, names the polygons it will be aggregated to: a vector file, H3
hexagons, or windows around points. Without a ``grid``, the grid is derived
from the geometry (:meth:`stilt.Mesh.from_spec`, then
:meth:`stilt.Mesh.to_grid`).

Loading a config does not read the geometry. ``project.variants`` reads it
(:meth:`stilt.ProjectConfig.resolve`), and the grid and the geometry's hash are
recorded with the footprints.

.. code-block:: yaml

   geometry:                     # the default footprint's geometry
     kind: file
     path: counties.shp
     ids: NAME

   variants:
     hrrr: {}
     hrrr-hexes:                   # a second footprint from the same particles
       geometry: {kind: h3, resolution: 8, bounds: {xmin: -112.3, xmax: -111.6, ymin: 40.4, ymax: 41.0}}
       cells_per_target: 4
"""

from __future__ import annotations

from typing import Annotated, Any, Literal

from pydantic import (
    BaseModel,
    ConfigDict,
    Field,
    field_serializer,
    field_validator,
)

from stilt.spatial import Bounds, Grid
from stilt.transforms import dump_transform, load_transform


class _GeometrySpec(BaseModel):
    """Base of the geometry specs: a frozen description of the polygons."""

    model_config = ConfigDict(frozen=True)


[docs] class FileGeometrySpec(_GeometrySpec): """Polygons read from a vector file such as a shapefile or GeoPackage.""" kind: Literal["file"] = "file" path: str = Field(..., description="Path to the vector file.") ids: str | None = Field( None, description="Attribute column holding the cell ids. Unset uses the row number.", ) layer: str | None = Field( None, description="Layer to read from a multi-layer file." ) where: str | None = Field( None, description="Attribute filter as an OGR SQL WHERE clause." )
[docs] class H3GeometrySpec(_GeometrySpec): """H3 hexagons of one resolution covering a longitude/latitude box.""" kind: Literal["h3"] = "h3" resolution: int = Field(..., description="H3 resolution (0-15).", ge=0, le=15) bounds: Bounds = Field(..., description="Longitude/latitude box to cover.")
[docs] class WindowsGeometrySpec(_GeometrySpec): """Rectangular windows centered on points, such as known point sources.""" kind: Literal["windows"] = "windows" coords: list[tuple[float, float]] = Field( ..., description="Window centers as (x, y) pairs in ``crs`` units." ) size: float | tuple[float, float] = Field( ..., description="Window width, or (width, height), in ``crs`` units." ) ids: list[str] | None = Field(None, description="Label for each window.") crs: str = Field( "+proj=longlat", description="Coordinate reference system of ``coords``." )
GeometrySpec = Annotated[ FileGeometrySpec | H3GeometrySpec | WindowsGeometrySpec, Field(discriminator="kind"), ] """Any geometry spec accepted by ``FootprintConfig.geometry``."""
[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 particles. Give ``geometry`` to name the polygons the footprint will be aggregated to, and the grid is derived from them (:meth:`stilt.Mesh.to_grid`) when the project first reads its variants (``project.variants``), not when the config loads. When both are given, ``grid`` is used as is and ``geometry`` is recorded with the footprints. """ grid: Grid | None = Field( None, description=( "Domain and resolution of the footprint. Unset with ``geometry`` " "derives it from the geometry; unset without it gives a run that " "produces particles 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, ) 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, ) @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]
__all__ = [ "FileGeometrySpec", "FootprintConfig", "GeometrySpec", "H3GeometrySpec", "WindowsGeometrySpec", ]