Source code for sasktran2.constituent.manual
from __future__ import annotations
import numpy as np
import sasktran2 as sk
from sasktran2._core_rust import PyManual
from .base import Constituent
[docs]
class Manual(Constituent):
_inner: PyManual
[docs]
def __init__(
self,
extinction: np.ndarray,
ssa: np.ndarray,
legendre_moments: np.ndarray | None = None,
delta_scale: bool = False,
) -> None:
"""
An implementation of a manual constituent where the user provides the extinction, single
scattering albedo, and optionally the Legendre moments.
Note that this is manual in the sense that the user provides all necessary atmospheric
quantities directly, on the model grid. No interpolation is done between levels, or between
"wavelength" calculations.
The legendre_moments must also be provided with the same number of moments as the model,
including the batching of spherical legendre moments if using multiple stokes parameters.
Parameters
----------
extinction : numpy.ndarray
Extinction cross section in m^-1. Shape ``(altitude, wavelength)``
or ``(horizontal, altitude, wavelength)``. Altitude-only input is
broadcast horizontally in a 2D atmosphere.
ssa : numpy.ndarray
Single scattering albedo with the same shape as ``extinction``.
legendre_moments : numpy.ndarray | None, optional
Legendre moments, by default None. Shape
``(moment, *extinction.shape)``.
delta_scale : bool, optional
Whether to apply delta-scaling to the scattering properties, by default False.
"""
extinction = np.asarray(extinction)
ssa = np.asarray(ssa)
if extinction.shape != ssa.shape:
msg = "extinction and ssa must have the same shape"
raise ValueError(msg)
if extinction.ndim not in (2, 3):
msg = (
"extinction and ssa must have shape (altitude, wavelength) or "
"(horizontal, altitude, wavelength)"
)
raise ValueError(msg)
self._input_volume_shape = (
extinction.shape[:-1] if extinction.ndim == 3 else None
)
if legendre_moments is not None:
legendre_moments = np.asarray(legendre_moments)
if (
legendre_moments.ndim != extinction.ndim + 1
or legendre_moments.shape[1:] != extinction.shape
):
msg = (
"legendre_moments must have shape (moment, *extinction.shape); "
f"got {legendre_moments.shape} for extinction shape "
f"{extinction.shape}"
)
raise ValueError(msg)
if delta_scale and legendre_moments is None:
msg = "legendre_moments must be provided when delta_scale is enabled"
raise ValueError(msg)
if delta_scale:
f = legendre_moments[::4][-1] / (2.0 * legendre_moments.shape[0] / 4 + 1)
extinction *= 1.0 - ssa * f
ssa *= (1.0 - f) / (1.0 - ssa * f)
for i in range(int(legendre_moments.shape[0] / 4)):
# a1
legendre_moments[4 * i] -= f * (2.0 * i + 1)
legendre_moments[4 * i] /= 1.0 - f
# a2
legendre_moments[4 * i + 1] -= f * (2.0 * i + 1)
legendre_moments[4 * i + 1] /= 1.0 - f
# a3
legendre_moments[4 * i + 2] -= f * (2.0 * i + 1)
legendre_moments[4 * i + 2] /= 1.0 - f
# b1
legendre_moments[4 * i + 3] /= 1.0 - f
flat_extinction = extinction.reshape(-1, extinction.shape[-1])
flat_ssa = ssa.reshape(-1, ssa.shape[-1])
flat_legendre = (
None
if legendre_moments is None
else legendre_moments.reshape(
legendre_moments.shape[0], -1, legendre_moments.shape[-1]
)
)
self._inner = PyManual(
extinction=flat_extinction,
ssa=flat_ssa,
legendre_moments=flat_legendre,
)
@property
def volume_spatial_mode(self) -> str:
return (
"native_2d" if self._input_volume_shape is not None else "altitude_profile"
)
@property
def extinction(self) -> np.ndarray:
extinction = self._inner.extinction
if self._input_volume_shape is None:
return extinction
return extinction.reshape((*self._input_volume_shape, extinction.shape[-1]))
@property
def ssa(self) -> np.ndarray:
ssa = self._inner.ssa
if self._input_volume_shape is None:
return ssa
return ssa.reshape((*self._input_volume_shape, ssa.shape[-1]))
@property
def leg_coeff(self) -> np.ndarray | None:
leg_coeff = self._inner.leg_coeff
if leg_coeff is None or self._input_volume_shape is None:
return leg_coeff
return leg_coeff.reshape(
(leg_coeff.shape[0], *self._input_volume_shape, leg_coeff.shape[-1])
)
def add_to_atmosphere(self, atmo: sk.Atmosphere):
"""
Parameters
----------
atmo : sk.Atmosphere
:meta private:
"""
extinction = self.extinction
if extinction.shape[-1] != atmo.num_wavel:
msg = (
"Manual constituent wavelength dimension does not match the atmosphere: "
f"{extinction.shape[-1]} != {atmo.num_wavel}"
)
raise ValueError(msg)
leg_coeff = self.leg_coeff
if (
leg_coeff is not None
and leg_coeff.shape[0] != atmo.storage.leg_coeff.shape[0]
):
msg = (
"Manual constituent Legendre moment dimension does not match the "
f"atmosphere: {leg_coeff.shape[0]} != "
f"{atmo.storage.leg_coeff.shape[0]}"
)
raise ValueError(msg)
if self._input_volume_shape is not None:
if tuple(atmo.volume_shape) != tuple(self._input_volume_shape):
msg = (
"Native 2D Manual constituent shape does not match the atmosphere: "
f"{self._input_volume_shape} != {atmo.volume_shape}"
)
raise ValueError(msg)
elif extinction.shape[0] != atmo.volume_shape[-1]:
msg = (
"Altitude-profile Manual constituent does not match the atmosphere: "
f"{extinction.shape[0]} altitudes != {atmo.volume_shape[-1]}"
)
raise ValueError(msg)
self._inner.add_to_atmosphere(atmo)
def register_derivative(self, atmo: sk.Atmosphere, name: str):
self._inner.register_derivative(atmo, name)