Configuration Guide — Building a Scene, Step by Step
For: users building up a scene one capability at a time (Rayleigh-only → polarization → gases → aerosols → surfaces), in file and in-script forms.
Next: Input Schema, IO API, Compute Jacobians.
This guide builds a vSmartMOM scene up from the simplest possible setup, adding one capability at a time. Every example is shown in two equivalent forms — a config file (TOML or YAML) and the equivalent pure-Julia construction with no file at all — because they map onto exactly the same parser.
For the per-field reference tables, see the Input Schema pages (one per block). This page is the narrative: what to reach for, in what order, and what has to line up.
Two ways to configure — they are the same thing
config file (TOML/YAML) ─┐
├─► read_parameters ─► vSmartMOM_Parameters ─► model_from_parameters ─► rt_run
in-script Dict ─┘read_parameters dispatches on its argument:
| Input | Call |
|---|---|
a .toml / .yaml path | read_parameters("scene.toml") |
an in-memory Dict | read_parameters(cfg) |
an existing vSmartMOM_Parameters | returned unchanged |
The in-script Dict mirrors the file 1:1. Each top-level block becomes a Dict entry; the typed fields (surfaces, polarization type, architecture, …) are given as the same strings the file uses — they are parsed identically. So anything you can write in TOML you can build in a script, and vice versa, without ever touching disk.
using vSmartMOM
params = read_parameters("scene.toml") # from a file
# — or, identically, with no file: —
params = read_parameters(cfg) # cfg::Dict, see every example below
model = model_from_parameters(params)
R, T = rt_run(model)Level 0 — Minimal pure-Rayleigh scalar scene
The smallest valid scene: one spectral point, a Lambertian surface, a two-layer Rayleigh atmosphere, scalar (intensity-only) radiation, no absorption, no aerosols.
File (TOML):
[radiative_transfer]
spec_bands = ["[12987.0]"] # one wavenumber, cm⁻¹
surface = ["LambertianSurfaceScalar(0.15)"]
nstreams = 3 # ≥3 (Rayleigh reaches m=2)
polarization_type = "Stokes_I()"
truncation = "NoTruncation()"
depol = -1 # -1 ⇒ depol from N₂/O₂ molecular constants
float_type = "Float64"
architecture = "CPU()"
[geometry]
sza = 60.0
vza = [60.0]
vaz = [180.0]
obs_alt = [0.0] # TOA + BOA radiances
[atmospheric_profile]
T = [250.0, 275.0] # K, full levels (layer centers)
p = [100.0, 500.0, 1000.0] # hPa, half levels (length = #layers + 1)
profile_reduction = -1 # -1 ⇒ keep all layersIn-script (Dict), exactly equivalent:
cfg = Dict(
"radiative_transfer" => Dict(
"spec_bands" => ["[12987.0]"],
"surface" => ["LambertianSurfaceScalar(0.15)"],
"nstreams" => 3,
"polarization_type" => "Stokes_I()",
"truncation" => "NoTruncation()",
"depol" => -1,
"float_type" => "Float64",
"architecture" => "CPU()"),
"geometry" => Dict(
"sza" => 60.0, "vza" => [60.0], "vaz" => [180.0], "obs_alt" => [0.0]),
"atmospheric_profile" => Dict(
"T" => [250.0, 275.0], "p" => [100.0, 500.0, 1000.0],
"profile_reduction" => -1))
params = read_parameters(cfg)
model = model_from_parameters(params)
R, T = rt_run(model)What each field means / what must line up
spec_bands— one entry per band. Two syntaxes: a single point/list"[12987.0]", or a range"(1e7/775):0.05:(1e7/755)"(start:step:stop, in cm⁻¹). Wavelength↔wavenumber:ν[cm⁻¹] = 1e7 / λ[nm]. See thespec_bandsschema.surface— one BRDF per band. Fewer surfaces than bands is an error at model construction; surplus entries are ignored with a warning.nstreams— angular resolution. Public contract:stream_l_cap = 2·nstreams − 1. Minimum 3.Thas length#layers;p(half levels) has length#layers + 1. They define the layering — get this off-by-one right.obs_altis a height selection in km above BOA.[0]requests the standard TOA-upwelling and BOA-downwelling pair. Scalar0requests BOA only; a scalar interiorHrequests both directions atH;[0, H]requests TOA, BOA, andH. See thegeometryschema for the complete scalar/vector convention and the namedObserverRTResultoutput.
Level 1 — Add polarization
Switch the Stokes vector from intensity-only to full linear (or full) polarization. Only polarization_type changes.
polarization_type = "Stokes_IQU()" # I, Q, U (n = 3)
# or "Stokes_IQUV()" for full Stokes (n = 4); "Stokes_I()" is scalar (n = 1)cfg["radiative_transfer"]["polarization_type"] = "Stokes_IQU()"The output R/T gain a Stokes dimension: R[ivza, istokes, ispec]. Nothing else in the config changes; the solver propagates the larger phase matrix.
Level 2 — Add trace-gas absorption
Introduce a new top-level absorption block. This is where line-by-line gas optical depth enters.
[absorption]
fixed_molecules = [["O2"]] # one list per band; no abundance Jacobian
variable_molecules = [[]] # one list per band; Jacobian computed
vmr = { O2 = 0.21 } # volume mixing ratio per molecule
broadening = "Voigt()"
CEF = "HumlicekWeidemann32SDErrorFunction()"
wing_cutoff = 40 # cm⁻¹ from line centercfg["absorption"] = Dict(
"fixed_molecules" => [["O2"]],
"variable_molecules" => [[]],
"vmr" => Dict("O2" => 0.21),
"broadening" => "Voigt()",
"CEF" => "HumlicekWeidemann32SDErrorFunction()",
"wing_cutoff" => 40)Deep dive — the absorption block
Per-band molecule lists. fixed_molecules[ib] and variable_molecules[ib] each hold the molecules active in band ib. There is one list per spectral band — band 1 might carry O2, band 2 CO2, etc.
fixed — abundance held constant, so no gas-abundance Jacobian is computed (other configured state derivatives remain available).
variable — abundance is a state-vector element, Jacobian computed (the linearized path differentiates these).
What must match
length(fixed_molecules) == length(variable_molecules) == length(spec_bands).Every molecule that appears in either list must have a
vmrentry, or parsing errors.Each molecule must actually have lines inside its band's wavenumber range — a molecule with no lines in-band contributes nothing.
VMR sources. For every non-H₂O gas, vmr means moles of species per mole of dry air. A value is either
a scalar (e.g.
O2 = 0.21) — constant with height, ora vector with either one value per layer (
N) or one value per pressure interface (N+1). Interface values are mapped to layer centers in log pressure before any observer-height interfaces are inserted.
H₂O is derived from q and stored internally as the dry-air molar ratio
Line shape. broadening ∈ Voigt() (default; Doppler+pressure, the right choice through the troposphere/stratosphere), Lorentz() (pressure limit), Doppler() (thermal limit). CEF is the complex-error-function evaluator for Voigt (HumlicekWeidemann32SDErrorFunction()). wing_cutoff (cm⁻¹) drops line contributions farther than that from each spectral point.
Where cross-sections come from. With no LUTfiles, lines are pulled from HITRAN on the fly. With LUTfiles (one vector per band, parallel to the molecule lists) the precomputed look-up tables are used instead — much faster, and the production pipeline path. cia_files / mtckd_file add collision-induced and continuum absorption.
For a CIA file containing more than one HITRAN reference family, select the source explicitly:
cia_files:
- path: "/data/HITRAN_CIA/O2-O2_2024.cia"
reference_codes: ["54"]
negative_policy: error
- "/data/HITRAN_CIA/O2-N2_2024.cia"reference_codes accepts one string or a nonempty list. A bare path remains valid when its reference families do not overlap on the requested spectral grid; overlapping families without an explicit selection are rejected. negative_policy is error by default. Use clamp_zero only as an explicit, documented decision to replace negative tabulated cross sections by zero.
Legacy form. Older configs use a single
molecules = [[O2], [CO2]]list (all treated as fixed) instead offixed_molecules/variable_molecules. It still parses for non-H₂O species; prefer the explicit split for new work.
Level 3 — Add water vapor via specific humidity q
H₂O is special: in the modern fixed_molecules/variable_molecules form it is never listed. Instead it is derived from the specific-humidity profile q on the atmospheric_profile block:
[atmospheric_profile]
T = [250.0, 275.0]
p = [100.0, 500.0, 1000.0]
q = [0.001, 0.005] # specific humidity, per full level (mass fraction)cfg["atmospheric_profile"]["q"] = [0.001, 0.005]vSmartMOM converts q to an H₂O volume mixing ratio per layer:
vmr_h2o = q / (1 − q) · (M_dry / M_H₂O)and adds the H₂O line absorption automatically (HITRAN on the fly, or the H₂O LUT if supplied). q may contain N layer-center values (same as T) or N+1 pressure-interface values; interface values are mapped to layer centers in log pressure. All-zero q (the default) means a dry atmosphere.
H₂O is driven only by
q. Do not listH2Oinfixed_molecules,variable_molecules, or the legacymoleculesform; the parser rejects it to prevent double-specifying water vapor.
Level 4 — Multiple bands
Bands are independent: each gets its own spectral range, surface, molecule lists, and aerosol Z-matrices. Just make every per-band list the same length.
[radiative_transfer]
spec_bands = ["(1e7/775):0.05:(1e7/755)", "(1e7/1640):0.1:(1e7/1590)"]
surface = ["LambertianSurfaceScalar(0.15)", "LambertianSurfaceScalar(0.30)"]
[absorption]
fixed_molecules = [["O2"], ["CO2"]]
variable_molecules = [[], ["CO2"]]
vmr = { O2 = 0.21, CO2 = 400e-6 }cfg["radiative_transfer"]["spec_bands"] =
["(1e7/775):0.05:(1e7/755)", "(1e7/1640):0.1:(1e7/1590)"]
cfg["radiative_transfer"]["surface"] =
["LambertianSurfaceScalar(0.15)", "LambertianSurfaceScalar(0.30)"]
cfg["absorption"]["fixed_molecules"] = [["O2"], ["CO2"]]
cfg["absorption"]["variable_molecules"] = [[], ["CO2"]]
cfg["absorption"]["vmr"] = Dict("O2" => 0.21, "CO2" => 400e-6)What must match: every per-band list — surface, fixed_molecules, variable_molecules, and (if used) LUTfiles — must have one entry per band. A single solve over several bands together (e.g. a combined-band Raman call) uses one lower boundary and therefore requires the same surface in each of those bands; see the surface schema.
Level 5 — Add aerosols (Mie scattering)
Add a scattering block. Each aerosol is a size distribution + refractive index + a vertical placement + a reference optical depth.
[radiative_transfer]
greek_beta_cutoff = 1.0e-5 # beta-only Fourier-support cutoff; see below
[scattering]
r_max = 50.0 # µm, max radius for the size-distribution quadrature
nquad_radius = 1000 # radius quadrature points
"λ_ref" = 0.770 # µm, wavelength at which τ_ref is defined
decomp_type = "NAI2()"
[[scattering.aerosols]]
"τ_ref" = 0.001 # aerosol optical depth at λ_ref
"μ" = 1.0 # geometric mean radius (µm), lognormal
"σ" = 1.5 # geometric std dev (≥ 1)
"nᵣ" = 1.3 # real refractive index
"nᵢ" = 1.0e-8 # imaginary (absorbing) part
"p₀" = 700.0 # vertical peak pressure (hPa)
"σp" = 50.0 # vertical width (hPa)TOML bare keys are ASCII-only, so the Unicode keys (λ_ref, τ_ref, μ, σ, nᵣ, nᵢ, p₀, σp, z₀, σ₀, ϖ, Δ_angle) must be quoted in TOML; YAML and Julia Dicts use them as is.
With aerosols present, also state radiative_transfer.greek_beta_cutoff: a positive threshold such as 1.0e-5 drops insignificant high-degree β tails, and in YAML greek_beta_cutoff: null keeps the full Mie series. Omitting the key behaves as null but the parser warns (see the radiative_transfer schema).
cfg["radiative_transfer"]["greek_beta_cutoff"] = 1.0e-5
cfg["scattering"] = Dict(
"r_max" => 50.0, "nquad_radius" => 1000, "λ_ref" => 0.770,
"decomp_type" => "NAI2()",
"aerosols" => [Dict(
"τ_ref" => 0.001, "μ" => 1.0, "σ" => 1.5,
"nᵣ" => 1.3, "nᵢ" => 1.0e-8,
"p₀" => 700.0, "σp" => 50.0)])Deep dive — the scattering block
Per-aerosol fields
| Field | Meaning |
|---|---|
τ_ref | optical depth of this aerosol at λ_ref (the normalization) |
μ, σ | lognormal size distribution — geometric mean radius (µm) and geometric std dev (σ ≥ 1) |
nᵣ, nᵢ | complex refractive index nᵣ − i·nᵢ; nᵢ ≥ 0 is absorption |
p₀, σp | pressure-form vertical profile: a normal in pressure, peak p₀ (hPa), width σp |
z₀, σ₀ | altitude-form vertical profile (preferred): lognormal in altitude (km) |
Vertical placement — exactly one form. Give either (p₀, σp) or (z₀, σ₀), never both and never neither. The profile shape multiplies τ_ref into a per-layer optical depth.
Band-level scattering knobs
r_max,nquad_radius— the radius quadratureMieintegrates over. Largernquad_radiusresolves the size distribution better at more cost. The optionalr_min(default1e-3µm) is the lower bound; the nodes on[r_min, r_max]do not move with the aerosolμ,σ, so the analytic size Jacobians are exact for the chosennquad_radius.λ_ref— the wavelengthτ_refis anchored to.decomp_type— phase-matrix Fourier decomposition:NAI2()(cheaper, GPU- capable, the default) orPCW()(precomputed Wigner; faster for very largenquad_radius, CPU-only).
Where the Mie work happens. For each band and aerosol, Lorenz–Mie theory is integrated over the radius quadrature to produce the extinction, single- scattering albedo ω̃, and the GreekCoefs (phase-matrix moments). These feed the layer optical properties (the per-layer τ, ϖ, Z⁺⁺, Z⁻⁺). Truncation (truncation = "auto") applies δBGE forward-peak removal only when the Greek series is longer than stream_l_cap.
Spectral resolution — what varies within a band, and what doesn't
Not every optical property is resolved at every spectral point. The forward and linearized paths are consistent here:
| Property | Resolution (forward = linearized) |
|---|---|
Gas absorption τ_abs(ν) | per wavelength (line-by-line) |
Rayleigh τ_rayl(ν) (σ ∝ ν⁴) | per wavelength |
Aerosol extinction k & AOD τ_aer | per wavelength (interpolated between Mie nodes, see below) |
Aerosol single-scattering albedo ω̃ | per wavelength (interpolated between Mie nodes) |
Aerosol phase matrix Z⁺⁺/Z⁻⁺ (Greek coefs) | per wavelength (interpolated between Mie nodes — no Mie calculation per wavelength) |
Absorption and Rayleigh are fully spectral — each is stored as an (nSpec × nLayers) matrix, recomputed at every wavenumber.
Aerosol AOD is anchored at λ_ref and re-scaled per wavelength. τ_ref is the optical depth at λ_ref; the model rescales it by the Mie extinction ratio:
τ_aer(λ, z) = τ_ref · ( k(λ) / k(λ_ref) ) · vertical_profile(z)To get k(λ), ω̃(λ) and the phase matrix without a full Mie calculation at every spectral point, the aerosol Mie is evaluated at the two band endpoints (the endpoints of the aerosol_anchor_bands grid when that keyword is passed to model_from_parameters). Each endpoint is truncated independently, and k, the scattering coefficient k·ω̃ (hence ω̃), the truncation factor and the Greek coefficients are linearly interpolated in wavenumber onto each spectral point — so AOD and the phase matrix both vary across the band. When λ_ref lies strictly inside the band, a third Mie node at λ_ref is added and the interpolation is a natural cubic through the three nodes. A single-wavelength band uses a Mie calculation at that point directly.
Approximation. Interpolating between Mie nodes assumes the aerosol optics vary smoothly across the band — true for bands narrow compared to the aerosol's spectral structure, which is a key reason to keep spectral bands small. (See the code comment at the aerosol Mie call site in
model_from_parameters.jl.)
Refractive index: in-band nᵣ/nᵢ vs. reference n_ref
Two refractive-index roles exist in the aerosol block:
| Symbol | Config key | Role |
|---|---|---|
nᵣ − i·nᵢ | per-aerosol nᵣ, nᵢ | In-band index — used for all Mie calculations within the spectral band (k(λ), ω̃(λ), Greek coefs). |
n_ref | [scattering].n_ref (optional) | Reference index — used only to compute k_ref = k(λ_ref, n_ref), the extinction at the normalisation wavelength that anchors τ_ref. |
If n_ref is not set, it defaults to the first aerosol's nᵣ/nᵢ and is shared by every aerosol, so for a single aerosol in-band n == reference n and the distinction is irrelevant. Setting n_ref is only needed when you want τ_ref to be normalised at a refractive index different from the aerosol's spectral value (e.g. a standard atmospheric convention), and it matters for every aerosol after the first. See the scattering schema for how n_ref enters the Jacobians.
No in-band n(λ) dispersion. The refractive index
nᵣ/nᵢis constant across the band — wavelength-dependent dispersion of the refractive index is not yet supported. Each aerosol uses a single (nᵣ, nᵢ) pair for all Mie calculations within the band.
Level 6 — Analytic phase-function aerosols (fast prototyping)
Skip Mie entirely with an analytic phase function — handy for synthetic scenes and quick sensitivity tests.
[[scattering.aerosols]]
"τ_ref" = 0.1
phase_function = "HenyeyGreensteinPhaseFunction(g=0.7)"
ssa = 0.95
"p₀" = 700.0
"σp" = 50.0Set ssa (single-scattering albedo; alias ϖ) here, because there is no Mie computation to derive it from; if omitted it defaults to 1.0 (no absorption). Caveat: analytic phase-function aerosols are not supported in the linearized (Jacobian) path.
Level 7 — Non-Lambertian surfaces
Swap the per-band surface string for any supported BRDF; the parser builds the matching surface type.
surface = ["rpvSurfaceScalar(0.1, 0.05, 0.6, -0.1)"] # RPV land (ρ₀, ρ_c, k, Θ)
# "RossLiSurfaceScalar(fvol, fgeo, fiso)" — Ross-Li (MODIS-style)
# "CoxMunkSurface(wind_speed=7.0)" — ocean glintEach BRDF has its own parameters (see surface). They are per-band like everything else.
Level 8 — Canopy
Wrap the band surface(s) in a structural canopy (leaves over soil) by adding a canopy block.
[canopy]
LAI = 3.0
n_layers = 1
leaf_reflectance = 0.4
leaf_transmittance = 0.05
soil = "from_surface" # reuse the band BRDF as understorycfg["canopy"] = Dict(
"LAI" => 3.0, "n_layers" => 1,
"leaf_reflectance" => 0.4, "leaf_transmittance" => 0.05,
"soil" => "from_surface")The canopy wraps each band's BRDF in a CanopySurface; the soil below is the true lower boundary. See the canopy schema for every canopy field and its default.
Level 9 — Raman (inelastic) scattering
Raman is selected through the RS_type passed to the run (the elastic config above is unchanged). The pure-elastic default is noRS(); rotational Raman is RRS(...). See the Inelastic concepts page for the source-term machinery and how the Cabannes/Rayleigh split feeds it.
Level 10 — Jacobians (linearized RT)
Same config; build the model in linearized mode and run the linearized solver. Molecules listed under variable_molecules, the aerosol parameters, and the surface parameters become the differentiable state.
params = read_parameters(cfg)
model, lin_model = model_from_parameters(LinMode(), params)
NAer = length(params.scattering_params.rt_aerosols)
NGas = size(lin_model.τ̇_abs[1], 1)
NSurf = 1
R, T, dR, dT = rt_run(model, lin_model, NAer, NGas, NSurf)(Note: the linearized path is pure-elastic — Raman is not linearized.)
Gas-abundance columns are the partial ∂τ/∂VMR = σ·N_dry with cross sections, CIA and MT_CKD held fixed. The exception is an ABSCO table with an H₂O-broadener axis: its exact response to the H₂O abundance is included in the q-driven H₂O columns. When the forward absorption responds to an abundance in another way (for the q-driven H₂O of Level 3: line-by-line self broadening, plus MT_CKD and CIA when configured), a solve requesting that column raises an ArgumentError under the default abundance_jacobians=:complete. Hold the abundance fixed (compute_h2o_jacobians=false, or list the gas in fixed_molecules), or accept the partial with model_from_parameters(LinMode(), params; abundance_jacobians=:frozen_cross_section). The vertical_grid keyword (FixedPressureGrid() by default, or FixedAltitudeGrid()) selects the surface-pressure Jacobian convention. See Compute Jacobians for both.
Level 11 — Precision, GPU, and solver knobs
[radiative_transfer]
float_type = "Float32" # or "Float64"
architecture = "GPU()" # or "CPU()"
[radiative_transfer.numerics]
blas_threads = 4 # cap BLAS threads for big spectral batches
verbose = falsecfg["radiative_transfer"]["float_type"] = "Float32"
cfg["radiative_transfer"]["architecture"] = "GPU()"
cfg["radiative_transfer"]["numerics"] =
Dict("blas_threads" => 4, "verbose" => false)Float32 roughly halves memory and speeds the kernels at the cost of per-pixel precision; GPU() dispatches the adding-doubling kernels to CUDA when available. numerics.blas_threads sets the process-wide BLAS thread count (often 4–8, or 1 for large spectral batches). The value persists after the solve, so select it consistently at application startup rather than varying it between concurrently executed models.
Where to go next
Per-block field reference: Input Schema.
Editor autocomplete + validation for TOML/YAML: Schema editor support.
Worked end-to-end runs: IO Examples and the Tutorials.