Source code for skyscapes.physical_model.exojax.components.clouds

"""Cloud opacity components."""

from __future__ import annotations

import equinox as eqx
import jax
import jax.numpy as jnp
from jaxtyping import Array

from .base import AbstractClouds, Contribution

# Typical liquid-water terrestrial cloud values: pure scattering
# (ssa = 1, no internal absorption at visible/NIR), strongly forward-
# scattering (g ~ 0.85).
DEFAULT_CLOUD_SSA = 1.0
DEFAULT_CLOUD_ASYMMETRY_G = 0.85
# Cloud vertical distribution width in log10(P) [dex]. 0.3 spreads the
# cloud over ~half a decade in pressure -- a reasonable single-deck width
# at the default 100-layer resolution.
DEFAULT_CLOUD_LOG_SIGMA = 0.3


[docs] class GrayCloud(AbstractClouds): """Single-layer gray-scattering cloud. The total cloud scattering optical depth is distributed across layers via a Gaussian in log-pressure (softmax-normalised so the weights are well-defined even when the cloud pressure is far outside the layer grid -- useful for ablation runs with ``log_opt_depth = -inf``). Wavelength-grey: the cloud's cross-section is constant across the spectrum. A Mie cloud with composition-dependent scattering would be a separate component implementing the same contract. Attributes (PyTree leaves, fittable): log_pressure_bar: Log10 cloud-deck pressure [bar], shape ``(K,)``. log_opt_depth: Log10 of the vertically-integrated cloud scattering optical depth, shape ``(K,)``. Static attributes (configuration): ssa: Single-scattering albedo (default 1.0, pure scattering). g: Asymmetry parameter (default 0.85, forward-peaked). log_sigma: Vertical-distribution width in log10(P) [dex]. """ log_pressure_bar: Array log_opt_depth: Array ssa: float = eqx.field(static=True, default=DEFAULT_CLOUD_SSA) g: float = eqx.field(static=True, default=DEFAULT_CLOUD_ASYMMETRY_G) log_sigma: float = eqx.field(static=True, default=DEFAULT_CLOUD_LOG_SIGMA)
[docs] def compute( self, log_pressure_bar_scalar: Array, log_opt_depth_scalar: Array, pressure: Array, n_nu: int, ) -> Contribution: """Gray cloud contribution at a single pressure level.""" n_layers = pressure.shape[0] log_p_layers = jnp.log10(pressure) log_w = -0.5 * ((log_p_layers - log_pressure_bar_scalar) / self.log_sigma) ** 2 # Softmax stabilizes the normalisation when the cloud pressure # is far from any layer (would otherwise give 0/0 underflow). weights = jax.nn.softmax(log_w) tau_total = 10.0**log_opt_depth_scalar dtau_layer = tau_total * weights # (n_layers,) dtau = jnp.broadcast_to(dtau_layer[:, None], (n_layers, n_nu)) dtau_scatter = self.ssa * dtau g_weighted_num = self.g * dtau_scatter return Contribution( dtau_total=dtau, dtau_scatter=dtau_scatter, g_weighted_num=g_weighted_num )
[docs] class NoCloud(AbstractClouds): """Disable cloud opacity: zero contribution. Equivalent to a ``GrayCloud`` with ``log_opt_depth = -inf`` but structurally explicit, so that ``isinstance(atm.clouds, NoCloud)`` documents intent in code that builds cloud-free atmospheres. """
[docs] def compute( self, log_pressure_bar_scalar: Array, log_opt_depth_scalar: Array, pressure: Array, n_nu: int, ) -> Contribution: """Return zero-everywhere cloud contribution. ``log_pressure_bar_scalar`` and ``log_opt_depth_scalar`` are accepted for parity with :class:`GrayCloud` but unused. """ zeros = jnp.zeros((pressure.shape[0], n_nu)) return Contribution(dtau_total=zeros, dtau_scatter=zeros, g_weighted_num=zeros)