Source terms
vSmartMOM v0.6 introduces a first-class source-term abstraction. Solar beams, surface fluorescence, atmospheric thermal emission, and future 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 = read_parameters(joinpath(pkgdir(vSmartMOM), "config", "quickstart.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; pol_n=params.polarization_type.n) # 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.
Entry points that take an explicit mode object (rt_run(rs, model, i_band), rt_run_ss(rs, model, i_band), rt_run_toa(rs::RRS, model) and the linearized rt_run(rs, model, lin_model, NAer, NGas, NSurf, i_band)) also accept the legacy preset rs.F₀ = F (a nonzero (nStokes, nSpec) matrix). The solar F₀ of a solve is then, in order of precedence: the SolarBeam of an explicit sources= keyword, the preset rs.F₀, then model.sources. A sources= keyword applies to its own call only: rs.F₀ is left as it was, so the next call without sources= uses the preset again (or model.sources when there is none).
At solve time, an explicit SolarBeam.sza must match the model geometry. Change params.sza before construction or use remake_geometry to change the illumination angle. Multiple solar/blackbody beams throw ArgumentError; for one geometry, sum their irradiance matrices into one SolarBeam. These checks also cover linearized, single-scatter, TOA and split solves.
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 · scale · 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).
ThermalEmission implements the atmospheric volume Planck source in forward RT. Its end-to-end Jacobians remain unsupported. DiffuseBoundary and LidarPulse are future source types.
Units convention
| Quantity | Unit |
|---|---|
SolarBeam.F₀ | mW · m⁻² · cm⁻¹ |
SurfaceSIF.SIF₀ | mW · m⁻² · cm⁻¹ |
SurfaceSIF.SIF755 | same radiance unit as output Stokes I/Q/U/V |
SurfaceSIF.slope | output radiance unit per nm |
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 = π, which gives πB, the exitance of a Lambertian emitter at T; for the irradiance from a distant disk of radius R at distance d, also set scale = (R/d)² (2.1629e-5 for the Sun at 1 AU). SolarBeam.F₀ is an irradiance, so factor = 1 alone does not describe a collimated lab beam (it leaves the radiance B in F₀); for lab illumination set factor · scale to the solid angle subtended by the source, or the throughput of the optics, that converts B into the irradiance of the beam.
For retrievals, SurfaceSIF(SIF755=..., slope=..., wavelength_nm=...) uses L_SIF(λ) = SIF755 + slope*(λ_nm-755). These two values are state-vector parameters in radiance units. A nonzero SIF amplitude or slope requires an explicit non-unit Fraunhofer spectrum in SolarBeam(F₀=...); the unit default is rejected. This checks for supplied non-unit irradiance; it does not validate calibration or Fraunhofer structure. The modern OCO parameterization uses SIF760, mSIF, and wavenumber_cm1; see the source configuration reference for both coordinate conventions, units, and current composition restrictions.
Differentiation
Forward source support and a declared AD mode do not establish end-to-end Jacobian support. The current linearized driver accepts SolarBeam, SurfaceSIF, NoSource, and compositions of these. It rejects thermal and other source types whose complete tangent propagation is not implemented. The opt-in jacobian_adding=:source path additionally restricts surfaces and observers to its supported Lambertian endpoint configurations.
To extend this support, implement source values and derivatives through elemental construction, doubling, adding, surface coupling, and output reconstruction, then validate against forward finite differences. Merely declaring AnalyticSourceJacobian or ForwardDiffSourceJacobian is insufficient.
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 | nonzero/retrievable SIF rejected at solve time |
Zero prescribed SIF is a valid placeholder on any surface. Retrievable SIF requires injection support even at zero amplitude because its derivatives are nonzero. Split surface replay validates the cached prepared sources against the replacement surface.
To support fluorescence on a new surface, implement both surface_source_contribute! and surface_source_contribute_lin! for PreparedSurfaceSIF and your surface type, then specialize CoreRT.supports_surface_sif(::YourSurface) = true. The shared validation uses this trait before entering the kernels.
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 aMatrix{Float32}into aFloat64model (or vice versa) — the conversion happens once inprepare_source.sza :: Union{Nothing, Real}: optional solar zenith angle in degrees. When supplied, it must match the model geometry at the model's floating-point precision; a mismatch throwsArgumentErrorat solve time. To change SZA, setparams.szabeforemodel_from_parameters(params)or useremake_geometry. A solve supports at most oneSolarBeam(includingBlackbodySource); sum irradiance spectra into one beam for a shared geometry.
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) # requires model SZA = 35 degrees
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 = π gives πB, the exitance of a Lambertian emitter at T; for the irradiance from a distant disk of radius R at distance d, also set scale = (R/d)² (2.1629e-5 for the Sun at 1 AU). F₀ is an irradiance, so factor = 1 alone does not describe a collimated lab beam (it leaves the radiance B in F₀); for lab illumination set factor · scale to the solid angle subtended by the source, or the throughput of the optics, that converts B into the irradiance of the beam.
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 (π: radiance → exitance of a Lambertian emitter).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, SIF760=nothing, mSIF=0,
wavenumber_cm1=nothing,
SIF755=nothing, slope=0, wavelength_nm=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.
Alternatively, SIF760 and mSIF define the retrievable unpolarized spectral radiance
Both coefficients are appended to the Jacobian state vector, in that order. SIF760 is in mW m⁻² sr⁻¹ (cm⁻¹)⁻¹ and mSIF is in those units per cm⁻¹. wavenumber_cm1 must contain the model spectral grid. The legacy SIF755/wavelength-domain slope interface remains supported. For nonzero retrievable SIF, the source set must also contain a SolarBeam with an explicit non-unit Fraunhofer F₀; unit-normalized radiances and physical SIF must not be mixed.
When SIF₀ === nothing, prepare_source materialises a zero matrix — the source is a no-op (useful as a placeholder). Nonzero prescribed emission and retrievable coefficients require a scalar, spectral, Legendre, or spline Lambertian surface. Unsupported combinations throw ArgumentError at solve time, including split surface replay. A zero prescribed source is allowed on any surface; zero retrievable amplitudes still require SIF support because their derivatives are nonzero.
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.
vSmartMOM.CoreRT.ThermalEmission Type
ThermalEmission(; B_layer = nothing) <: AbstractSourceUser-facing per-layer thermal volume source. Carries a pre-computed Planck radiance matrix B_layer of shape (nLayers, nSpec) in mW · m⁻² · sr⁻¹ · cm⁻¹ (matching vSmartMOM.SolarModel.planck_spectrum_wn).
Two construction patterns:
# Pre-computed B_layer
ThermalEmission(B_layer = my_planck_matrix)
# T_layers + spectral grid → computes B_layer internally
ThermalEmission(T_layers, ν_grid)Compose with other sources via +:
sources = SolarBeam() + ThermalEmission(T_layers, ν_grid)Fields
B_layer :: Union{Nothing, AbstractMatrix}: Planck radiance per layer per spectral point with shape(nLayers, nSpec). Passed directly to a solve (rt_run(model; sources)),nLayersmust be the final model layer count;model_from_parameters(...; sources)also accepts the input-profile grid and reframes it. Only the elastic multiple-scattering solver injects thermal emission:rt_run_ssand the Raman solvers reject it, and interior-height and linearized runs do too. Stored without aneltypeconstraint —prepare_sourcedoes the precision conversion against the model'sFTat solve time, matchingSolarBeam's FT-deferred design.
AD mode
source_ad_mode returns AnalyticSourceJacobian; a future source_tangent! body provides the analytic (∂j/∂T_layer, ∂j/∂τ, ∂j/∂ϖ) derivatives (Phase A.2).
vSmartMOM.CoreRT.ThermalEmission Method
ThermalEmission(T_layers::AbstractVector, ν_grid::AbstractVector) -> ThermalEmissionConvenience constructor: compute B_layer[iz, n] = B(T_layers[iz], ν_grid[n]) via vSmartMOM.SolarModel.planck_spectrum_wn, in mW · m⁻² · sr⁻¹ · cm⁻¹.
vSmartMOM.CoreRT.PreparedThermalEmission Type
PreparedThermalEmission{FT, AT} <: AbstractPreparedSourceKernel-ready per-layer Planck source. B_layer is materialised on the model's array type at the right (nLayers, 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.supports_surface_sif Function
supports_surface_sif(surface) -> BoolWhether the surface implements fluorescence injection through surface_source_contribute! and surface_source_contribute_lin!. The scalar, spectral, Legendre, and spline Lambertian surfaces support SIF. An extension implementing both contributions for another surface should also specialize this trait. The default is false.
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,
LambertianSurfaceSpectrum,
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.
vSmartMOM.CoreRT.prepare_source Method
prepare_source(s::ThermalEmission, FT, pol_n, nSpec, arr_type) -> PreparedThermalEmissionResolve a ThermalEmission into a kernel-ready PreparedThermalEmission. The default (B_layer === nothing) materialises a zero (1, nSpec) matrix on the active architecture (no-op source, useful as a placeholder). A user-supplied B_layer is precision-converted; the leading nLayers dimension is taken from the matrix itself rather than from pol_n since thermal is unpolarized.
Note that pol_n is unused — included only to keep the prepare_source contract uniform across source types.
vSmartMOM.CoreRT.contribute! Method
contribute!(prep::PreparedThermalEmission, added_layer,
ϖ_λ, dτ_λ, iz, m, pol_type, quad_points, architecture)Inject the per-layer Planck volume source into the elemental added layer's j₀⁺ and j₀⁻ accumulators, in the I (Stokes-1) component only, at the m=0 Fourier moment.
For each stream μᵢ and spectral point n:
δ = 2π · (1 - ϖ_λ[n]) · B_layer[iz, n] · (1 - exp(-dτ_λ[n] / μᵢ))
j₀⁺[i_I, 1, n] += δ
j₀⁻[i_I, 1, n] += δwhere i_I = (iμ - 1) · pol_type.n + 1 indexes the Stokes-I row for quadrature stream iμ. Q/U/V rows are untouched (thermal Planck is unpolarized).
The 2π factor undoes the uniform 0.5/π weight applied by postprocessing_vza! so that B(T) survives at full strength after reconstruction. This is exactly analogous to the factor of 2 used in surface_sif.jl:177, with the extra π accounting for B(T) being a radiance rather than a Lambertian-disk irradiance.
For m > 0 this is a no-op — thermal is isotropic and contributes only at m=0.