Canopy-Coupled Surface Tutorial
vSmartMOM supports vegetation canopy as a lower boundary condition via CanopySurface. The canopy replaces the usual surface BRDF and internally runs canopy sub-layers through the adding-doubling method before combining with the soil reflectance.
1) How it works
CanopySurface is an AbstractSurfaceType that wraps:
A soil BRDF (e.g.,
LambertianSurfaceScalar)Leaf area index (LAI) controlling canopy density
Leaf angle distribution (LAD) from
CanopyOptics.jlLeaf reflectance/transmittance (scalar or spectral vector)
Optional spectral grid for wavelength-dependent leaf optics
Optional within-canopy atmosphere via
canopy_dppressure thicknessOptional multi-layer decomposition (1 = big-leaf, >1 = sub-layers)
When rt_run() reaches the surface, dispatch on CanopySurface internally:
Precomputes azimuthal Z-matrices from the canopy scattering model
Runs canopy sub-layers through elemental → doubling
Applies interaction with the soil BRDF
Returns the effective canopy+soil reflectance to the atmospheric RT
using vSmartMOM
using vSmartMOM.CoreRT
using CanopyOptics
using CairoMakie2) Scalar leaf optics (simplest setup)
A big-leaf canopy with constant leaf reflectance and transmittance over a Lambertian soil:
soil = LambertianSurfaceScalar(0.1)
canopy_scalar = CanopySurface(
soil = soil,
LAI = 3.0,
n_layers = 1,
leaf_reflectance = 0.45,
leaf_transmittance = 0.05,
)3) Spectral leaf optics from PROSPECT
The PROSPECT leaf model computes reflectance and transmittance as a function of leaf biochemistry. CanopySurface_from_prospect wraps this into a spectrally-resolved canopy surface.
leaf = CanopyOptics.LeafProspectProProperties(
N = 1.4, # leaf structure parameter
Ccab = 40.0, # chlorophyll a+b [μg/cm²]
Ccar = 8.0, # carotenoids [μg/cm²]
Canth = 0.0, # anthocyanins
Cbrown = 0.0, # brown pigments
Cw = 0.01, # equivalent water thickness [cm]
Cm = 0.009, # dry matter content [g/cm²]
Cprot = 0.0, # protein content [g/cm²]
Ccbc = 0.0, # carbon-based constituents [g/cm²]
)
canopy_prospect = CanopySurface_from_prospect(
leaf, 400.0:1.0:2500.0;
soil = LambertianSurfaceScalar(0.1),
LAI = 3.0,
n_layers = 2,
)
println("Spectral grid points: ", length(canopy_prospect.leaf_optics_grid))
println("Grid unit: ", canopy_prospect.grid_unit)
println("Sample R (at grid point 200): ", canopy_prospect.leaf_reflectance[200])
println("Sample T (at grid point 200): ", canopy_prospect.leaf_transmittance[200])4) Custom spectral vectors
You can provide your own spectral leaf R/T on any wavelength or wavenumber grid:
canopy_custom = CanopySurface(
soil = LambertianSurfaceScalar(0.1),
LAI = 3.0,
n_layers = 1,
leaf_reflectance = [0.05, 0.08, 0.10, 0.45, 0.48],
leaf_transmittance = [0.02, 0.03, 0.05, 0.40, 0.42],
leaf_optics_grid = [550.0, 660.0, 680.0, 750.0, 780.0], # nm
grid_unit = :nm,
)5) Within-canopy atmosphere
For tall canopies (e.g. forests), there is an atmospheric sub-column within the canopy where gas absorption matters. Set include_atm=true and canopy_dp to the pressure thickness (in hPa) of the canopy air column:
canopy_atm = CanopySurface(
soil = LambertianSurfaceScalar(0.1),
LAI = 5.0,
n_layers = 4,
leaf_reflectance = 0.45,
leaf_transmittance = 0.05,
include_atm = true,
canopy_dp = 3.0, # ~30m forest canopy ≈ 3 hPa
)The within-canopy optical depth _within_canopy_τ is automatically computed from the bottom-of-atmosphere conditions by rt_run(), using the same absorption models as the atmospheric RT.
6) Running with the forward model
yaml_path = joinpath(pkgdir(vSmartMOM),
"test", "test_parameters", "PureRayleighParameters.yaml")
params = read_parameters(yaml_path)
params.architecture = vSmartMOM.Architectures.CPU()
params.max_m = 2
params.l_trunc = 20
params.brdf[1] = canopy_scalar
model = model_from_parameters(params)
R_canopy, T_canopy = rt_run(model)
println("R shape: ", size(R_canopy))
println("R(nadir, I) with canopy: ", R_canopy[1, 1, 1])Compare with bare Lambertian surface:
params2 = read_parameters(yaml_path)
params2.architecture = vSmartMOM.Architectures.CPU()
params2.max_m = 2
params2.l_trunc = 20
model2 = model_from_parameters(params2)
R_bare, T_bare = rt_run(model2)
println("R(nadir, I) bare soil: ", R_bare[1, 1, 1])
println("Canopy effect on TOA R: ",
round((R_canopy[1,1,1] - R_bare[1,1,1]) / R_bare[1,1,1] * 100, digits=1), "%")Compare the reflectance spectra:
fig = Figure(size=(700, 450))
ax = Axis(fig[1,1],
xlabel = "Spectral index",
ylabel = "TOA Reflectance (Stokes I)")
lines!(ax, R_canopy[1, 1, :], label="Canopy (LAI=3)")
lines!(ax, R_bare[1, 1, :], label="Bare soil (α=0.1)")
axislegend(ax, position=:rt)
figThe rendered docs include a Plotly red-edge view so the spectral contrast is visible even when static Makie figures are not rendered by the docs frontend:
7) Effect of within-canopy atmosphere
The within-canopy atmosphere adds gas absorption between canopy sub-layers. This matters for tall canopies and strong absorption bands. Here we compare the TOA reflectance with and without the canopy atmosphere, using a multi-layer canopy so the interleaved atmospheric layers have an effect.
canopy_no_atm = CanopySurface(
soil = LambertianSurfaceScalar(0.1),
LAI = 5.0,
n_layers = 4,
leaf_reflectance = 0.45,
leaf_transmittance = 0.05,
include_atm = false,
)
canopy_with_atm = CanopySurface(
soil = LambertianSurfaceScalar(0.1),
LAI = 5.0,
n_layers = 4,
leaf_reflectance = 0.45,
leaf_transmittance = 0.05,
include_atm = true,
canopy_dp = 3.0, # ~30m forest ≈ 3 hPa
)Run without canopy atmosphere:
params_na = read_parameters(yaml_path)
params_na.architecture = vSmartMOM.Architectures.CPU()
params_na.max_m = 2
params_na.l_trunc = 20
params_na.brdf[1] = canopy_no_atm
model_na = model_from_parameters(params_na)
R_no_atm, _ = rt_run(model_na)
invalidate_canopy_cache!(model_na.params.brdf[1])Run with canopy atmosphere:
params_wa = read_parameters(yaml_path)
params_wa.architecture = vSmartMOM.Architectures.CPU()
params_wa.max_m = 2
params_wa.l_trunc = 20
params_wa.brdf[1] = canopy_with_atm
model_wa = model_from_parameters(params_wa)
R_with_atm, _ = rt_run(model_wa)
invalidate_canopy_cache!(model_wa.params.brdf[1])
println("R(nadir, I) without canopy atm: ", R_no_atm[1, 1, 1])
println("R(nadir, I) with canopy atm: ", R_with_atm[1, 1, 1])
diff_pct = (R_with_atm[1,1,1] - R_no_atm[1,1,1]) / R_no_atm[1,1,1] * 100
println("Within-canopy atmosphere effect: ", round(diff_pct, digits=3), "%")For a pure Rayleigh atmosphere (no gas absorption), the effect is negligible. In bands with strong molecular absorption (O₂ A-band, CO₂), the canopy atmosphere can change the effective surface reflectance by several percent, which matters for trace-gas retrievals.
8) YAML configuration
The canopy can also be configured via YAML, including spectral properties:
radiative_transfer:
surface:
- LambertianSurfaceScalar(0.1)
canopy:
LAI: 3.0
n_layers: 4
leaf_reflectance: [0.05, 0.10, 0.45, 0.48]
leaf_transmittance: [0.02, 0.05, 0.40, 0.42]
leaf_optics_grid: [660.0, 680.0, 750.0, 780.0]
grid_unit: nm
include_atm: true
canopy_dp: 3.0The parser wraps the surface entry as the soil BRDF inside a CanopySurface.
This page was generated using Literate.jl.