Skip to content

3 · Layer Optical Properties — the bridge ​

For: anyone reading the RT basics arc top-to-bottom. This is the page that connects "physics inputs" (gas absorption, Mie scattering) to "what the solver does." Centerpiece of the thread.

Prev: 2 · Vector RTE & Discretization · Next: 3a · Gas Absorption

Concepts/02 ended on a four-array abstraction: , one set per Fourier moment per atmospheric layer. This page is the hinge: everything before it is "how to build those arrays" and everything after it is "what the solver does with them."

Every atmospheric layer reduces to four arrays ​

For each Fourier moment , per layer:

SymbolMeaningShape
optical depth(nSpec,)
single-scattering albedo(nSpec,)
forward-direction phase matrix(NquadN, NquadN, nSpec)
backscatter phase matrix(NquadN, NquadN, nSpec)

These are also the three core RT variables the linearized solver differentiates against ( and count as one "core variable" because they share the same Greek expansion). Everything upstream is plumbing to build them; everything downstream is linear algebra over them. See Concepts/06.

The solar direction is a rectangular operator, not a diffuse stream ​

With external_solar=true, the collimated direction is not added to the quadrature grid. The square arrays above remain purely diffuse-to-diffuse. Separate phase columns , shaped (NquadN,nStokes,nSpec), map the fixed solar Stokes vector into every diffuse output stream.

The elemental solver converts these phase columns into four explicit solar column operators:

OperatorMeaningShape
direct-solar to diffuse reflection(NquadN,nStokes,nSpec)
direct-solar to diffuse scattered transmission(NquadN,nStokes,nSpec)

contains only radiation scattered out of the collimated beam. The unscattered beam remains the analytic quantity  . The layer source is formed only after the operators are built, e.g.

This representation avoids dummy zero-weight solar nodes and gives elastic and Raman SFI the same operator contract.

How the four arrays are built (the operator chain) ​

The Julia type that carries them is CoreScatteringOpticalProperties, and the constructor is constructCoreOpticalProperties in src/CoreRT/LayerOpticalProperties/compEffectiveLayerProperties.jl:314–375. The build happens once per band, per Fourier moment, before the layer loop:

   Gas opacity τ_abs  ─┐
   (HITRAN LBL)        │
                       │
   Rayleigh ─────────┐ │
   (greek + ω̃_Cab)   │ │
                     ▼ ▼
   Aerosol_1 ──→ constructCoreOpticalProperties ──→ Per-layer
   (GreekCoefs,                                     (τ, ϖ₀,
    ω̃, k, fᵗ)                                       Z⁺⁺, Z⁻⁺)
                     ▲                                   │
                     │                                   │
   Aerosol_2 ────────┘                                   ▼
                                                    MOM solver

The build proceeds in five steps (the relevant lines from compEffectiveLayerProperties.jl:314–375):

  1. Pick the Rayleigh greek source. For pure-elastic runs (noRS), use the full Rayleigh greek — rotational Raman is rolled into the effective depolarization. For Raman-aware runs (RRS, VS_*), use the Cabannes greek and handle rotational Raman explicitly. Selector at lines 8–9. See Concepts/08.

  2. Build the Rayleigh layer's from the chosen greek and the quadrature points via Scattering.compute_Z_moments(pol_type, μ, greek, m).

  3. For each aerosol, run compute_Z_moments again (with that aerosol's GreekCoefs), wrap in a CoreScatteringOpticalProperties via createAero (which also applies δ-M truncation — see Concepts/03c), and mix it into the layer with +.

  4. Add gas absorption to the total optical depth:   ,   . Coded as combo + CoreAbsorptionOpticalProperties(τ_abs) (line 363).

  5. Stack layers vertically with * (line 372): prod([band_layer_props[i][iz] for i = 1:length(iBand)]). The result is a Vector{CoreScatteringOpticalProperties}, one entry per atmospheric layer, ready for Concepts/04.

The walkthrough in Concepts/03c goes through the body of the loop in detail, including the scattering-vs-total τ split that makes the doubling step cheap.

The two operators on CoreScatteringOpticalProperties ​

Mixing scatterers in a layer (src/CoreRT/types.jl:1743–1783):

julia
function Base.:+(x::CoreScatteringOpticalProperties, y::CoreScatteringOpticalProperties)
    τ  = x.τ .+ y.τ                  # additive optical depth
    wx = x.τ .* x.ϖ                  # scattering-weight from each
    wy = y.τ .* y.ϖ
    w  = wx .+ wy
    ϖ  = w ./ τ                       # τϖ-weighted SSA
    Z⁺⁺ = (wx .* xZ⁺⁺ .+ wy .* yZ⁺⁺) ./ w   # τϖ-weighted phase matrix
    Z⁻⁺ = (wx .* xZ⁻⁺ .+ wy .* yZ⁻⁺) ./ w
    CoreScatteringOpticalProperties(τ, ϖ, Z⁺⁺, Z⁻⁺)
end

The weighting is    — proportional to the scattering optical depth contribution from each species. That's what conserves the total scattered intensity per stream pair when species are combined.

Stacking layers vertically (src/CoreRT/types.jl:1785+):

julia
function Base.:*(x::CoreScatteringOpticalProperties, y::CoreScatteringOpticalProperties)
    arr_type = array_type(architecture(x.τ))
    x = expandOpticalProperties(x, arr_type)
    y = expandOpticalProperties(y, arr_type)
    CoreScatteringOpticalProperties(
        [x.τ; y.τ], [x.ϖ; y.ϖ],
        cat(x.Z⁺⁺, y.Z⁺⁺, dims=3),
        cat(x.Z⁻⁺, y.Z⁻⁺, dims=3))
end

This stacks the spectral axis (cat(..., dims=3)) for and concatenates the per-spectral and . The result is not a column of layers (those are kept as a Vector{CoreScatteringOpticalProperties}) — it's a multi-band concatenation along the spectral axis, used when several bands share the same atmospheric column.

A worked example ​

Consider an OCO-2-like O₂ A-band layer at altitude iz, with one Rayleigh contribution and one fine-mode aerosol:

julia
# inside constructCoreOpticalProperties, conceptually:

# (1) Rayleigh: τ_rayl from molecular cross-section, ϖ_Cab ≈ 1 for pure scattering
rayl = CoreScatteringOpticalProperties(τ_rayl, ϖ_Cab, RaylZ⁺⁺, RaylZ⁻⁺)

# (2) Aerosol fine mode, with δ-M applied via createAero
aer  = createAero(τ_aer_fine, aer_optics_fine, AerZ⁺⁺, AerZ⁻⁺)
#      → modifies (τ, ϖ) for forward-peak truncation per SS2015

# (3) Mix scatterers in the layer
combo = rayl + aer
#      → τ, ϖ, Z⁺⁺, Z⁻⁺ are τϖ-weighted averages

# (4) Add gas absorption (τ_abs is per-wavelength)
layer_optics = combo + CoreAbsorptionOpticalProperties(τ_abs)
#      → τ_λ = τ_abs + τ_scat
#        ϖ_λ = τ_scat·ϖ / τ_λ
#        Z⁺⁺, Z⁻⁺ unchanged

After all Nz layers are built, rt_kernel! consumes them one at a time from TOA to BOA inside the Fourier-moment loop (Concepts/04).

What's next ​

To understand how each ingredient is computed:

If you only care about what the solver does with these arrays, skip to Concepts/04.

Code anchors ​

ConceptSource
Build per-layer (τ, ϖ, Z)src/CoreRT/LayerOpticalProperties/compEffectiveLayerProperties.jl:314–375
Aerosol δ-M wrapper (createAero)compEffectiveLayerProperties.jl:478–491
Cabannes vs Rayleigh greekcompEffectiveLayerProperties.jl:8–9
Mix scatterers (operator +)src/CoreRT/types.jl:1743–1783
Stack layers (operator *)src/CoreRT/types.jl:1785+
CoreScatteringOpticalProperties typesrc/CoreRT/types.jl::CoreScatteringOpticalProperties
Gas absorption typesrc/CoreRT/types.jl::CoreAbsorptionOpticalProperties

Hands-on tutorials ​

Runnable examples with Plotly figures:

References ​

  • Sanghavi et al. (2014), JQSRT 133:412–433, doi:10.1016/j.jqsrt.2013.09.004. §2 (layer-averaged optics, Eqs. C.22–C.24 for the forward-only definitions used here).

  • Sanghavi & Frankenberg (2023), JQSRT 311:108791, doi:10.1016/j.jqsrt.2023.108791. §3.2 — separating from for the constant-N_doubl design (more in Concepts/03c).

  • Crib sheet: docs/dev_notes/theory_references.md §F.