Skip to content

Source terms

vSmartMOM v0.6 introduces a first-class source-term abstraction. Solar beams, surface fluorescence, and (in the near future) thermal emission and lidar pulses are all concrete subtypes of AbstractSource, composed via +, and dispatched per layer through multiple dispatch — no if SFI branching, no RS_type.F₀/RS_type.SIF₀ ownership leak.

The MOM solver is mathematically affine:

optical properties define the operator A
sources         define the additive RHS b

Doubling and adding propagate (A, b) generically. The new design makes that explicit at the type level.

User API

julia
using vSmartMOM
using vSmartMOM.CoreRT

params = parameters_from_yaml("config/o2_a_band.yaml")
model  = model_from_parameters(params)

# Default — RTModel.sources defaults to a SolarBeam (unit Stokes I irradiance).
R, T = rt_run(model)

# Explicit solar beam with custom F₀ (irradiance, mW · m⁻² · cm⁻¹).
R, T = rt_run(model; sources = SolarBeam(F₀ = solar_irradiance))

# Solar + surface fluorescence (Lambertian-only emission at m=0).
R, T = rt_run(model; sources = SolarBeam() + SurfaceSIF(SIF₀ = sif_spec))

# Thermal-IR / Carbon-I-style scene (1500 K blackbody source at 2-2.4 µm).
spec_band = collect(model.atmosphere.spec_bands[1])   # cm⁻¹
sources   = BlackbodySource(1500, spec_band)          # SolarBeam with Planck F₀
R, T = rt_run(model; sources = sources)

sources can also be set at model build time:

julia
model = model_from_parameters(params; sources = SolarBeam() + SurfaceSIF())

The sources= kwarg on rt_run overrides model.sources for that specific solve; both flow through the same dispatch.

Composition

Sources compose via + and are stored as a type-stable SourceSet of concrete prepared types in the hot loop:

ExpressionResult
NoSource() + ss
SolarBeam() + SurfaceSIF()SourceSet((SolarBeam, SurfaceSIF))
(SolarBeam() + SurfaceSIF()) + ThermalEmission()SourceSet((SolarBeam, SurfaceSIF, ...))
SourceSet + SourceSetflattened — no nesting

SourceSet iteration is unrolled at compile time, so dispatching to per- source kernels is type-stable on both CPU and GPU.

Built-in source types (v0.6)

TypeWhere it contributesMath
NoSourcenowhereidentity for source composition
SolarBeamatmospheric layer j₀± per Fourier momentexact finite-δ single-scatter (Fell 1997 Eqs. 1.52-1.54)
BlackbodySourceatmospheric layer j₀±sugar around SolarBeam with F₀ = factor · π · B(ν, T)
SurfaceSIFsurface layer j₀⁻ at m=0factor-2 broadcast across Nquad streams (Lambertian only)

BlackbodySource(T, spec_band) is a constructor that returns a SolarBeam with F₀ filled from the Planck spectrum at temperature T. Use it for Carbon-I-like scenes (hot lab source illuminating CO₂/CH₄/H₂O absorption in the 2-2.4 µm range).

Future versions will add ThermalEmission (atmospheric volume Planck integral), DiffuseBoundary, and (later) LidarPulse.

Units convention

QuantityUnit
SolarBeam.F₀mW · m⁻² · cm⁻¹
SurfaceSIF.SIF₀mW · m⁻² · cm⁻¹
rt_run output (R, T, J)mW · m⁻² · sr⁻¹ · cm⁻¹
SolarBeam(F₀ = ones(...))1 mW · m⁻² · cm⁻¹ per spectral pt

All sources in a SourceSet should share these units so additive composition makes physical sense. BlackbodySource defaults to factor = π (Lambertian-disk → hemispheric irradiance) so its F₀ is comparable to a SolarBeam(F₀ = 1) baseline.

Differentiation

AbstractSourceADMode traits declare how each source's parameters participate in linearized RT:

julia
abstract type AbstractSourceADMode end
struct AnalyticSourceJacobian   <: AbstractSourceADMode end
struct ForwardDiffSourceJacobian <: AbstractSourceADMode end   # reserved for v0.7+
struct NoSourceJacobian          <: AbstractSourceADMode end

The AD seam is prepare_source(::AbstractSource, FT, pol_n, nSpec, arr_type). Above the seam, a future prepare_source_with_tangent can trace through user parameters with ForwardDiff. Below, the kernels run on plain FT arrays and the analytic Sanghavi 2014 App. C tangents (for SolarBeam) — bit-equal to today's hand-written linearization.

Dispatch architecture (for software folks)

                      AbstractSource

              ┌─────────────┼──────────────────────┐
              │             │                      │
        NoSource    SourceSet{S<:Tuple}     concrete sources

                              ┌───────────────┼─────────────────┐
                          SolarBeam    SurfaceSIF     (ThermalEmission, ...)

prepare_source lifts each user-facing source to a Prepared* form on the model's arr_type and FT:

                  AbstractPreparedSource

              ┌─────────────┼──────────────────────┐
              │             │                      │
        NoSource    SourceSet           concrete prepared sources

                              ┌───────────────┼─────────────────┐
                     PreparedSolarBeam   PreparedSurfaceSIF      ...

The hot loop calls one of two dispatchers:

contribute!(prepared_sources, j₀⁺, j₀⁻, layer_ctx...)         # forward
source_tangent!(prepared_sources, j₀⁺, j₀⁻, J̇₀⁺, J̇₀⁻, ap..., layer_ctx...)  # linearized

NoSource → no-op; SourceSet → unrolled tuple iteration with per- source dispatch; concrete prepared → kernel call.

For surface contributions, the same pattern applies but with double- dispatch on (source-type, surface-type):

surface_source_contribute!(prepared_sources, surface, surface_added_layer, m, pol_type, arch)

The dispatch table below shows how each (source × surface) pair is handled:

SourceSurfaceBody
PreparedNoSourceanyno-op
PreparedSourceSetanyiterate
PreparedSolarBeamany(currently in create_surface_layer!; will move to dispatch in a later sub-phase)
PreparedSurfaceSIFLambertianSurface*factor-2 SIF₀ broadcast (m=0 only)
PreparedSurfaceSIFnon-Lambertianno-op

Architecture invariants

  1. Sources only contribute on the elemental / surface step. Doubling and interaction propagate j₀± and J̇₀± affinely without knowing which source produced them. This is why the elastic linearized path is fully if SFI-free in v0.6: the math handles j₀±=0 as a natural no-op.

  2. Per-Fourier-moment cleanliness. Sources are prepared once before the Fourier loop; each m runs through the same operator → source- contribution → propagation pipeline.

  3. AD seam at prepare_source. User-parameter space (potentially Dual) lives above; kernel-space (plain FT) lives below.

  4. RTModel.sources is the source of truth. Defaults to SolarBeam() to preserve historical unit-Stokes-I behavior; users override at construction or per-rt_run call.

See also

API reference

Source vocabulary

vSmartMOM.CoreRT.AbstractSource Type
julia
AbstractSource

Top-level abstract type for v0.6 first-class source terms (solar, thermal, surface fluorescence, lidar, …). Distinct from the legacy AbstractSourceType (which has unused subtypes DNI / SFI and is the type slot reserved by the v0.5 dispatch design for future thermal RT — left untouched in v0.6 to avoid breaking external code).

A concrete source must satisfy the contract documented in src/CoreRT/Sources/types.jl: user-facing configuration that round-trips through prepare_source into a AbstractPreparedSource before reaching kernel code.

source
vSmartMOM.CoreRT.AbstractPreparedSource Type
julia
AbstractPreparedSource

Kernel-ready, isbits-friendly counterpart of an AbstractSource. Concrete prepared sources (e.g. PreparedSolarBeam) hold values already materialised on the active architecture — μ₀-index for the solar stream, broadcast-shaped F₀ on the device array type, attenuation buffers, etc.

Prepared sources flow through the elemental hot loop via contribute! (forward) and source_tangent! (linearized). They do not own their buffers' lifetime; allocation is pinned at model build time.

source
vSmartMOM.CoreRT.NoSource Type
julia
NoSource()

Additive identity for source composition. Useful as an explicit dispatch target for "no active source" and as the default when rt_run is called without a sources= kwarg in legacy paths.

julia
NoSource() + s == s   # for any AbstractSource s
source
vSmartMOM.CoreRT.SourceSet Type
julia
SourceSet(sources::Tuple) <: AbstractSource

Type-stable composite of source contributions. The internal tuple holds one AbstractSource per affine RHS contributor; the elemental hot loop unrolls over it at compile time.

Construct via the + operator, never directly:

julia
src = SolarBeam(F₀=F₀_spec, sza=35) + SurfaceSIF(strength=sif)
# ⇒ SourceSet((SolarBeam(...), SurfaceSIF(...)))

Iteration, length, and indexing forward to the underlying tuple.

source

Concrete sources

vSmartMOM.CoreRT.SolarBeam Type
julia
SolarBeam(; F₀ = nothing, sza = nothing)

User-facing collimated direct-beam source. Carries the solar Stokes irradiance spectrum and an optional zenith angle — without committing to a floating-point precision, so the same SolarBeam works in any model regardless of FT. prepare_source does the precision/architecture conversion against the model's FT at solve time (and that's also the AD seam — see Pillar D in the v0.6 plan).

F₀, when supplied, must be a (pol_type.n, nSpec) matrix:

  • F₀[1, :] is unpolarized Stokes-I irradiance per spectral point.

  • F₀[2:end, :] is incident polarization (Q/U/V), zero by default.

When F₀ === nothing, prepare_source materialises a unit Stokes-I vector at the model's FT — bit-equal to today's RS_type.F₀ = ones default.

Fields

  • F₀ :: Union{Nothing, AbstractMatrix}: solar irradiance Stokes vector or nothing for the unit default. Stored without an eltype constraint so users can pass a Vector{Float32} matrix into a Float64 model (or vice versa) — the conversion happens once in prepare_source.

  • sza :: Union{Nothing, Real}: advisory only in v0.6. vSmartMOM currently always reads SZA from RTModel.obs_geom.sza (which is fixed at model construction by parameters_from_yaml(...).sza). The SolarBeam.sza field is reserved for a future per-source-geometry override (rt_run will rebuild quad_points from this when set), but setting it today is a no-op — the model's geometry wins. To change SZA today, set params.sza before model_from_parameters(params).

AD mode

source_ad_mode returns AnalyticSourceJacobian; Phase 3 provides the analytic source_tangent! body (relocated from get_elem_rt_SFI_fused!).

Examples

julia
sb = SolarBeam()                                   # default unit Stokes I
sb = SolarBeam(; sza = 35.0)                       # override SZA
sb = SolarBeam(; F₀ = my_solar_irradiance)         # custom spectrum
source
vSmartMOM.CoreRT.PreparedSolarBeam Type
julia
PreparedSolarBeam{FT, AT<:AbstractMatrix} <: AbstractPreparedSource

Kernel-ready solar-beam payload. F₀ is materialised on the model's array type (CPU Array or CuArray) at the right shape (pol_type.n, nSpec). The geometry indices live in the QuadPoints struct already on the model — PreparedSolarBeam holds neither μ₀ nor iμ₀ to avoid duplicating mutable state.

Fields

  • F₀ :: AT: solar irradiance Stokes vector on the active architecture.
source
vSmartMOM.CoreRT.BlackbodySource Method
julia
BlackbodySource(T, spec_band; pol_component=1, pol_n=3, factor=π, scale=1) -> SolarBeam

Construct a SolarBeam whose F₀ is the Planck spectrum at temperature T (K) on spectral grid spec_band (cm⁻¹). Useful for modelling thermal-IR scenes such as a Carbon-I-like 2-2.4 µm setup with a hot lab source illuminating CO₂/CH₄/H₂O absorption.

F₀[pol_component, :] = factor · scale · B(ν, T) where B(ν,T) is the Planck radiance from planck_spectrum_wn in mW · m⁻² · sr⁻¹ · cm⁻¹. The default factor = π converts radiance to hemispheric irradiance for a Lambertian-disk source — set factor = 1 for a head-on collimated lab beam where F₀ is the radiance directly.

Arguments

  • T :: Real: blackbody temperature in K.

  • spec_band :: AbstractVector{<:Real}: spectral grid in cm⁻¹.

  • pol_component :: Integer = 1: which Stokes component carries the source (1=I, 2=Q, 3=U, 4=V); only 1 is physically meaningful for an unpolarized blackbody.

  • pol_n :: Integer = 3: number of Stokes components in the model (1=Stokes_I, 3=Stokes_IQU, 4=Stokes_IQUV). Match the model's polarization to avoid prepare_source shape errors.

  • factor :: Real = π: geometric factor (π for Lambertian-disk → hemisphere irradiance).

  • scale :: Real = 1: extra normalization multiplier.

Units

F₀ has units mW · m⁻² · cm⁻¹ (irradiance). All sources in a SourceSet must agree on units, so a default SolarBeam(F₀=ones(...)) should be interpreted as 1 mW · m⁻² · cm⁻¹. The radiance returned by rt_run is in mW · m⁻² · sr⁻¹ · cm⁻¹.

Example: Carbon-I-like 2-2.4 µm with a 1500 K source

julia
spec_band = collect(4167:0.1:5000)            # cm⁻¹ for 2-2.4 µm
sources   = BlackbodySource(1500, spec_band)  # SolarBeam with Planck F₀
R, T      = rt_run(model; sources = sources)  # R in mW·m⁻²·sr⁻¹·cm⁻¹
source
vSmartMOM.CoreRT.SurfaceSIF Type
julia
SurfaceSIF(; SIF₀ = nothing) <: AbstractSource

User-facing surface fluorescence source. Carries an isotropic emission spectrum and (optionally) is composed with other sources via +:

julia
sources = SolarBeam() + SurfaceSIF(SIF₀ = sif_spec)

SIF₀ is a Stokes vector (pol_type.n × nSpec) of surface-emitted hemispheric irradiance (mW · m⁻² · cm⁻¹). The unpolarized component is typically SIF₀[1, :]; higher Stokes components are zero unless the canopy emission is polarized.

When SIF₀ === nothing, prepare_source materialises a zero matrix — the source is a no-op (useful as a placeholder).

Fields

  • SIF₀ :: Union{Nothing, AbstractMatrix}: surface emission Stokes vector or nothing (zero-default). Stored without an eltype constraint — the model's FT drives precision via prepare_source, matching SolarBeam's FT-deferred design.
source
vSmartMOM.CoreRT.PreparedSurfaceSIF Type
julia
PreparedSurfaceSIF{FT, AT} <: AbstractPreparedSource

Kernel-ready surface-fluorescence payload. SIF₀ is materialised on the model's array type at the right (pol_type.n, nSpec) shape and FT precision.

source

AD-mode traits

vSmartMOM.CoreRT.AbstractSourceADMode Type
julia
AbstractSourceADMode

Trait hierarchy describing how a source's parameters participate in linearized RT. Each source declares its mode via source_ad_mode; the linearization driver dispatches to the appropriate path.

Concrete modes:

  • AnalyticSourceJacobian: hand-written source_tangent! is the authoritative path. Today's solar SFI tangents (Sanghavi 2014 App. C) belong here.

  • ForwardDiffSourceJacobian: declared but not implemented in v0.6 — reserved for v0.7+ source-parameter Jacobians via ForwardDiff through prepare_source.

  • NoSourceJacobian: the source contributes no parameters to the Jacobian (e.g. NoSource).

source
vSmartMOM.CoreRT.AnalyticSourceJacobian Type
julia
AnalyticSourceJacobian()

Source ships a hand-written source_tangent! that fills the analytic core derivatives (∂j/∂τ, ∂j/∂ϖ, ∂j/∂Z) plus any source-parameter column claims in the linearized layer's ap_J̇₀± slabs.

source
vSmartMOM.CoreRT.ForwardDiffSourceJacobian Type
julia
ForwardDiffSourceJacobian()

Reserved for v0.7+. Source-parameter Jacobians come from ForwardDiff through prepare_source_with_tangent; optical-parameter tangents remain analytic via source_tangent!. v0.6 declares this trait so future code lands without renaming the API.

source
vSmartMOM.CoreRT.NoSourceJacobian Type
julia
NoSourceJacobian()

Source contributes no parameters to the Jacobian and has no source_tangent! body. NoSource uses this mode.

source
vSmartMOM.CoreRT.source_ad_mode Method
julia
source_ad_mode(::AbstractSource) -> AbstractSourceADMode

Trait. Concrete sources override this; the default is AnalyticSourceJacobian so adding a new analytic source requires no extra ceremony.

source

Dispatch entry points

vSmartMOM.CoreRT.prepare_source Method
julia
prepare_source(sb::SolarBeam, FT::Type, pol_n::Int, nSpec::Int, arr_type) -> PreparedSolarBeam

Resolve a SolarBeam into a kernel-ready PreparedSolarBeam.

The default (F₀ === nothing) materialises a unit Stokes-I irradiance matching today's rt_run allocation — F₀[1, :] .= 1, all other Stokes components zero. A user-provided F₀ is reshaped/converted to the requested (pol_n, nSpec) shape and FT precision; when the user-side shape doesn't match, this is an error rather than a silent broadcast.

This is the AD seam (constraint 3): everything above this call can use ForwardDiff Dual numbers; the returned PreparedSolarBeam.F₀ is plain FT. A future prepare_source_with_tangent will mirror this signature and return the source-parameter Jacobian alongside.

source
vSmartMOM.CoreRT.prepare_source Method
julia
prepare_source(s::SurfaceSIF, FT, pol_n, nSpec, arr_type) -> PreparedSurfaceSIF

Resolve a SurfaceSIF into a kernel-ready PreparedSurfaceSIF. The default (SIF₀ === nothing) materialises a zero matrix on the active architecture; a user-supplied SIF₀ is precision-converted and shape-checked.

source
vSmartMOM.CoreRT.prepare_sources Method
julia
prepare_sources(srcs::AbstractSource, FT, pol_n, nSpec, arr_type) -> AbstractPreparedSource

Walk the SourceSet tuple and call prepare_source on each member, returning a structurally-parallel SourceSet of AbstractPreparedSource. NoSource round-trips unchanged. A bare single source is wrapped in a one-element SourceSet so the rest of the orchestration only has to iterate.

source
vSmartMOM.CoreRT.surface_source_contribute! Method
julia
surface_source_contribute!(prep::PreparedSurfaceSIF,
                            surface::Union{LambertianSurfaceScalar,
                                           LambertianSurfaceLegendre,
                                           LambertianSurfaceSpline},
                            surface_added_layer, m, pol_type, architecture)

Inject hemispheric SIF emission into the Lambertian surface added-layer's upwelling source vector at the m=0 Fourier moment. Bit-equal to today's inject_surface_SIF!(brdf, surface_added_layer, m, pol_type, SIF₀, arch).

The factor of 2 is (1/π) · 2π1/π converts SIF irradiance to Lambertian radiance, undoes the weight = 0.5/π azimuthal weighting applied downstream in postprocessing_vza! so the isotropic SIF contribution survives unweighted.

source
vSmartMOM.CoreRT.surface_source_contribute! Method
julia
surface_source_contribute!(prepared_sources::AbstractSource,
                            surface::AbstractSurfaceType,
                            surface_added_layer,
                            m::Int, pol_type, architecture)

Iterate prepared_sources and call the per-source per-surface kernel for each member. Empty / NoSource → no-op; SourceSet → tuple unroll.

source