Scattering API
Scattering computes aerosol optical properties, Fourier/Greek coefficients, and phase-matrix reconstructions used by the RT core.
Mie and Aerosol Optics
vSmartMOM.Scattering.make_mie_model Function
make_mie_model(::NAI2, aerosol::Aerosol, λ, polarization, truncation_type, r_max, nquad_radius;
r_min = DEFAULT_R_MIN, architecture = Architectures.CPU(),
precision_policy = nothing) -> MieModelConstruct a MieModel configured for the Siewert NAI-2 workflow.
Arguments
aerosol: aerosol size-distribution and refractive-index specification.λ: wavelength (must use the same length units asr_maxand the aerosol radius scale).polarization: one ofStokes_I,Stokes_IQ,Stokes_IQU,Stokes_IQUV.truncation_type: typicallyδBGE.r_max: upper radius used in size quadrature.nquad_radius: number of radius quadrature points.
Keyword arguments
r_min: lower radius of the size quadrature (defaultDEFAULT_R_MIN,1e-3µm). The radius nodes are fixed byr_min,r_maxandnquad_radiusand do not move with the size distribution; seeradius_quadrature. Requires0 < r_min < r_max.architecture:Architectures.CPU()(default) orArchitectures.GPU(). Selects the CPU vs GPU Mie path atcompute_aerosol_optical_propertiestime.precision_policy: GPUDₙ-recursion precision (NativeFloat64/DSEmulated).nothing(default) auto-selects on the GPU and is ignored on the CPU.
Returns
MieModelready forcompute_aerosol_optical_properties.
make_mie_model(::PCW, aerosol::Aerosol, λ, polarization, truncation_type, r_max, nquad_radius,
wigner_filepath::String; r_min = DEFAULT_R_MIN, ...) -> MieModelConstruct a MieModel configured for the Domke PCW workflow, loading Wigner tables from wigner_filepath via load_wigner_values. The keyword arguments are those of the NAI-2 method.
make_mie_model(::PCW, aerosol::Aerosol, λ, polarization, truncation_type, r_max, nquad_radius,
wigner_A, wigner_B; r_min = DEFAULT_R_MIN, ...) -> MieModelConstruct a MieModel configured for the Domke PCW workflow, using precomputed Wigner tables wigner_A and wigner_B. The keyword arguments are those of the NAI-2 method.
Use this overload when Wigner tensors are already in memory.
sourcevSmartMOM.Scattering.DEFAULT_R_MIN Constant
DEFAULT_R_MINDefault lower bound of the radius quadrature, 1e-3 µm (a radius of 1 nm, the smallest atmospheric aerosol particles).
The bound is fixed: the radius nodes depend only on r_min, r_max and nquad_radius, never on the size-distribution parameters, so the analytic size-parameter tangents are exact derivatives of the discretized optics. For the size distributions in the shipped configurations, at most 1e-14 of the particle number lies below 1e-3 µm; the extinction cross-section falls at least as fast as r² towards small radii, so their optical contribution is smaller still.
vSmartMOM.Scattering.radius_quadrature Function
radius_quadrature(model::MieModel, ::Type{T}=typeof(model.r_max); norm=false) -> (r, wᵣ)Radius nodes and weights of model's size-distribution quadrature: gauleg_log with model.nquad_radius nodes on [model.r_min, model.r_max]. The bounds are converted to T; the nodes and weights come back in promote_type(T, Float64) (the Gauss–Legendre abscissae are Float64).
The nodes depend only on r_min, r_max and nquad_radius, not on the size distribution, so a change of the distribution parameters changes only the weights (compute_wₓ); the linearized NAI-2 path differentiates exactly those weights. Every log-normal Mie path (NAI-2 CPU and GPU, linearized NAI-2, PCW) builds its nodes here.
vSmartMOM.Scattering.compute_aerosol_optical_properties Function
compute_aerosol_optical_properties(model::MieModel{<:NAI2,FT}, FT_out::Type) -> AerosolOpticsCompute bulk aerosol optical properties with the Siewert NAI-2 formulation on the CPU.
This method is the internal two-argument form called by the architecture/method router (_dispatch_aerosol_optics) in phase_function_autodiff.jl. The router supplies FT_out = _mie_output_type(model) (the model's own FT type parameter) so that a Float32 model produces Float32 greek coefficients. For architecture = GPU() the GPU KernelAbstractions pipeline is dispatched instead (see compute_aerosol_optical_properties_gpu).
The second argument FT_out sets the output float type of the returned AerosolOptics (greek coefficients, ω̃, k). It is supplied by the router and must be passed explicitly — there is no default.
Internal precision: the Mie cross-section sums (S₁, S₂, Cₑₓₜ, Cₛ arrays) and the Greek-coefficient angular quadrature always run in Float64 on the CPU path, regardless of FT. This is intentional — the Dₙ downward continued-fraction recursion and the size-integral sums are catastrophically unstable in Float32 for large or absorbing particles (|y| ≳ 50). The final convert.(FT_out, …) call narrows back to the requested output precision.
Reference:
S. Sanghavi, Revisiting the Fourier expansion of Mie scattering matrices in generalized spherical functions, JQSRT 136 (2014), 16-27. https://doi.org/10.1016/j.jqsrt.2013.12.015
Cross-section expressions are the standard Mie-series forms (paper Eq. (1)); Greek-coefficient Fourier expansion follows the Siewert/Domke framework discussed around paper Eq. (17).
For each radius quadrature point, Mie coefficients are used to compute extinction and scattering cross-sections:
with
Greek coefficients (α, β, γ, δ, ϵ, ζ) are then derived from the bulk phase matrix moments (Sanghavi, 2014).
Returns
AerosolOpticswithgreek_coefs,ω̃,k, andfᵗ = 1.
Notes
Uses the convention
nᵢ >= 0.Radius and wavelength units must be consistent.
compute_aerosol_optical_properties(lin::LinMode, model::MieModel{FDT,FT}, FT2::Type) where {FDT<:NAI2, FT}Reference: Suniti Sanghavi 2014, https://doi.org/10.1016/j.jqsrt.2013.12.015
Compute the aerosol optical properties and their derivatives (w.r.t. nᵣ, nᵢ and the native LogNormal log-radius location and width) using the Siewert-NAI2 linearized method.
The radius nodes (radius_quadrature) are fixed by model.r_min, model.r_max and model.nquad_radius, so the size directions act only on the normalized quadrature weights. The returned tangents are therefore the exact derivatives of the discretized forward optics of the same MieModel at any nquad_radius; their accuracy as derivatives of the continuous size integral follows the convergence of the forward quadrature.
The third argument FT2 sets the output float type for both the forward Greek coefficients and the derivative (lin) greek coefficients. It must be passed explicitly — there is no default. The internal computation always runs in Float64 for numerical stability of the Dₙ recursion (same rationale as the forward path).
Input: LinMode, MieModel Output: (AerosolOptics, linAerosolOptics)
sourcecompute_aerosol_optical_properties(model::MieModel{<:PCW,FT}, FT2::Type) -> AerosolOpticsCompute bulk aerosol optical properties with the Domke PCW formulation (Sanghavi, 2014), using precomputed Wigner tables.
The second argument FT2 sets the output float type of the returned AerosolOptics. It is supplied by the router (_dispatch_aerosol_optics in phase_function_autodiff.jl) and must be passed explicitly — there is no default.
Reference:
- S. Sanghavi, Revisiting the Fourier expansion of Mie scattering matrices in generalized spherical functions, JQSRT 136 (2014), 16-27. https://doi.org/10.1016/j.jqsrt.2013.12.015
The method evaluates AerosolOptics with:
Use make_mie_model with PCW() and Wigner inputs before calling this function.
There is no GPU PCW kernel; a GPU()-architecture PCW model falls back to this CPU implementation (with a one-time @warn) via the single-verb compute_aerosol_optical_properties(model) router.
compute_aerosol_optical_properties(model::MieModel; autodiff=false) -> AerosolOpticsSingle-verb entry point for aerosol optics. Everything (Fourier-decomposition method, compute architecture, GPU precision policy) is read off the model.
autodiff=false(default): dispatches onmodel.computation_typeandmodel.architecture:NAI2+CPU()→ the analytic CPU implementation (compute_aerosol_optical_properties2-arg method).NAI2+ a GPU architecture with a Mie pipeline (has_gpu_mietrue, CUDAGPU()or Float32MetalGPU()) → the KernelAbstractions GPU pipeline (compute_aerosol_optical_properties_gpu).NAI2/PCW+ a non-CPU architecture without a GPU Mie pipeline → one-time@warnthen CPU fallback (Mie on CPU; RT arrays still run on that architecture).PCW+ GPU → one-time@warnthen CPU fallback (no GPU PCW kernel).PCW+CPU()→ the analytic PCW implementation.
autodiff=trueis retired and raises an error. For aerosol Jacobians usecompute_aerosol_optical_properties(LinMode(), model, FT), which returns forward and hand-linearized optics. Its native size coordinates are the lognormal distribution parameters, not median radius/geometric width.
vSmartMOM.Scattering.compute_aerosol_optical_properties_nodes Function
compute_aerosol_optical_properties_nodes(radii, weights, n_real, n_imag,
wavelength, polarization, truncation;
architecture = Architectures.CPU(),
l_max = nothing,
precision_policy = nothing) -> AerosolOpticsBulk Mie aerosol optics for a CALLER-DEFINED set of size-distribution nodes, using the Siewert NAI-2 formulation (same physics/conventions as compute_aerosol_optical_properties for a log-normal MieModel, and the same AerosolOptics/GreekCoefs output shape).
This is the intended seam for sectional or tabulated size distributions that are not naturally expressed as a Distributions.jl continuous distribution — e.g. GCHPIO's TOMAS two-moment sectional bins. No log-normal (or any other analytic) size distribution is assumed anywhere in this path.
Arguments
radii::AbstractVector: wet particle radii, one per node, [μm].weights::AbstractVector: non-negative node weights (number-mixing-ratio or number-concentration weights). Need not be normalized — they are divided by their sum internally, so the returned bulk quantities are number-mean properties and are invariant to a uniform rescaling ofweights(e.g.weights .* 7.3gives the identicalAerosolOptics).n_real, n_imag: the ONE complex refractive index shared by the whole node ensemble, using vSmartMOM's conventionn = n_real - i·n_imag(n_imag ≥ 0; positiven_imagis absorption — same convention asAerosoland asserted identically).wavelength: wavelength, same units asradii(μm internally).polarization::AbstractPolarizationType: accepted for API parity withmake_mie_model/MieModel; as in the existing NAI2 path, all six Greek coefficient arrays are always computed regardless of polarization type (the type does not currently gate the computation — matchingcompute_aerosol_optical_properties's existing behavior).truncation::AbstractTruncationType: applied to the raw bulk Greek coefficients viatruncate_phase, exactly mirroring how the RT pipeline (model_from_parameters/update_model) applies aMieModel'struncation_typedownstream of the raw Mie call: aδBGEis fit only if it would shorten the natural (node-driven) Greek series; otherwise (includingNoTruncation) the untruncated series passes through withfᵗ = 0.
Keyword arguments
architecture:Architectures.CPU()(default) or a GPU architecture with a registered Mie pipeline (Architectures.has_gpu_mie, e.g. CUDAGPU()) — same architecture routing ascompute_aerosol_optical_properties(model). Non-CPU architectures without a GPU Mie pipeline warn once and fall back to CPU (mirroring the existingMieModelrouter).l_max: optional cap on the number of OUTPUT Greek terms, applied by plain array slicing (min(l_max, series_length)) AFTERtruncationis applied — aδBGEfit always sees the full natural series, and the cap then shortens whatever the truncation produced. This is an efficiency knob, independent oftruncation: because angular resolution here scales with the largest supplied node (nor_maxcap), a very wide node ensemble can produce a long natural Greek series;l_maxlets a caller who does not need the full series avoid paying for it downstream, without engagingδBGE's forward-peak re-fit.nothing(default) keeps the full length.precision_policy: GPU-only, seeMiePrecisionPolicy(NativeFloat64/DSEmulated/NativeFloat32);nothing(default) auto-selects viaArchitectures.default_mie_precision_policyexactly as theMieModelGPU path does (NativeFloat32onMetalGPU(), since Metal has no Float64 hardware at all). Ignored on the CPU path.
Angular resolution
n_max is get_n_max(maximum(2π .* radii ./ wavelength)) — i.e. derived from the size parameter of the ACTUAL largest supplied radius, with no artificial cap. n_mu = 2n_max - 1, exactly as the existing NAI2 path computes it from r_max when the size distribution comes from a log-normal MieModel.
Returns
AerosolOptics with greek_coefs, number-mean ω̃, number-mean k, and fᵗ from the requested truncation.
vSmartMOM.Scattering.compute_aerosol_extinction_nodes Function
compute_aerosol_extinction_nodes(radii, weights, n_real, n_imag, wavelength;
architecture = Architectures.CPU(),
precision_policy = nothing) -> kEXTINCTION-ONLY fast path for the caller-node Mie seam: returns just the number-mean bulk extinction cross-section k [μm²] — the SAME quantity, same convention, and the SAME arithmetic as .k from compute_aerosol_optical_properties_nodes — without computing any amplitude matrices, phase-matrix elements, or the six Greek coefficient series. This equality is EXACT (not merely close) on the CPU path, on CUDA (real hardware and the KA-CPU KernelAbstractions backend), and on the KA-CPU backend under every precision policy — see "Numerics" below for exactly what "exact" means on each path and how it is tested.
Motivation
A downstream hybrid consumer (GCHPIO's :parametric_exact_tau mode) needs only the number-mean extinction cross-section from ONE node set while taking shapes (Greek coefficients) from another. Today it would have to call the full compute_aerosol_optical_properties_nodes API and discard the amplitude matrices and all six Greek series, which dominate the cost at large angular orders. This function computes only what k actually depends on: the per-node Mie coefficients (aₙ, bₙ) and a weighted sum over nodes — no Gauss-Legendre angular quadrature, π/τ functions, amplitude functions, or Greek-coefficient projection anywhere in this path.
Arguments
Same node-ensemble arguments as compute_aerosol_optical_properties_nodes, EXCEPT there is no polarization/truncation/l_max — none of these are relevant to a bare extinction cross-section (there is no phase matrix or Greek series to polarize, truncate, or length-cap).
radii::AbstractVector: wet particle radii, one per node, [μm].weights::AbstractVector: non-negative node weights (number-mixing-ratio or number-concentration weights). Need not be normalized — divided by their sum internally, sokis invariant to a uniform rescaling ofweights.n_real, n_imag: the ONE complex refractive index shared by the whole node ensemble, conventionn = n_real - i·n_imag(n_imag ≥ 0).wavelength: same units asradii(μm internally).
Keyword arguments
architecture:Architectures.CPU()(default) or a GPU architecture with a registered Mie pipeline (Architectures.has_gpu_mie) — identical routing tocompute_aerosol_optical_properties_nodes(CPU / GPU / warn-once CPU fallback for an architecture without a GPU Mie pipeline).precision_policy: GPU-only, seeMiePrecisionPolicy(NativeFloat64/DSEmulated/NativeFloat32); ignored on the CPU path.
Returns
The number-mean bulk extinction cross-section k [μm²], as a scalar of the promoted float type of the inputs.
Validation
Identical to compute_aerosol_optical_properties_nodes: _validate_node_inputs plus sum(weights) > 0.
Numerics
CPU path: internal computation runs in IC = Float64 (same IC/FT narrowing convention as the rest of this module). Per node, this computes exactly the SAME C_ext expression the shared NAI-2 core uses — 2π/k² * dot(n_v, real.(an_v .+ bn_v)) (the internal _nai2_bulk_optics core's C_ext formula, via the shared _mie_node_ab_and_C_ext! helper) — in the SAME arithmetic order, weighted by the SAME normalized weights (same normalization step), so k is BIT-IDENTICAL (===) to compute_aerosol_optical_properties_nodes(...).k given identical (radii, weights, n_real, n_imag, wavelength).
GPU path (compute_aerosol_extinction_nodes_gpu): shares its preamble (validation, FT/RA, per-node arrays, Kernel 1) with compute_aerosol_optical_properties_nodes_gpu via the internal _prepare_node_mie_gpu helper, so both entry points run cross_sections_kernel! and the single-thread (ndrange=1) weighted_sum_kernel! reduction on IDENTICAL per-node/weight device arrays. k is confirmed EXACT-EQUALITY (==, not merely isapprox) tested for all three precision policies (NativeFloat64, DSEmulated, NativeFloat32) on both the standard and wide-range TOMAS-like node sets — CPU path (===), the KA-CPU KernelAbstractions backend (==), and real CUDA hardware (==), all in test/test_mie_nodes.jl. On MetalGPU() (NativeFloat32 only, see MiePrecisionPolicy) the same exact-equality test is wired in and expected to hold by the same reasoning, but is unverified on real Metal hardware (this package's CI has none); the test is written to activate automatically the first time it runs on a Mac with a functional Metal device.
vSmartMOM.Scattering.truncate_phase Function
truncate_phase(::NoTruncation, aero::AerosolOptics; kwargs...) -> AerosolOpticsIdentity passthrough. Returns an AerosolOptics with the same Greek coefficients, ω̃ and k, and fᵗ = 0 (the f_tr → 0 limit of Sanghavi & Stephens 2015 Eq. 8 — the truncation-modified τ*, ω*, Z* collapse to the originals).
Note: raw Mie outputs initialise fᵗ = 1 as a "untruncated yet" sentinel — passing them through unchanged would let downstream delta_m_forward interpret the 1 as "everything is in the forward peak" and silently zero out aerosol scattering. We return fᵗ = 0 so the rescaling is a true no-op.
truncate_phase(mod::δBGE, aero::AerosolOptics; reportFit=false) -> AerosolOpticsApply δ-BGE truncation to aerosol Greek coefficients.
The method removes/approximates the forward peak using a least-squares fit over all quadrature angles, then renormalizes with retained scattering fraction
All six families share the
Returns a new AerosolOptics with truncated coefficients and updated fᵗ. The common 1/c₀ normalization is required because all three fits target the unnormalized original matrix outside the forward cone; downstream δ-scaling multiplies the smooth remainder by c₀ = 1-fᵗ.
truncate_phase(mod::δBGE, aero::AerosolOptics, lin_aero::linAerosolOptics; reportFit=false)Apply δ-BGE truncation to aerosol optics and their linearized derivatives (Jacobians).
Performs the same least-squares fit as truncate_phase(mod::δBGE, aero) for β, γ, ϵ, then propagates derivatives through the truncation to obtain truncated Greek-coefficient Jacobians. Reference: Sanghavi & Stephens (2015).
All six truncated families carry the 1/c₀ renormalisation of SS2015 Eq. 8 (c₀ = 1 - fᵗ), so each derivative picks up the matching -xᵗ·ċ₀ chain-rule term with ċ₀ = ẋβ[:,1].
Arguments
mod:δBGEwithl_maxaero:AerosolOpticsto truncatelin_aero:linAerosolOpticswith derivatives w.r.t. state (e.g. nᵣ, nᵢ, r)reportFit: iftrue, print RMS fit errors
Returns
(AerosolOptics, linAerosolOptics): truncated optics and their derivatives
truncate_phase(::NoTruncation, aero::AerosolOptics, lin_aero::linAerosolOptics; kwargs...)
-> (AerosolOptics, linAerosolOptics)Identity passthrough for the linearised (Jacobian) path. Resets the raw Mie sentinel fᵗ = 1 (and the corresponding ḟᵗ) to zero, matching the two-argument truncate_phase(::NoTruncation, aero) so delta_m_truncation_lin doesn't silently zero the aerosol SSA Jacobian when params.truncation = NoTruncation().
Batched Caller-Node Mie Seam
Exact-nmax-grouped GPU batching + reusable single-owner workspace for many-ensemble columns (e.g. GCHPIO's per-layer TOMAS inventory). See proposals/batched_mie_nodes_seam.md for the full design rationale.
vSmartMOM.Scattering.prepare_mie_node_geometry Function
prepare_mie_node_geometry(radii, weights, offsets;
architecture = Architectures.CPU(), backend = nothing) -> MieNodeGeometryBuild the λ-independent, immutable MieNodeGeometry backing the batched caller-node Mie API. Concatenates E ensembles' nodes (E = length(offsets) - 1) into flat radii/weights arrays, normalizes each ensemble's weights to sum to 1 (Float64, then narrowed — the same "P2 lesson" convention as compute_aerosol_optical_properties_nodes_gpu: normalizing in Float64 before narrowing avoids a Float32 sum of large-but-finite weights overflowing to Inf), and (for GPU architectures) uploads the result once.
Arguments
radii::AbstractVector: concatenated wet particle radii for ALL ensembles, one per node, [μm], ensemble-contiguous (ensemblee's nodes occupyradii[offsets[e]+1:offsets[e+1]]).weights::AbstractVector: concatenated non-negative node weights, same layout asradii. Need not be normalized per ensemble — normalized internally, exactly as the single-ensemble API's weights are.offsets::AbstractVector{<:Integer}: lengthE+1,offsets[1] == 0, non-decreasing,offsets[end] == length(radii). Every ensemble needs at least one node (offsets[e+1] > offsets[e]).
Keyword arguments
architecture:Architectures.CPU()(default:GFT = Float64always, matching the CPU-threaded path'sIC = Float64convention exactly, no device copy made) or a GPU architecture with a registered Mie pipeline (Architectures.has_gpu_mie;GFT = float(promote_type(eltype(radii), eltype(weights))), i.e. the caller's own choice — typicallyFloat32forNativeFloat32/DSEmulated). OnMetalGPU(),radii/weightsmust beFloat32(or narrower) —Float64throwsArgumentErrorimmediately (Metal has no Float64 hardware support at all; the batched device arrays would be illegal), mirroring the single-ensemble_prepare_node_mie_gpu's Metal guard.backend:nothing(default) derives the device-array backend fromarchitecture(Architectures.ka_backend) wheneverarchitectureis NON-CPU, exactly like every other GPU entry point in this module. Pass an EXPLICIT KA backend (e.g.KernelAbstractions.CPU()) to override this — the main use case is testing a GPU-style geometry (architecturenon-CPU, soGFTis the caller's Float32/Float64 choice rather than the CPU path's forcedFloat64) on a portable backend without real GPU hardware, mirroring howcompute_aerosol_optical_properties_nodes_gputakes an explicitbackenddecoupled from any "architecture" concept. Whichever backend is resolved here (the override, or thearchitecture-derived default) is STORED on the returned geometry (MieNodeGeometry.backend) and reused verbatim by every later call, INCLUDING the top-levelcompute_aerosol_optical_properties_nodes_batched/compute_aerosol_extinction_nodes_batcheddispatchers — they launch kernels on this stored backend rather than re-deriving one fromarchitecture, so an overridden geometry stays internally consistent no matter which entry point (top-level or the explicit-backend_gpusibling) is used on it afterward. Ignored (must be leftnothing) whenarchitecture isa Architectures.CPU.
Returns
MieNodeGeometry, reusable across arbitrarily many compute_aerosol_optical_properties_nodes_batched / compute_aerosol_extinction_nodes_batched calls at different wavelengths (grouping is recomputed fresh each call — see those functions' docstrings — but the underlying node data is never re-normalized or re-uploaded).
vSmartMOM.Scattering.MieNodeGeometry Type
MieNodeGeometry{GFT, ARCH, RV, IV}Immutable, λ-INDEPENDENT, device-resident (for GPU architectures) caller-node size-distribution geometry backing compute_aerosol_optical_properties_nodes_batched / compute_aerosol_extinction_nodes_batched. Built once via prepare_mie_node_geometry and reused across many per-λ batched calls (and, in the intended GCHPIO usage, across many spectral bands) without re-uploading or re-normalizing anything.
Fields (all internal; use the accessors / pass the geometry object around,
do not construct or mutate directly)
architecture: fixed at construction; determinesGFTand whetherradii/weights/node_to_ensembleare device-resident copies (GPU) or identical to the host arrays (CPU — no copy is made).backend: the ACTUAL KA backendradii/weights/node_to_ensemblewere built on (nothingforArchitectures.CPU()) — may be an explicit override passed toprepare_mie_node_geometry'sbackendkeyword, NOT necessarilyArchitectures.ka_backend(architecture). The top-level batched dispatchers (compute_aerosol_optical_properties_nodes_batched/compute_aerosol_extinction_nodes_batched) launch kernels on THIS stored backend rather than re-deriving one fromarchitecture, so an overridden geometry stays internally consistent no matter which entry point is used.output_FT: the CALLER's ORIGINALradii/weightspromoted element type (float(promote_type(eltype(radii), eltype(weights))), captured BEFORE any internal-compute forcing) — this is what the batched CPU output narrows to (together withn_real/n_imag/wavelength's own eltypes), exactly mirroringcompute_aerosol_optical_properties_nodes's own output-type formula. Distinct fromGFTforArchitectures.CPU()geometries, whereGFTis forced toFloat64for internal compute regardless of the caller's actualradii/weightseltype (e.g. an all-Float32CPU call must still returnFloat32, notFloat64) — for GPU architecturesoutput_FT === GFTtrivially, sinceGFTis never forced there.n_ensembles,n_nodes:Eand total node count.offsets::Vector{Int}: lengthE+1, ALWAYS host-resident (needed for per-λ host-side grouping and the CPU threaded loop regardless of architecture);offsets[1] == 0,offsets[end] == n_nodes.radii_host::Vector{GFT}: host mirror of the concatenated, narrowed radii — needed every λ-call for the host-side per-ensemblen_max_globalcomputation that drives GPU grouping, even whenradiiitself is device-resident.radii::RV,weights::RV: concatenated, narrowed-to-GFTradii and PRE-NORMALIZED (per ensemble, in Float64, then narrowed — seeprepare_mie_node_geometry) weights.RV === Vector{GFT}(same object asradii_host/the host weight mirror) forArchitectures.CPU(); a device array type (CuArray/MtlArray) for GPU architectures.node_to_ensemble::IV: lengthn_nodes, 1-based ensemble id (in ORIGINAL ensemble order) per node.
GFT (the geometry's own float type) is Float64 for CPU() architectures (matching the single-ensemble CPU path's IC = Float64 internal-computation convention exactly, so the pre-normalized weights need no further narrowing or widening round-trip before being fed to the shared _nai2_bulk_optics core) and float(promote_type(eltype(radii), eltype(weights))) for GPU architectures (matching the single-ensemble GPU path's kernel type FT).
vSmartMOM.Scattering.compute_aerosol_optical_properties_nodes_batched Function
compute_aerosol_optical_properties_nodes_batched(geometry::MieNodeGeometry,
wavelength, n_real::AbstractVector, n_imag::AbstractVector,
polarization, truncation;
l_max = nothing,
precision_policy = nothing,
workspace = nothing) -> Vector{AerosolOptics}Batched sibling of compute_aerosol_optical_properties_nodes: given a MieNodeGeometry (E ensembles, prepared once via prepare_mie_node_geometry) and ONE wavelength, returns a Vector{AerosolOptics} of length E — element e is what compute_aerosol_optical_properties_nodes(radii_e, weights_e, n_real[e], n_imag[e], wavelength, polarization, truncation; l_max, precision_policy) would return for ensemble e's own (raw) nodes, in ORIGINAL ensemble order. Output type: AerosolOptics{FT_out} where FT_out is derived from geometry's ORIGINAL radii/weights eltype (MieNodeGeometry.output_FT, captured before any internal-compute-precision forcing) promoted with n_real/n_imag/wavelength's eltypes — an all-Float32 CPU call returns Float32, matching compute_aerosol_optical_properties_nodes exactly (this is a load-bearing contract: GCHPIO's consumer code narrows to its own working precision and must be able to rely on this without a manual post-hoc narrowing pass).
Bit-compatibility contract (precise, by backend)
Architectures.CPU()and KA-CPU (portable KernelAbstractions backends that aren't real CUDA/Metal hardware): bit-for-bit identical (==) to the single-ensemble path, by construction (same arithmetic order, same per-ensemble normalize-once semantics — see this file's top-of-file module docstring for the full argument).Real CUDA hardware: near-machine-precision, NOT bit-for-bit (different
ndrangelaunch shapes between the batched and single-ensemble kernel invocations can produce different, but equally valid, PTX/SASS for logically identical code — seedocs/src/pages/gpu.md's "Batched caller-node Mie seam" section for the measured magnitudes).Exact even on real CUDA:
compute_aerosol_extinction_nodes_batched's result for ensembleeequals THIS function's.kfor that same ensemble — the invariant GCHPIO's hybrid exact-τ mechanism depends on (seecompute_aerosol_extinction_nodes_batched).
See test/test_mie_nodes_batched.jl for the acceptance-gate tests (bitwise batched-vs-single on CPU/KA-CPU, batching-split invariance, workspace-reuse invariance, original-order reassembly, and the dedicated strict real-CUDA .k-equality regression test).
GPU: exact-n_max_global grouping
For a GPU geometry.architecture, ensembles are grouped by EXACT n_max_global at THIS wavelength (λ-dependent — group membership may differ between wavelengths; recomputed fresh every call), and one kernel-1 launch covers each group's concatenated nodes (per-ensemble refractive index via a node_to_ensemble index — see gpu_mie_kernels.jl's _batched sibling kernels).
CPU: no grouping
For Architectures.CPU(), this is a Threads.@threads loop over ensembles (grouping is a GPU-only concern — see the module docstring for why a single sequential "CPU batch" would regress the measured 16-thread performance this proposal is built around).
Arguments
geometry: prepared once viaprepare_mie_node_geometry; reused across as many calls (differentwavelengths,n_real/n_imag, etc.) as needed without re-normalizing or re-uploading anything.wavelength: ONE wavelength for this call (μm, matchinggeometry's radii units).n_real::AbstractVector,n_imag::AbstractVector: length-Eper-ensemble refractive index (conventionn = n_real - i·n_imag,n_imag ≥ 0for every ensemble), one complex value per ensemble (shared by all of that ensemble's own nodes — the same "one RI per ensemble" convention as the single-ensemble API).polarization,truncation: same meaning ascompute_aerosol_optical_properties_nodes;truncation(δBGE thenl_maxcap ordering) is applied PER ENSEMBLE on the host after the raw batched compute.
Keyword arguments
l_max: same meaning as the single-ensemble API, applied per ensemble.precision_policy: GPU-only, seeMiePrecisionPolicy;nothingauto-selects viaArchitectures.default_mie_precision_policy(architecture, eltype(geometry.radii_host)). Ignored on the CPU path.workspace:nothing(default, allocate-and-drop) or aMieBatchWorkspaceto reuse device scratch across calls.
vSmartMOM.Scattering.compute_aerosol_extinction_nodes_batched Function
compute_aerosol_extinction_nodes_batched(geometry::MieNodeGeometry,
wavelength, n_real::AbstractVector, n_imag::AbstractVector;
precision_policy = nothing, workspace = nothing) -> VectorBatched, extinction-only sibling of compute_aerosol_extinction_nodes — same geometry/grouping/workspace story as compute_aerosol_optical_properties_nodes_batched, but returns just Vector{k} (length E, one number-mean bulk extinction cross-section per ensemble, ORIGINAL order, output type from geometry.output_FT promoted with n_real/n_imag/wavelength – same load-bearing Float32-output contract as the full API, see its docstring), computing none of the amplitude matrices, phase-matrix elements, or Greek coefficients — see compute_aerosol_extinction_nodes's own docstring for the motivation (GCHPIO's hybrid exact-τ leg is the intended consumer of this batched variant).
Exact-even-on-CUDA invariant
compute_aerosol_extinction_nodes_batched's result for ensemble e equals compute_aerosol_optical_properties_nodes_batched's .k for that SAME ensemble EXACTLY (==), on EVERY backend including real CUDA, for all three precision policies — see that function's "Bit-compatibility contract" section for why (shared Kernel-1 + cross-section + segmented-weighted-sum code path). GCHPIO's hybrid exact-τ mechanism rests on this; see the dedicated strict real-CUDA regression test in test/test_mie_nodes_batched.jl.
vSmartMOM.Scattering.MieBatchWorkspace Type
MieBatchWorkspace(backend, FT::Type, RA::Type)Reusable, GROW-ONLY GPU scratch space for the batched caller-node Mie API, keyed on the backend VALUE (objectid(backend)) plus FT (kernel precision) and RA (reduction precision, see _mie_reduction_type) — NOT polarization: polarization does not change the raw kernel-1/reduction scratch layout, so there is no separate workspace per AbstractPolarizationType.
objectid(backend) is backend-VALUE identity, not device/stream identity: KA backend structs (KernelAbstractions.CPU(), CUDA.CUDABackend(), Metal's backend) are isbits values carrying only backend-TYPE/config fields (e.g. CUDABackend's prefer_blocks/always_inline flags) — NOT which physical GPU or CUDA stream is currently active. Two separately constructed CUDA.CUDABackend() values are objectid-EQUAL regardless of CUDA.device()/CUDA.stream() (verified: same objectid across 2 distinct GPUs on a multi-GPU host). This check therefore catches genuinely different backend TYPES/configs (a KA-CPU workspace handed a CUDA launch, or vice versa) but does NOT catch a workspace built while one CUDA device/stream was active being reused while a DIFFERENT device/stream is active — that is a real gap, not yet closed: true device/stream-aware keying (e.g. incorporating CUDA.device()/CUDA.stream() into the key) is future work. Single-device, single-stream usage (the common case) is unaffected.
Capacities are tracked independently PER SCRATCH ARRAY (grouped-node count, coefficient count, node-angle scratch, Greek-table size): a request larger than an array's current capacity reallocates JUST that array (grow-only — never shrinks, so reuse across calls with varying group sizes never thrashes). nothing (the default workspace keyword everywhere in this module) means allocate-and-drop, i.e. the pre-existing zero-workspace behavior — passing an explicit MieBatchWorkspace is a pure optimization, never a correctness requirement (see the workspace-reuse-invariance test in test/test_mie_nodes_batched.jl: warm == cold, and reuse across two different wavelengths — where group membership genuinely changes — still reproduces the fresh/cold result bit-for-bit).
NOT thread-safe. Single-owner only: one workspace per concurrent GPU stream / CPU task. The FIRST use from a given task stamps owner_task; a LATER use from a DIFFERENT task throws an ArgumentError immediately rather than silently corrupting scratch shared across two concurrent callers. This check is UNCONDITIONAL (every _ws_array! call, not gated behind bounds-checking) — it is one field compare (plus, on first use, one field write), cheap enough that making it a debug-only check would only trade a negligible cost saving for the ability to silently disable safety via @inbounds. Every use also verifies objectid(backend) === backend_id (the backend VALUE the workspace was constructed for): this catches a workspace built for one backend TYPE/config being handed to a launch against a DIFFERENT one (e.g. KA-CPU vs CUDA vs Metal, or a differently-configured CUDABackend). It does NOT distinguish CUDA devices or streams (see the caveat above) — single-device, single-stream usage only; true device/stream keying is future work.
Phase Matrix Reconstruction
vSmartMOM.Scattering.reconstruct_phase Function
reconstruct_phase(greek_coefs, μ; returnLeg = false)Reconstruct angle-space scattering-matrix elements from Greek coefficients.
Reference: Sanghavi (2014), Fourier/Greek framework around Eq. (17).
f₁₁ represents the scalar phase function and is normalized as:
Using Legendre basis matrices computed at μ, this function evaluates:
Arguments
greek_coefs:GreekCoefscoefficients.μ: cosine of scattering angles where the matrix should be reconstructed.returnLeg: iftrue, also return(P, P²).
Returns
ScatteringMatrixwhenreturnLeg=false.(ScatteringMatrix, P, P²)whenreturnLeg=true.
$(FUNCTIONNAME)(greek_coefs, μ; returnLeg = false)Returns the reconstructed elements of the 4x4 scattering matrix at positions f₁₁, f₁₂, f₂₂, f₃₃, f₃₄, f₄₄ from the greek coefficients
f₁₁ represents the phase function p for the Intensity (first Stokes Vector element) and is normalized as follows:
greek_coefsgreek coefficients (Domke Type)returnLegiffalse(default), just returnf₁₁, f₁₂, f₂₂, f₃₃, f₃₄, f₄₄, iftrue,return
f₁₁, f₁₂, f₂₂, f₃₃, f₃₄, f₄₄, P, P²(i.e. also the two legendre polynomials as matrices)
Fourier Z-Moment Assembly
Per-Fourier-moment phase operators on the quadrature grid: compute_Z_moments builds the diffuse Z⁺⁺/Z⁻⁺ blocks from Greek coefficients (optionally via the per-solve ZMomentTables precompute), and compute_Z_source_moments builds the rectangular direct-solar Z₀⁺/Z₀⁻ columns used by the external-solar SFI mode.
vSmartMOM.Scattering.compute_Z_moments Function
compute_Z_moments(mod::AbstractPolarizationType, μ, α, β, γ, δ, ϵ, ζ, m::Int)Compute moments of the phase matrix
sourcecompute_Z_moments(mod::AbstractPolarizationType, μ, α, β, γ, δ, ϵ, ζ, m::Int)Compute moments of the phase matrix.
Keywords tables/Π_pair (see ZMomentTables / make_Π_lists) supply the scatterer-independent precompute; results are identical (the same sums in the same order), only the redundant recomputation is skipped.
vSmartMOM.Scattering.compute_Z_source_moments Function
Forward and tangent solar-direction phase blocks without a square μ₀ node.
sourcecompute_Z_source_moments(mod, μ_out, μ₀, greek_coefs, m; arr_type=Array)Evaluate the two Fourier phase blocks coupling one collimated incident direction μ₀ to the diffuse output directions μ_out, without constructing an augmented square angular matrix. The angular shape is length(μ_out) × 1; after Stokes expansion the returned arrays have shape (length(μ_out)*nStokes, nStokes[, nSpec]).
Keeping the complete incident Stokes block preserves support for polarized collimated sources. For the usual unpolarized solar beam only column one is used, so its contraction is an NquadN × 1 source vector.
vSmartMOM.Scattering.ZMomentTables Type
ZMomentTables(μ, l_max)Scatterer-independent precompute for compute_Z_moments: the generalised spherical functions l_max (the longest Greek-coefficient expansion among all scatterers in the run). Build once per solve and pass via the tables keyword.
vSmartMOM.Scattering.make_Π_lists Function
make_Π_lists(mod, tables, m)Assemble the Π matrices (Sanghavi 2014, eq. 15) for Fourier moment m (0-indexed) from precomputed tables, for every l = m+1 … l_max (1-indexed). The result is shared by ALL scatterers at this m — Π does not involve the Greek coefficients. Returns (Π, Π⁻) indexed as Π[l], or nothing when m ≥ l_max (all Z moments vanish there).
Convenience Computations
Useful for cross-sections or scalar phase-function outputs without running the full RT-facing workflow.
vSmartMOM.Scattering.phase_function Function
phase_function(aerosol::Aerosol, λ, r_max, nquad_radius; r_min=DEFAULT_R_MIN)
-> (μ, w_μ, p11, C_ext, C_sca, g)Compute scalar phase-function output for a size-distributed aerosol, with the size integral on nquad_radius log-spaced Gauss-Legendre nodes in the fixed interval [r_min, r_max] (the grid of radius_quadrature).
The returned p11(μ) is normalized by the bulk scattering cross-section and satisfies:
Asymmetry factor:
Reference: Sanghavi (2014), Mie-amplitude/cross-section setup from Eq. (1), with NAI-style angular integration discussed in Secs. 3-4.
sourcephase_function(r::FT, λ::FT, nᵣ::FT, nᵢ::FT) where {FT<:AbstractFloat}
-> (μ, w_μ, p11, C_ext, C_sca, g)Monodisperse version of phase_function, for a single radius r. Returns the same tuple as the size-distribution overload.
vSmartMOM.Scattering.compute_aerosol_XS Function
compute_aerosol_XS(aerosol::Aerosol, λ, r_max, nquad_radius; r_min=DEFAULT_R_MIN)
-> (XS_ext, XS_sca, Cext_eff, Csca_eff)Compute bulk extinction/scattering cross-sections for a size-distributed aerosol without reconstructing phase matrices. The size integral uses the fixed radius interval [r_min, r_max] (the grid of radius_quadrature).
Returns
XS_ext: bulk extinction cross-sectionXS_sca: bulk scattering cross-sectionCext_eff: area-normalized extinctionCsca_eff: area-normalized scattering
with
Reference: Sanghavi (2014), Eq. (1) for particle-level C_ext/C_sca.
vSmartMOM.Scattering.compute_ref_aerosol_extinction Function
compute_ref_aerosol_extinction(model::MieModel{<:NAI2,FT}, FT_out::Type) -> RealCompute only the bulk extinction cross-section for the aerosol model:
where [r_min, r_max] (see radius_quadrature).
The second argument FT_out sets the return type. It is supplied by the caller and must be passed explicitly — there is no default. The internal cross-section computation always runs in Float64 for plain floats (same numerical stability rationale as compute_aerosol_optical_properties).
This helper avoids phase-function and Greek-coefficient reconstruction.
Reference: Sanghavi (2014), Eq. (1) for C_ext(r).
Analytic Phase Functions
Lightweight scattering sources for idealized tests, sensitivity experiments, and StandaloneSS validation. Not a separate RT implementation: each analytic source is projected into GreekCoefs and then consumed by the same CoreRT optical-property machinery as Mie-derived aerosols. phase_matrix_first_column evaluates the single-scatter phase-matrix column needed for an unpolarized direct solar beam at one exact sun-view geometry — the shared hook used by StandaloneSS vector paths for the sun-to-atmosphere-to-sensor term.
vSmartMOM.Scattering.AbstractAnalyticPhaseFunction Type
AbstractAnalyticPhaseFunctionAnalytic phase/scattering matrix source that can be converted to Greek coefficients and then used by the standard MOM optical-property path.
sourcevSmartMOM.Scattering.HenyeyGreensteinPhaseFunction Type
HenyeyGreensteinPhaseFunction(g)Scalar Henyey-Greenstein phase function, (1 - g^2) / (1 + g^2 - 2g cosΘ)^(3/2), normalized so its sphere average is (1 - g^2) / (1 + g^2 - 2g cosΘ)^(3/2), normalized so its sphere average is one.
vSmartMOM.Scattering.SyntheticPolarizedHenyeyGreensteinPhaseFunction Type
SyntheticPolarizedHenyeyGreensteinPhaseFunction(; g, polarization_fraction)Diagnostic polarizing Henyey-Greenstein-like scattering matrix. The f11 element is standard Henyey-Greenstein; f12/f11 follows the bounded toy law polarization_fraction * (1 - cosΘ^2) / (1 + cosΘ^2). This is intended for tests and sensitivity experiments, not as a Mie substitute.
vSmartMOM.Scattering.greek_coefficients Function
greek_coefficients(phase; l_max=64, nquad=max(2l_max+1, 64))Project an analytic phase/scattering matrix into Greek coefficients so it can enter the same CoreRT MOM path as Mie-derived aerosol optics.
sourcevSmartMOM.Scattering.analytic_aerosol_optics Function
analytic_aerosol_optics(phase; single_scattering_albedo=1, extinction_cross_section=1, l_max=64)Create an AerosolOptics object from an analytic phase/scattering matrix. The returned object is directly consumable by the existing CoreRT aerosol mixing and Fourier-moment code.
vSmartMOM.Scattering.phase_matrix_first_column Function
phase_matrix_first_column(greek, μ₀, μv, Δϕ, Val(N))Evaluate the first column of the scattering phase matrix for an unpolarized incident beam at one plane-parallel sun-view geometry. μ₀ and μv are positive cosines and Δϕ is the vSmartMOM relative azimuth in radians.
Pure-Gas Molecular Rayleigh
Method-explicit pure-gas molecular Rayleigh properties (no frequency redistribution). See Rayleigh and the Cabannes choice for the physics and provenance.
vSmartMOM.Scattering.AbstractMolecularRayleighMethod Type
AbstractMolecularRayleighMethodDispatch category for independently implemented molecular-Rayleigh cross-section methods with no explicit frequency redistribution. The method is an explicit argument so a refractive-index calculation can never be confused with a polarizability- tensor calculation in provenance records or calling code.
Concrete methods:
UsingRefInd: refractive index plus effective King factor (RefractiveIndexRayleighis a compatibility alias);UsingPolTen: mean polarizability and anisotropy of the molecular polarizability tensor, with the wavelength dependence selected by anAbstractPolarizabilityDispersion(defaultDispersiveAnisotropy;ConstantAnisotropyis the Raman-module tensor with a wavelength-independent anisotropy).
This abstraction is part of Scattering, not Inelastic. Only UsingRefInd is independent of the Raman-module (InelasticScattering) constants. UsingPolTen deliberately uses them: it builds fresh Raman-module molecular constants on each call and never mutates shared Raman state.
vSmartMOM.Scattering.UsingRefInd Type
UsingRefInd()
RefractiveIndexRayleigh()Select the conventional refractive-index plus effective-King-factor Rayleigh formulation without constructing frequency-shifted Raman branches. The Habitable Worlds Observatory (HWO) exo-Earth studies use this explicit method for the Ar and CO₂ cross sections. RefractiveIndexRayleigh is a compatibility alias (RefractiveIndexRayleigh === UsingRefInd) kept so existing calling code, e.g. ExoOptics, continues to work unchanged. The polarizability-tensor implementation uses the distinct dispatch type UsingPolTen; it never silently replaces this method.
vSmartMOM.Scattering.UsingPolTen Type
UsingPolTen(anisotropy::AbstractPolarizabilityDispersion = DispersiveAnisotropy())Select the polarizability-tensor Rayleigh formulation, built from the mean polarizability :N₂ are accepted). anisotropy selects their wavelength dependence:
DispersiveAnisotropy(default):and from the UsingRefIndrefractivity and King factor of N₂ and O₂, with the Buldakov temperature factor on. This is the formulation of the RT model's automatic ( depol: -1) Rayleigh depolarization.ConstantAnisotropy: the Raman-module tensor (Buldakov, wavelength-independent ), which the Raman and Cabannes cross sections use. UsingPolTen(ConstantAnisotropy())reproduces theUsingPolTen()results of the v2.2 pre-releases.
Both go through the Raman-module routines getMolecularConstants, compute_effective_coefficents! and compute_σ_Rayl_coeff! and never mutate shared state. See molecular_rayleigh_properties for the formulation.
vSmartMOM.Scattering.AbstractPolarizabilityDispersion Type
AbstractPolarizabilityDispersionWavelength dependence of the N₂ and O₂ polarizability-tensor invariants, the mean polarizability
DispersiveAnisotropy(default forUsingPolTenand for the automaticdepol: -1Rayleigh depolarization of the RT model):from the UsingRefIndrefractivity through the Lorentz–Lorenz relation andfrom its King factor; ConstantAnisotropy(default for the Raman/Cabannes cross sections): the published Raman-module tensor, Buldakov-typeand a wavelength-independent .
See docs/dev_notes/molecular_rayleigh_dispersion.md for the sources and the size of the difference.
vSmartMOM.Scattering.DispersiveAnisotropy Type
DispersiveAnisotropy()Polarizability-tensor invariants of N₂ and O₂ that follow the refractive-index data of UsingRefInd:
where UsingRefInd King factor (Bates 1984 for N₂; He et al. 2021, Table 1, for O₂), and UsingRefInd value UsingRefInd fits: N₂ 254–2059 nm, O₂ 288–1542 nm measured (longer wavelengths are dispersion extrapolations, which are smooth because
H₂ has no refractive-index fit in UsingRefInd; for H₂ this formulation uses the Raman-module tensor of ConstantAnisotropy unchanged.
vSmartMOM.Scattering.ConstantAnisotropy Type
ConstantAnisotropy()The polarizability tensor of the Raman module (getMolecularConstants in src/Inelastic/src/molecular_constructors.jl): mean polarizability with the Buldakov et al. single-pole dispersion and temperature dependence, test/test_ray_cab_raman.jl), and they remain the default of the Raman and Cabannes cross sections. For N₂ and O₂ the constant anisotropy equals the King-factor anisotropy of DispersiveAnisotropy near 488 nm (within 0.03 % for N₂ and 0.32 % for O₂); it is 2 % (N₂) and 4 % (O₂) low at 400 nm and 4 % (N₂) and 8 % (O₂) high at 2060 nm.
vSmartMOM.Scattering.molecular_rayleigh_properties Function
molecular_rayleigh_properties(::UsingRefInd, species, wavelength_nm)
molecular_rayleigh_properties(species, wavelength_nm)Return no-frequency-redistribution Rayleigh properties for a pure gas at vacuum wavelength wavelength_nm (nm). Supported species are :He, :Ar, :N2, :O2, and :CO2 (Unicode aliases such as :N₂ are accepted).
The two-argument form is equivalent to passing UsingRefInd(). The machine-readable provenance record (molecular_rayleigh_provenance) currently covers Ar and CO₂ only; the He, N₂ and O₂ coefficient sets have no such record yet (their sources are listed below).
The returned named tuple contains
refractive_index: refractive index at the species reference number density: 2.546899×10¹⁹ cm⁻³ (288.15 K, 1013.25 hPa) for He, Ar, N₂ and CO₂ and 2.68678×10¹⁹ cm⁻³ (273.15 K, 1013.25 hPa) for O₂. A fit given at another density is converted through the Lorentz–Lorenz relation ((n²-1)/(n²+2)proportional toN). It is stored asitself, so a Float32result resolves only to abouteps(1f0)/(n-1)relative (eps(1f0)≈ 1.2e-7): a few times 1e-4 for N₂, O₂, Ar and CO₂, and a few times 1e-3 for He. The cross section is computed from the refractivity directly and is not affected;king_factor: molecular-anisotropy correction; depolarization: depolarization ratioin the convention consumed by get_greek_rayleigh, related by ;cross_section_cm2: Rayleigh scattering cross section in cm² molecule⁻¹.
The individual dispersion and King-factor sources are returned by molecular_rayleigh_provenance for Ar and CO₂. Wilmouth and Sayres (2019) provide a direct 333–363 nm check of both gases. The cross-section expression is
where N in molecule cm⁻³, the number density at which the dispersion fit gives molecular_rayleigh_provenance and mark extrapolation. The returned conventional total molecular-Rayleigh cross section uses an effective King factor; it is not a separately computed Cabannes-only cross section. No species-specific rotational or vibrational Raman redistribution is built.
N₂ and O₂ dispersion and King factors
Each gas joins two dispersion fits at the wavenumber where they agree, so
N₂,
22 919.551 cm⁻¹ ( 436.309 nm): Bates (1984), at 288.15 K (He et al. 2021, Table 1; fitted over 254–468 nm). Longer wavelengths: Peck and Khanna (1966), at 0 °C and 760 Torr (measured 467.9–2058.7 nm; checked against the refractiveindex.infoN2/nk/Peck-0C.ymlrecord). The two fits differ by 1.4×10⁻⁴ in at 468 nm; joining them there would leave a 0.03 % jump in. O₂,
21 428.071 cm⁻¹ ( 466.678 nm): Bates (1984), at 273.15 K (He et al. 2021, Table 1; fitted over 288–546 nm). Longer wavelengths: Egan (2026), Eq. (8), , in µm⁻¹, at 20 °C and 100 kPa, constrained by refractivity at 0.633 and 1.542 µm (uncertainty 7×10⁻⁶ in ), visible data and a 0.254/0.436 µm ratio. At 546 nm the two fits differ by 0.10 % in (0.21 % in); at 1.6 µm the extrapolated Bates fit is 0.43 % high in .King factors: N₂
(Bates 1984); O₂ (He et al. 2021, Table 1). The O₂ constant 1.09 is He et al.'s; Bates (1984) and Bodhaine et al. (1999) use 1.096, which gives a 0.54 % larger O₂ cross section (0.54 % at 400 nm, 0.55 % at 1.6 µm). Both King factors are fitted to visible depolarization data and extrapolated elsewhere.
References
Bates (1984), Planet. Space Sci. 32, 785–790, https://doi.org/10.1016/0032-0633(84)90102-8.
Bodhaine et al. (1999), J. Atmos. Oceanic Technol. 16, 1854–1861, https://doi.org/10.1175/1520-0426(1999)016<1854:ORODC>2.0.CO;2.
Egan (2026), Meas. Sci. Technol. 37, 255003 (CC BY 4.0), https://doi.org/10.1088/1361-6501/ae6e94.
He et al. (2021), Atmospheric Chemistry and Physics 21, 14927–14940, https://doi.org/10.5194/acp-21-14927-2021.
Peck and Khanna (1966), J. Opt. Soc. Am. 56, 1059–1063, https://doi.org/10.1364/JOSA.56.001059.
Sneep and Ubachs (2005), JQSRT 92, 293–310, https://doi.org/10.1016/j.jqsrt.2004.07.025.
Wilmouth and Sayres (2019), JQSRT 234, 32–39, https://doi.org/10.1016/j.jqsrt.2019.05.031.
molecular_rayleigh_properties(method::UsingPolTen, species, wavelength_nm;
temperature_K = 288.15,
number_density_cm3 = 2.546899e19)Polarizability-tensor variant. Species: :N2, :O2, :H2. For any other species an ArgumentError is thrown that names the species and suggests UsingRefInd().
Tensor invariants, selected by method.anisotropy:
DispersiveAnisotropy(UsingPolTen()), N₂ and O₂: and from theUsingRefIndLorentz–Lorenz factor, density and King factor at cm⁻¹, with the Buldakov temperature factor . At (288.15 K for N₂ below 436.3 nm, 273.15 K above; 273.15 K for O₂ below 466.7 nm, 293.15 K above) the cross section and depolarization equal theUsingRefIndvalues; at other temperatures onlychanges (by 1.8×10⁻⁶ K⁻¹ for N₂ and about 2.6×10⁻⁶ K⁻¹ for O₂ near 288 K). H₂ uses the constant-anisotropy tensor below. ConstantAnisotropy(Raman-module constants ofgetMolecularConstants(::N₂/::O₂/::H₂, vmr)): mean polarizability with the Buldakov et al. single-pole dispersion and temperature dependence, , and the anisotropy , independent of wavelength and temperature.
Derived quantities (no further inputs):
depolarization (the module'srho_depol_Rayl, the same convention as the RI method andget_greek_rayleigh);king_factor ;cross_section_cm2 , i.e. the module'sσ_Rayl_coeff · ν⁴(cm² molecule⁻¹,in cm³, in cm⁻¹). This is the total (Cabannes plus rotational Raman) molecular Rayleigh cross section, as for the RI method; refractive_index: the Lorentz–Lorenz equivalent atnumber_density_cm3(default 2.546899e19 molecule cm⁻³, 288.15 K and 1013.25 hPa). Passnumber_density_cm3 = nothingto returnrefractive_index = nothing. The cross section, depolarization and King factor are per molecule and do not depend onnumber_density_cm3.
The default temperature_K = 288.15 K (kept by decision 13d, 2026-10-19) is the temperature of the default number_density_cm3, the standard state of the He, Ar, N₂ (λ < 436 nm) and CO₂ RI fits and of the Bates (1984) and Bodhaine et al. (1999) cross sections. It therefore compares the tensor with those data at their own state; the O₂ RI refractive index refers to 273.15 K and 2.68678e19 molecule cm⁻³, so compare O₂ refractive indices at that density. The temperature enters only through the small Buldakov factor on
The internal evaluation is always in Float64 (Float32); results are converted to the floating-point type of float(wavelength_nm). The method is therefore not ForwardDiff-aware (dual numbers are not propagated) and does not gain precision from BigFloat inputs. As for the RI method, a Float32 refractive_index resolves eps(1f0)/(n-1) relative (up to ~1e-3, for H₂). The Raman-module polarizability constants and their temperature coefficients come from Buldakov et al.; the O₂ and H₂ anisotropies from Asawaroengchai & Rosenblatt (1980), per the source comments. The H₂ tensor is checked against the Peck & Huang (1977) refractivity and the Raj, Hamaguchi & Witek (2018) anisotropy in test/test_molecular_rayleigh.jl.
vSmartMOM.Scattering.molecular_rayleigh_cross_section_ratio Function
molecular_rayleigh_cross_section_ratio(method::AbstractMolecularRayleighMethod,
species, wavelength_nm,
reference_nm)
molecular_rayleigh_cross_section_ratio(::UsingPolTen, species,
wavelength_nm, reference_nm;
temperature_K = 288.15)
molecular_rayleigh_cross_section_ratio(species, wavelength_nm, reference_nm)Return wavelength_nm and reference_nm. This ratio is the appropriate scaling when a comparison fixes the Rayleigh optical depth at one reference wavelength and varies molecular identity. For UsingPolTen both cross sections are evaluated at the same temperature_K; the number density cancels. The three-argument form uses UsingRefInd.
vSmartMOM.Scattering.molecular_rayleigh_provenance Function
molecular_rayleigh_provenance(::UsingRefInd, species)Return the coefficient provenance and separate evidence intervals for the refractive-index/King-factor coefficients of Ar and CO₂. The primary RI measurement range, adopted dispersion-formula envelope, King-factor support, and direct scattering checks are reported as distinct fields, so callers can tell measured or directly validated coverage from formula extrapolation.
Ar uses the Peck–Fisher dispersion relation and unity King factor. CO₂ uses the Bideau-Mehu dispersion relation (scaled to the stated reference density) and the Sneep–Ubachs King-factor fit to Alms–Burnham–Flygare measurements. Wilmouth and Sayres (2019) directly measured both gases at 333–363 nm; Thalman et al. (2014) checked Ar in selected regions between 350 and 660 nm; and He et al. (2021) provide a continuous 307–725 nm direct check for CO₂. This provenance API is currently limited to Ar and CO₂.
sourcevSmartMOM.Scattering.get_greek_rayleigh Function
get_greek_rayleigh(depol)Returns the greek coefficients (as GreekCoefs) of the Rayleigh phase function given depolarization value.
depolDepolarization (best use 0 as default )
Wigner Utilities (PCW)
vSmartMOM.Scattering.compute_wigner_values Function
compute_wigner_values(m_max::Integer, n_max::Integer, l_max::Integer)Compute the Wigner 3j values for all (m, n, l) combinations up to m/n/l max, for (m1, m2, m3) = (-1, 1, 0) (wigner_A) and (-1, -1, 2) (wigner_B)
sourcecompute_wigner_values(N_max)Shorthand for compute_wigner_values(2N_max + 1, N_max + 1, 2N_max + 1)
sourcevSmartMOM.Scattering.save_wigner_values Function
save_wigner_values(filepath, wigner_A, wigner_B)Save the Wigner A and Wigner B matrices at the given filepath
sourcevSmartMOM.Scattering.load_wigner_values Function
load_wigner_values(filename)Load the Wigner A and Wigner B matrices from the given filepath
sourceCore Abstract Interfaces
vSmartMOM.Scattering.AbstractAerosolType Type
type AbstractAerosolTypeAbstract aerosol type
sourcevSmartMOM.Scattering.AbstractFourierDecompositionType Type
type AbstractFourierDecompositionTypeAbstract aerosol Fourier Decomposition computation type (NAI2 and PCW)
sourcevSmartMOM.Scattering.AbstractPolarizationType Type
type AbstractPolarizationTypeAbstract Polarization type
sourcevSmartMOM.Scattering.AbstractTruncationType Type
AbstractTruncationTypeAbstract supertype for phase-function truncation methods. All concrete methods are dispatched through truncate_phase and supply l_max(t) (the per-band Legendre cutoff that the RT pipeline allocates for). Subtypes:
NoTruncation— identity. Use when the phase function has no sharp forward peak (canopy, isotropic scattering, smooth Rayleigh).δBGE— δ-BGE-fit (Sanghavi & Stephens 2015, JQSRT 159 §3); recommended for hyperspectral retrievals.
The atmospheric Δ_angle (forward exclusion half-angle) lives inside the truncation type that needs it, not as a free parameter on CoreRT.vSmartMOM_Parameters — different methods have different hyper-parameters and NoTruncation has none.
Output Types
vSmartMOM.Scattering.GreekCoefs Type
GreekCoefs{FT<:Real}Expansion coefficients of the 4×4 scattering (phase) matrix in generalised spherical functions (the "Greek" coefficients). Six independent coefficient vectors (α, β, γ, δ, ϵ, ζ) fully describe the azimuthal Fourier decomposition of the scattering matrix B for a given particle or mixture. See Eq. 16 in Sanghavi (2014) for the mapping to B elements.
For scalar (intensity-only) RT, only β (the phase-function expansion) is used.
Fields
α: Greek matrix coefficient α, is in B[2,2]β: Greek matrix coefficient β, is inB[1,1](only important one for scalar!)γ: Greek matrix coefficient γ, is in B[2,1],B[1,2]δ: Greek matrix coefficient δ, is in B[4,4]ϵ: Greek matrix coefficient ϵ, is in B[3,4] and - in B[4,3]ζ: Greek matrix coefficient ζ, is in B[3,3]
vSmartMOM.Scattering.Aerosol Type
AerosolAerosol microphysical properties: particle size distribution and complex refractive index. Used as input to MieModel for computing single-scattering optical properties via Lorenz-Mie theory.
The refractive index convention is n = nᵣ - i·nᵢ, where positive nᵢ indicates absorption.
The struct is parameterized by the refractive-index element type FT. FT is typically a plain AbstractFloat (Float64/Float32) but may also be a ForwardDiff.Dual so the autodiff path can track derivatives with respect to nᵣ/nᵢ; for that reason FT is intentionally not constrained to <:AbstractFloat. The outer convenience constructor Aerosol(dist, nᵣ, nᵢ) promotes nᵣ and nᵢ to a common type, so existing call sites continue to work unchanged.
Fields
size_distribution::ContinuousUnivariateDistribution: Particle radius distribution (e.g.,LogNormal). Units: μm.nᵣ::FT: Real part of the refractive index (relative to air).nᵢ::FT: Imaginary part of the refractive index (absorption).
Example
using Distributions
aer = Aerosol(LogNormal(log(0.3), 0.4), 1.3, 0.01)vSmartMOM.Scattering.AerosolOptics Type
AerosolOptics{FT<:Real}Computed aerosol single-scattering optical properties for one aerosol type at one (or more) wavelengths. Produced by integrating the Mie solution over the particle size distribution.
Fields
greek_coefs: Greek matrixω̃: Single Scattering Albedok: Extinction cross-sectionfᵗ: Truncation factorderivs: Derivativesphase_ν: Wavenumber nodes (cm⁻¹) at which the post-truncation phase coefficients were evaluatedphase_greek: Post-truncation Greek coefficients atphase_ν; retained so Z is evaluated only at these nodesphase_greek_raw: UNTRUNCATED Greek coefficients atphase_ν(or at the single Mie wavelength whenphase_ν === nothing). Retained when δ-BGE truncation actually removed a forward peak, so the TMS single-scattering correction can evaluate the exact phase function at the view geometries without a rebuild.nothingwhen no truncation occurred (exact == truncated).
vSmartMOM.Scattering.MieModel Type
MieModel{FDT<:AbstractFourierDecompositionType, FT, ARCH}Configuration for a Lorenz–Mie scattering computation. Specifies the aerosol (size distribution + refractive index), wavelength, polarization type, truncation strategy, integration parameters, and the compute architecture. The size integral uses nquad_radius Gauss–Legendre nodes in ln r on the fixed interval [r_min, r_max] (radius_quadrature). The computation_type selects between NAI-2 (Siewert) and PCW (Domke) Fourier decomposition algorithms.
Pre-computed Wigner symbol tables (wigner_A, wigner_B) can be supplied for PCW; they default to trivial 1×1×1 placeholders (of element type FT) when unused (NAI2 path).
FT semantics (three orthogonal precision axes)
FT is the output float type of the returned Greek coefficients and optical scalars. It is carried by λ, r_max, r_min, and aerosol (which must all share the same FT) so that the entire MieModel is FT-consistent. This is independent of two other precision choices:
FT(output type) — what the user consumes downstream in the RT pipeline.The internal
Dₙcontinued-fraction recursion: on the CPU path the recursion is always promoted toFloat64for numerical stability regardless ofFT(see_mie_dn_recursion!inmie_helper_functions.jl);Dualinputs keep native arithmetic for AD compatibility.precision_policy(GPU only) —NativeFloat64(default on CUDA) runs the GPUDₙrecursion in hardware FP64;DSEmulateduses Float32 double-single pairs (for Metal/L40S). This axis is inert on the CPU path.
Architecture dispatch
architecture::ARCH is either Architectures.CPU() (default) or Architectures.GPU(). compute_aerosol_optical_properties(mie_model) dispatches on it: NAI2+CPU → analytic CPU path; NAI2+GPU → the KernelAbstractions GPU pipeline; PCW+GPU → CPU fallback (no GPU PCW kernel).
Fields
computation_typeaerosol: Aerosol microphysics; element type must match the model'sFTλ: Wavelength[μm]; must have the same float type asr_maxpolarization_typetruncation_typer_max: Maximum radius[μm]r_min: Minimum radius[μm]; fixed, independent of the size distribution (defaultDEFAULT_R_MIN)nquad_radius: Number of quadrature points for integration over size distributionwigner_A: Precomputed Wigner ν=0 table (PCW only; trivial 1×1×1 placeholder for NAI2)wigner_B: Precomputed Wigner ν=2 table (PCW only; trivial 1×1×1 placeholder for NAI2)architecture: Compute architecture (CPU()orGPU()); selects CPU vs GPU Mie pathprecision_policy: GPU precision policy (NativeFloat64/DSEmulated);nothing= auto-select on GPU, ignored on CPU
Linearized Output Types
vSmartMOM.Scattering.linGreekCoefs Type
struct GreekCoefs{FT}A struct which holds all Greek coefficient lists (over l) in one object. See eq 16 in Sanghavi 2014 for details.
Fields
α̇: Greek matrix coefficient α, is in B[2,2]β̇: Greek matrix coefficient β, is inB[1,1](only important one for scalar!)γ̇: Greek matrix coefficient γ, is in B[2,1],B[1,2]δ̇: Greek matrix coefficient δ, is in B[4,4]ϵ̇: Greek matrix coefficient ϵ, is in B[3,4] and - in B[4,3]ζ̇: Greek matrix coefficient ζ, is in B[3,3]
vSmartMOM.Scattering.linAerosolOptics Type
struct AerosolOpticsA struct which holds all computed aerosol optics
Fields
lin_greek_coefs: Greek matrixω̃̇: Single Scattering Albedok̇: Extinction cross-sectionḟᵗ: Truncation factorphase_ν: Wavenumber nodes corresponding tophase_lin_greekphase_lin_greek: Post-truncation Greek-coefficient tangents retained at phase nodes
Fourier Decomposition Modes
vSmartMOM.Scattering.NAI2 Type
type NAI2Perform Siewart's numerical integration method, NAI-2, to compute aerosol phase function decomposition. See: http://adsabs.harvard.edu/full/1982A%26A...109..195S
sourcevSmartMOM.Scattering.PCW Type
type PCWPerform Domke's Precomputed Wigner Symbols method, PCW, to compute aerosol phase function decomposition. See: http://adsabs.harvard.edu/full/1984A%26A...131..237D
sourcePolarization Types
vSmartMOM.Scattering.Stokes_IQUV Type
struct Stokes_IQUV{FT<:AbstractFloat}A struct which defines full Stokes Vector ([I,Q,U,V]) RT code
Fields
n: Number of Stokes components (int)D: Vector of lengthnfor ... (see eq in Sanghavi )I₀: Incoming Stokes vector for scalar only
vSmartMOM.Scattering.Stokes_IQU Type
struct Stokes_IQU{FT<:AbstractFloat}A struct which defines Stokes Vector ([I,Q,U]) RT code
Fields
n: Number of Stokes components (int)D: Vector of lengthnfor ... (see eq in Sanghavi )I₀: Incoming Stokes vector for scalar only
vSmartMOM.Scattering.Stokes_IQ Type
struct Stokes_IQ{FT<:AbstractFloat}A struct which defines Stokes Vector ([I,Q]) RT code.
The CoreRT solvers reject Stokes_IQ: neglecting U in multiple scattering can cause intensity errors of several percent (in the tested aerosol scene, largest in the principal plane). The type remains for the Scattering-level functions and StandaloneSS ExactSSConfig single-scattering runs. Use Stokes_IQU for RT.
Fields
n: Number of Stokes components (int)D: Vector of lengthnfor ... (see eq in Sanghavi )I₀: Incoming Stokes vector for scalar only
vSmartMOM.Scattering.Stokes_I Type
struct Stokes_I{FT<:AbstractFloat}A struct which define scalar I only RT code
Fields
n: Number of Stokes components (int)D: Vector of lengthnfor ... (see eq in Sanghavi )I₀: Incoming Stokes vector for scalar only
Phase-function Truncation
vSmartMOM.Scattering.AutoTruncation Type
AutoTruncation()Phase D — deferred-decision marker for truncation: auto in YAML (mirrors VLIDORT's DO_DELTAM_SCALING philosophy). At model_from_parameters time it resolves deterministically:
No aerosol scattering, or all aerosols'
length(greek.β) - 1fits withinstream_l_cap⇒NoTruncation()(typical for Rayleigh-only or smooth-aerosol scenes).Aerosols with
phase_lmax > stream_l_cap⇒δBGE(stream_l_cap)(the retiredΔ_angleis always 0).
The resolver emits an @info line stating which branch was taken so the user can always see what was applied. AutoTruncation is deliberately not threaded through the Mie/RT kernels — it is a build-time placeholder that gets replaced before any kernel sees it.
User-facing knobs in YAML:
| Value | Meaning |
|---|---|
truncation: auto | This deferred-decision mode (Phase D recommended) |
truncation: NoTruncation() / null | Exactly no transform; errors if coefs exceed cap |
truncation: δBGE(N, Δ) | Explicit; used for benchmarks / cross-validation (Δ is retired and forced to 0) |
vSmartMOM.Scattering.NoTruncation Type
NoTruncation(; l_max=typemax(Int))Identity truncation — phase functions are passed through unchanged.
This is the correct choice for radiative transfer through media whose phase function has no sharp forward peak: canopy bi-Lambertian scattering (the f_tr → 0 limit of Sanghavi & Stephens 2015 Eq. 8 is exactly the identity), isotropic scattering, and smooth Rayleigh. For Mie aerosol or ice-cloud forward peaks use δBGE instead.
vSmartMOM.Scattering.δBGE Type
δBGE{FT}(l_max)δ-BGE-fit truncation, vector form (Sanghavi & Stephens 2015, JQSRT 159, §3 — extension of Hu et al. 2000 to vector RT). Fits truncated Legendre coefficients over the full angular domain and renormalises by the retained scattering fraction Δ_angle is retired and always 0; see the field docs.)
Recommended over plain δ-m for hyperspectral retrievals because δ-m has known DSE and PTE errors near exact backscatter (Sanghavi & Stephens 2015 §2.4).
Fields
l_max: Truncation length for Legendre termsΔ_angle: RETIRED — always0(design decision 2026-08-23). The δBGE least-squares fit always uses the full angular domain; the former forward-peak exclusion cone was honoured only by the forwardtruncate_phase, never by the linearized overload, so any non-zero value made LinMode Jacobians differentiate a different truncated phase function than the forward model evaluated. The field remains (fixed at zero) only so stored objects and printing stay compatible.