Source code for skyscapes.physical_model.exojax.components.absorption
"""Absorption opacity components.
``Absorption`` iterates over the atmosphere's :class:`MolecularSpecies`
tuple and sums per-molecule line-list / cross-section contributions.
Each species owns its own opa engine and altitude-resolved mmr profile;
this component is a thin orchestrator with no per-molecule state of
its own.
"""
from __future__ import annotations
import jax.numpy as jnp
from jaxtyping import Array
from .base import AbstractAbsorption, Contribution
from .species import MolecularSpecies
[docs]
class Absorption(AbstractAbsorption):
"""Sum of per-species line-list / cross-section absorption.
Iterates over the species tuple, skipping any with ``opa is None``
(e.g. a species included purely for its Rayleigh contribution).
"""
[docs]
def compute(
self,
species: tuple[MolecularSpecies, ...],
Tarr: Array,
pressure: Array,
gravity: Array,
rt_engine,
) -> Contribution:
"""Sum per-species absorption optical depth."""
dtau_per_mol = [
rt_engine.opacity_profile_xs(
s.opa.xsmatrix(Tarr, pressure),
s.profile.evaluate(pressure),
s.molmass,
gravity,
)
for s in species
if s.opa is not None
]
if not dtau_per_mol:
zeros = jnp.zeros((pressure.shape[0], rt_engine.nu_grid.shape[0]))
return Contribution(
dtau_total=zeros, dtau_scatter=zeros, g_weighted_num=zeros
)
dtau = jnp.stack(dtau_per_mol, axis=0).sum(axis=0)
zeros = jnp.zeros_like(dtau)
return Contribution(dtau_total=dtau, dtau_scatter=zeros, g_weighted_num=zeros)
[docs]
class PrecomputedAbsorption(AbstractAbsorption):
"""Absorption with per-species ``xsmatrix`` precomputed at a fixed TP.
Drop-in for :class:`Absorption` for retrievals: holds each absorbing species'
cross-section matrix (computed once via ``opa.xsmatrix`` at the fixed
temperature-pressure structure) and applies only the live mmr scaling. The
spectrum is therefore differentiable in abundance but INERT IN TEMPERATURE --
the stored ``xsmatrix`` bakes ``Tarr``, so ``tp_profile`` leaves no longer
change the output. To fit temperature, use the full recompute path
(``from_default_setup``) instead. Build via
``ExoJaxPhysicalModel.for_retrieval``.
``xsmatrix_per_species`` corresponds, in order, to the species with
``opa is not None`` (the same ones :class:`Absorption` would sum over).
"""
xsmatrix_per_species: tuple[Array, ...]
[docs]
def compute(
self,
species: tuple[MolecularSpecies, ...],
Tarr: Array,
pressure: Array,
gravity: Array,
rt_engine,
) -> Contribution:
"""Sum per-species absorption from the stored cross-sections."""
absorbing = [s for s in species if s.opa is not None]
dtau_per_mol = [
rt_engine.opacity_profile_xs(
xs, s.profile.evaluate(pressure), s.molmass, gravity
)
for xs, s in zip(self.xsmatrix_per_species, absorbing, strict=True)
]
if not dtau_per_mol:
zeros = jnp.zeros((pressure.shape[0], rt_engine.nu_grid.shape[0]))
return Contribution(
dtau_total=zeros, dtau_scatter=zeros, g_weighted_num=zeros
)
dtau = jnp.stack(dtau_per_mol, axis=0).sum(axis=0)
zeros = jnp.zeros_like(dtau)
return Contribution(dtau_total=dtau, dtau_scatter=zeros, g_weighted_num=zeros)