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 bDoubling and adding propagate (A, b) generically. The new design makes that explicit at the type level.
User API
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:
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:
| Expression | Result |
|---|---|
NoSource() + s | s |
SolarBeam() + SurfaceSIF() | SourceSet((SolarBeam, SurfaceSIF)) |
(SolarBeam() + SurfaceSIF()) + ThermalEmission() | SourceSet((SolarBeam, SurfaceSIF, ...)) |
SourceSet + SourceSet | flattened — 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)
| Type | Where it contributes | Math |
|---|---|---|
NoSource | nowhere | identity for source composition |
SolarBeam | atmospheric layer j₀± per Fourier moment | exact finite-δ single-scatter (Fell 1997 Eqs. 1.52-1.54) |
BlackbodySource | atmospheric layer j₀± | sugar around SolarBeam with F₀ = factor · π · B(ν, T) |
SurfaceSIF | surface layer j₀⁻ at m=0 | factor-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
| Quantity | Unit |
|---|---|
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:
abstract type AbstractSourceADMode end
struct AnalyticSourceJacobian <: AbstractSourceADMode end
struct ForwardDiffSourceJacobian <: AbstractSourceADMode end # reserved for v0.7+
struct NoSourceJacobian <: AbstractSourceADMode endThe 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...) # linearizedNoSource → 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:
| Source | Surface | Body |
|---|---|---|
PreparedNoSource | any | no-op |
PreparedSourceSet | any | iterate |
PreparedSolarBeam | any | (currently in create_surface_layer!; will move to dispatch in a later sub-phase) |
PreparedSurfaceSIF | LambertianSurface* | factor-2 SIF₀ broadcast (m=0 only) |
PreparedSurfaceSIF | non-Lambertian | no-op |
Architecture invariants
Sources only contribute on the elemental / surface step. Doubling and interaction propagate
j₀±andJ̇₀±affinely without knowing which source produced them. This is why the elastic linearized path is fullyif SFI-free in v0.6: the math handlesj₀±=0as a natural no-op.Per-Fourier-moment cleanliness. Sources are prepared once before the Fourier loop; each
mruns through the same operator → source- contribution → propagation pipeline.AD seam at
prepare_source. User-parameter space (potentiallyDual) lives above; kernel-space (plainFT) lives below.RTModel.sourcesis the source of truth. Defaults toSolarBeam()to preserve historical unit-Stokes-I behavior; users override at construction or per-rt_runcall.
See also
src/CoreRT/Sources/types.jl—AbstractSource,SourceSet,NoSource, AD-mode traits.src/CoreRT/Sources/solar_beam.jl—SolarBeam,PreparedSolarBeam,BlackbodySource,source_tangent!.src/CoreRT/Sources/surface_sif.jl—SurfaceSIF,surface_source_contribute!.test/test_sources.jl— per-phase regression tests with end-to-end bit-equality assertions.The original v0.6 design note:
dev_notes/source_terms_architecture_v0_6.md.
API reference
Source vocabulary
vSmartMOM.CoreRT.AbstractSource Type
AbstractSourceTop-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.
vSmartMOM.CoreRT.AbstractPreparedSource Type
AbstractPreparedSourceKernel-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.
vSmartMOM.CoreRT.NoSource Type
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.
NoSource() + s == s # for any AbstractSource svSmartMOM.CoreRT.SourceSet Type
SourceSet(sources::Tuple) <: AbstractSourceType-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:
src = SolarBeam(F₀=F₀_spec, sza=35) + SurfaceSIF(strength=sif)
# ⇒ SourceSet((SolarBeam(...), SurfaceSIF(...)))Iteration, length, and indexing forward to the underlying tuple.
sourceConcrete sources
vSmartMOM.CoreRT.SolarBeam Type
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 ornothingfor the unit default. Stored without aneltypeconstraint so users can pass aVector{Float32}matrix into aFloat64model (or vice versa) — the conversion happens once inprepare_source.sza :: Union{Nothing, Real}: advisory only in v0.6. vSmartMOM currently always reads SZA fromRTModel.obs_geom.sza(which is fixed at model construction byparameters_from_yaml(...).sza). TheSolarBeam.szafield 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, setparams.szabeforemodel_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
sb = SolarBeam() # default unit Stokes I
sb = SolarBeam(; sza = 35.0) # override SZA
sb = SolarBeam(; F₀ = my_solar_irradiance) # custom spectrumvSmartMOM.CoreRT.PreparedSolarBeam Type
PreparedSolarBeam{FT, AT<:AbstractMatrix} <: AbstractPreparedSourceKernel-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.
vSmartMOM.CoreRT.BlackbodySource Method
BlackbodySource(T, spec_band; pol_component=1, pol_n=3, factor=π, scale=1) -> SolarBeamConstruct 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); only1is 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 avoidprepare_sourceshape 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
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⁻¹vSmartMOM.CoreRT.SurfaceSIF Type
SurfaceSIF(; SIF₀ = nothing) <: AbstractSourceUser-facing surface fluorescence source. Carries an isotropic emission spectrum and (optionally) is composed with other sources via +:
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 ornothing(zero-default). Stored without aneltypeconstraint — the model'sFTdrives precision viaprepare_source, matchingSolarBeam's FT-deferred design.
vSmartMOM.CoreRT.PreparedSurfaceSIF Type
PreparedSurfaceSIF{FT, AT} <: AbstractPreparedSourceKernel-ready surface-fluorescence payload. SIF₀ is materialised on the model's array type at the right (pol_type.n, nSpec) shape and FT precision.
AD-mode traits
vSmartMOM.CoreRT.AbstractSourceADMode Type
AbstractSourceADModeTrait 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-writtensource_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 throughprepare_source.NoSourceJacobian: the source contributes no parameters to the Jacobian (e.g.NoSource).
vSmartMOM.CoreRT.AnalyticSourceJacobian Type
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.
vSmartMOM.CoreRT.ForwardDiffSourceJacobian Type
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.
vSmartMOM.CoreRT.NoSourceJacobian Type
NoSourceJacobian()Source contributes no parameters to the Jacobian and has no source_tangent! body. NoSource uses this mode.
vSmartMOM.CoreRT.source_ad_mode Method
source_ad_mode(::AbstractSource) -> AbstractSourceADModeTrait. Concrete sources override this; the default is AnalyticSourceJacobian so adding a new analytic source requires no extra ceremony.
Dispatch entry points
vSmartMOM.CoreRT.prepare_source Method
prepare_source(sb::SolarBeam, FT::Type, pol_n::Int, nSpec::Int, arr_type) -> PreparedSolarBeamResolve 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.
vSmartMOM.CoreRT.prepare_source Method
prepare_source(s::SurfaceSIF, FT, pol_n, nSpec, arr_type) -> PreparedSurfaceSIFResolve 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.
vSmartMOM.CoreRT.prepare_sources Method
prepare_sources(srcs::AbstractSource, FT, pol_n, nSpec, arr_type) -> AbstractPreparedSourceWalk 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.
vSmartMOM.CoreRT.surface_source_contribute! Method
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, 2π undoes the weight = 0.5/π azimuthal weighting applied downstream in postprocessing_vza! so the isotropic SIF contribution survives unweighted.
vSmartMOM.CoreRT.surface_source_contribute! Method
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.