Source code for rustmatrix.scatter

"""Angular-integrated scattering quantities.

Port of ``pytmatrix.scatter``. These helpers operate on a
:class:`~rustmatrix.scatterer.Scatterer` (not the Rust core directly)
and are used by :class:`~rustmatrix.psd.PSDIntegrator` to tabulate
scattering / extinction cross-sections and asymmetry per diameter.
"""

from __future__ import annotations

import numpy as np
from scipy.integrate import dblquad

_deg_to_rad = np.pi / 180.0
_rad_to_deg = 180.0 / np.pi


[docs] def sca_intensity(scatterer, h_pol: bool = True) -> float: """Differential scattering cross-section (value of the phase function). Parameters ---------- scatterer : Scatterer h_pol : bool Returns ------- float ``Z[0,0] ± Z[0,1]`` depending on polarisation. """ Z = scatterer.get_Z() return (Z[0, 0] - Z[0, 1]) if h_pol else (Z[0, 0] + Z[0, 1])
[docs] def ldr(scatterer, h_pol: bool = True) -> float: """Linear depolarisation ratio (linear H/H or V/V ratio). Convert to dB with ``10 * log10(ldr(...))``. """ Z = scatterer.get_Z() if h_pol: return (Z[0, 0] - Z[0, 1] + Z[1, 0] - Z[1, 1]) / ( Z[0, 0] - Z[0, 1] - Z[1, 0] + Z[1, 1] ) return (Z[0, 0] + Z[0, 1] - Z[1, 0] - Z[1, 1]) / ( Z[0, 0] + Z[0, 1] + Z[1, 0] + Z[1, 1] )
[docs] def sca_xsect(scatterer, h_pol: bool = True) -> float: """Polarised scattering cross-section σ_sca [mm²]. Integrates ``sca_intensity · sin(θ)`` over the full scattering sphere using :func:`scipy.integrate.dblquad`. When a ``psd_integrator`` is attached, the pre-tabulated value is returned instead (much faster). """ if scatterer.psd_integrator is not None: return scatterer.psd_integrator.get_angular_integrated( scatterer.psd, scatterer.get_geometry(), "sca_xsect", h_pol=h_pol ) old_geom = scatterer.get_geometry() def d_xsect(thet, phi): scatterer.phi = phi * _rad_to_deg scatterer.thet = thet * _rad_to_deg I = sca_intensity(scatterer, h_pol) return I * np.sin(thet) try: xsect = dblquad(d_xsect, 0.0, 2 * np.pi, lambda x: 0.0, lambda x: np.pi)[0] finally: scatterer.set_geometry(old_geom) return xsect
[docs] def ext_xsect(scatterer, h_pol: bool = True) -> float: """Extinction cross-section σ_ext [mm²] from the optical theorem. Temporarily rotates the scatterer into forward-scatter geometry, evaluates ``S``, and returns ``2 λ Im(S_ii)``. Restores the original geometry before returning. """ if scatterer.psd_integrator is not None: try: return scatterer.psd_integrator.get_angular_integrated( scatterer.psd, scatterer.get_geometry(), "ext_xsect", h_pol=h_pol ) except AttributeError: # Fall back if the table wasn't populated. pass old_geom = scatterer.get_geometry() thet0, thet, phi0, phi, alpha, beta = old_geom try: # Forward-scattering geometry (thet == thet0, phi == phi0) for the # optical theorem: sigma_ext = (2 lambda) * Im(S_forward). scatterer.set_geometry((thet0, thet0, phi0, phi0, alpha, beta)) S = scatterer.get_S() finally: scatterer.set_geometry(old_geom) if h_pol: return 2 * scatterer.wavelength * S[1, 1].imag return 2 * scatterer.wavelength * S[0, 0].imag
[docs] def ssa(scatterer, h_pol: bool = True) -> float: """Single-scattering albedo ω = σ_sca / σ_ext (dimensionless, 0–1). Returns 0 when σ_ext is zero (e.g. perfectly non-attenuating medium). """ ext = ext_xsect(scatterer, h_pol=h_pol) return sca_xsect(scatterer, h_pol=h_pol) / ext if ext > 0.0 else 0.0
[docs] def asym(scatterer, h_pol: bool = True) -> float: """Asymmetry parameter g = ⟨cos Θ⟩ (dimensionless). g = 0 for isotropic scattering, g → 1 for forward-peaked scattering, g < 0 for backscattering-dominated regimes. """ if scatterer.psd_integrator is not None: return scatterer.psd_integrator.get_angular_integrated( scatterer.psd, scatterer.get_geometry(), "asym", h_pol=h_pol ) old_geom = scatterer.get_geometry() cos_t0 = np.cos(scatterer.thet0 * _deg_to_rad) sin_t0 = np.sin(scatterer.thet0 * _deg_to_rad) p0 = scatterer.phi0 * _deg_to_rad def integrand(thet, phi): scatterer.phi = phi * _rad_to_deg scatterer.thet = thet * _rad_to_deg cos_T_sin_t = 0.5 * ( np.sin(2 * thet) * cos_t0 + (1 - np.cos(2 * thet)) * sin_t0 * np.cos(p0 - phi) ) I = sca_intensity(scatterer, h_pol) return I * cos_T_sin_t try: cos_int = dblquad( integrand, 0.0, 2 * np.pi, lambda x: 0.0, lambda x: np.pi )[0] finally: scatterer.set_geometry(old_geom) return cos_int / sca_xsect(scatterer, h_pol)