"""FV3GFS preprocessing interfaces used by the ACE model package.""" from __future__ import annotations from dataclasses import dataclass from typing import Callable import numpy as np @dataclass(frozen=True) class VerticalCoordinate: a_pa: np.ndarray b: np.ndarray source_indices: np.ndarray @classmethod def ace_default(cls) -> "VerticalCoordinate": return cls( np.asarray([64.247, 5167.14603, 12905.42546, 13982.4677, 12165.28766, 8910.07678, 4955.72632, 2155.78385, 0.0]), np.asarray([0.0, 0.0, 0.01755, 0.11746, 0.2896, 0.49806, 0.72625, 0.88192, 1.0]), np.asarray([0, 18, 26, 31, 36, 41, 47, 53, 63]), ) def interface_pressure(self, surface_pressure: np.ndarray) -> np.ndarray: ps = np.asarray(surface_pressure) if ps.ndim < 2: raise ValueError("surface_pressure must end in [lat, lon]") return np.einsum("k,...ij->...kij", self.b, ps) + self.a_pa.reshape((1,) * (ps.ndim - 2) + (9, 1, 1)) def conservative_vertical_coarsen(values: np.ndarray, layer_thickness: np.ndarray, coordinate: VerticalCoordinate | None = None, layer_axis: int = -3) -> tuple[np.ndarray, np.ndarray]: coord = coordinate or VerticalCoordinate.ace_default() x = np.moveaxis(np.asarray(values, dtype=np.float64), layer_axis, -3) dp = np.moveaxis(np.asarray(layer_thickness, dtype=np.float64), layer_axis, -3) if x.shape[-3] != 63 or dp.shape != x.shape: raise ValueError(f"expected values/dp shape [...,63,H,W], got {x.shape} and {dp.shape}") means, thickness = [], [] for start, end in zip(coord.source_indices[:-1], coord.source_indices[1:]): chunk_dp, chunk_x = dp[..., start:end, :, :], x[..., start:end, :, :] dpk = chunk_dp.sum(axis=-3) means.append((chunk_x * chunk_dp).sum(axis=-3) / np.maximum(dpk, 1e-12)) thickness.append(dpk) return np.moveaxis(np.stack(means, axis=-3), -3, layer_axis), np.moveaxis(np.stack(thickness, axis=-3), -3, layer_axis) def conservative_regrid(fields: np.ndarray, regrid_fn: Callable[[np.ndarray], np.ndarray] | None = None) -> np.ndarray: if regrid_fn is None: raise NotImplementedError("MISSING: NOAA fregrid or another conservative cubed-sphere adapter") return np.asarray(regrid_fn(np.asarray(fields))) def spherical_harmonic_roundtrip(fields: np.ndarray, filter_fn: Callable[[np.ndarray], np.ndarray] | None = None) -> np.ndarray: return np.asarray(fields if filter_fn is None else filter_fn(np.asarray(fields))).copy()