Source code for arlmet.vertical
"""
Vertical coordinate helpers for ARL meteorology grids.
This module keeps vertical metadata separate from the horizontal grid model in
``arlmet.grid`` and provides lightweight helpers for deriving level
coordinates.
"""
from collections.abc import Sequence
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing_extensions import override
else:
def override(f: object) -> object:
return f
import numpy as np
import numpy.typing as npt
[docs]
class VerticalAxis:
"""
Vertical coordinate metadata for an ARL file.
Parameters
----------
flag : int
ARL vertical coordinate flag. Supported values include ``1`` (sigma),
``2`` (pressure), ``3`` (terrain-following), ``4`` (hybrid), and
``5`` (WRF).
levels : sequence of float
Native level values stored in the file.
offset : float, default 0.0
Pressure offset used by sigma and hybrid coordinate conversions.
Attributes
----------
flag : int
Raw ARL vertical flag.
coord_system : str
Human-readable coordinate system name.
levels : numpy.ndarray
Copy of the stored level values.
offset : float
Pressure offset for sigma or hybrid coordinates.
Methods
-------
calculate_coords()
Return the native level coordinate values.
sigma_to_pressure(surface_pressure, levels)
Convert sigma or hybrid levels to pressure at sample points.
Examples
--------
>>> from arlmet.vertical import VerticalAxis
>>> axis = VerticalAxis(flag=2, levels=[1000.0, 925.0, 850.0])
>>> axis.coord_system
'pressure'
>>> axis.calculate_coords()["level"].tolist()
[1000.0, 925.0, 850.0]
"""
FLAGS: dict[int, str] = {
1: "sigma",
2: "pressure",
3: "terrain",
4: "hybrid",
5: "wrf",
}
def __init__(
self,
flag: int,
levels: Sequence[float],
*,
offset: float = 0.0,
):
self.flag = flag
self._levels = np.asarray(levels, dtype=float)
self.offset = float(offset)
@property
def coord_system(self) -> str:
return self.FLAGS.get(self.flag, "unknown")
@property
def levels(self) -> np.ndarray:
return self._levels.copy()
[docs]
def calculate_coords(self) -> dict[str, np.ndarray]:
"""Return the native level coordinate values stored in the file."""
return {"level": self._levels.copy()}
[docs]
def sigma_to_pressure(
self,
surface_pressure: npt.ArrayLike,
levels: Sequence[int],
) -> np.ndarray:
"""
Compute per-point pressure at each level for sigma or hybrid axes.
Matches HYSPLIT metlvl.f: PLEVEL = OFFSET + (SFCP - OFFSET) * HEIGHT(LL)
Parameters
----------
surface_pressure : array-like of shape (n_points,)
Surface pressure in hPa at each sample point.
levels : sequence of int
Level indices into self.levels to compute pressure for.
Returns
-------
np.ndarray of shape (n_points, n_levels)
"""
lv = self._levels[list(levels)]
sp = np.asarray(surface_pressure, dtype=float)
if self.flag == 1:
# sigma: p = p_top + (p_surface - p_top) * sigma
return self.offset + (sp[:, None] - self.offset) * lv[None, :]
if self.flag == 4:
# hybrid: level encoded as floor_pressure + sigma_fraction
floor_p = np.floor(lv)
sigma = lv - floor_p
p = sp[:, None] * sigma[None, :] + floor_p[None, :]
if len(levels) > 0 and levels[0] == 0:
p[:, 0] = sp # first hybrid level is always surface
return p
raise ValueError(
f"sigma_to_pressure() is only valid for flag=1 (sigma) or flag=4 (hybrid); "
f"got flag={self.flag} ({self.coord_system})."
)
@override
def __eq__(self, other: object) -> bool:
if not isinstance(other, VerticalAxis):
return False
return (
self.flag == other.flag
and self.offset == other.offset
and np.array_equal(self._levels, other._levels)
)
def __hash__(self) -> int:
return hash((self.flag, self.offset, tuple(self._levels)))
def __len__(self) -> int:
return len(self._levels)
@override
def __repr__(self) -> str:
return f"VerticalAxis({self.coord_system}, n={len(self._levels)})"