Skip to content

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 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 = 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:

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.

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:

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 · scale · 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).

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 ​

QuantityUnit
SolarBeam.F₀mW · m⁻² · cm⁻¹
SurfaceSIF.SIF₀mW · m⁻² · cm⁻¹
SurfaceSIF.SIF755same radiance unit as output Stokes I/Q/U/V
SurfaceSIF.slopeoutput 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:

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-Lambertiannonzero/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 ​

  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 Matrix{Float32} into a Float64 model (or vice versa) — the conversion happens once in prepare_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 throws ArgumentError at solve time. To change SZA, set params.sza before model_from_parameters(params) or use remake_geometry. A solve supports at most one SolarBeam (including BlackbodySource); 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

julia
sb = SolarBeam()                                   # default unit Stokes I
sb = SolarBeam(; sza = 35.0)                       # requires model SZA = 35 degrees
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 = π 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); 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 (π: 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

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, SIF760=nothing, mSIF=0,
           wavenumber_cm1=nothing,
           SIF755=nothing, slope=0, wavelength_nm=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.

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 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
vSmartMOM.CoreRT.ThermalEmission Type
julia
ThermalEmission(; B_layer = nothing) <: AbstractSource

User-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:

julia
# 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 +:

julia
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)), nLayers must 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_ss and the Raman solvers reject it, and interior-height and linearized runs do too. Stored without an eltype constraint — prepare_source does the precision conversion against the model's FT at solve time, matching SolarBeam'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).

source
vSmartMOM.CoreRT.ThermalEmission Method
julia
ThermalEmission(T_layers::AbstractVector, ν_grid::AbstractVector) -> ThermalEmission

Convenience constructor: compute B_layer[iz, n] = B(T_layers[iz], ν_grid[n]) via vSmartMOM.SolarModel.planck_spectrum_wn, in mW · m⁻² · sr⁻¹ · cm⁻¹.

source
vSmartMOM.CoreRT.PreparedThermalEmission Type
julia
PreparedThermalEmission{FT, AT} <: AbstractPreparedSource

Kernel-ready per-layer Planck source. B_layer is materialised on the model's array type at the right (nLayers, 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.supports_surface_sif Function
julia
supports_surface_sif(surface) -> Bool

Whether 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.

source
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,
                                           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.

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
vSmartMOM.CoreRT.prepare_source Method
julia
prepare_source(s::ThermalEmission, FT, pol_n, nSpec, arr_type) -> PreparedThermalEmission

Resolve 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.

source
vSmartMOM.CoreRT.contribute! Method
julia
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.

source