Add a Surface BRDF
For: method developers adding a new lower-boundary reflectance model.
Next: Surfaces (Concepts), Surfaces tutorial, The MOM Solver, Library.
Surface models are part of CoreRT because the lower boundary is represented as an AddedLayer and then interacts with the atmospheric composite layer. A new BRDF usually needs three pieces:
a concrete
AbstractSurfaceType;a
reflectanceorcreate_surface_layer!method that builds the surface layer;a parser registration if the surface should be available from YAML/TOML scene files.
Choose The Implementation Shape
Use the simplest hook that matches the model:
| Surface behavior | Implement | Examples |
|---|---|---|
| Scalar BRDF that can be integrated over azimuth | reflectance(surface, n, mu_i, mu_r, dphi) plus the generic reflectance(surface, pol_type, mu, m) path | rpvSurfaceScalar, RossLiSurfaceScalar |
| Lambertian or spectral albedo with a simple closed form | specialized create_surface_layer! | LambertianSurfaceScalar, LambertianSurfaceLegendre, LambertianSurfaceSpline |
| Polarized ocean or other Mueller BRDF | specialized reflectance(surface, pol_type, mu, m) and helper kernels | CoxMunkSurface |
| Composite lower boundary | specialized create_surface_layer! that internally runs sub-layers and interacts with a soil layer | CanopySurface |
The generic surface layer stores upward surface reflection in added_layer.r⁻⁺, keeps t⁺⁺ as the identity pass-through for the downwelling surface field, sets t⁻⁻ = 0 for an opaque lower boundary, and adds optional direct solar source terms in j₀⁺/j₀⁻ when SFI is active.
Define The Type
Add the type near the existing surface definitions in src/CoreRT/types.jl.
struct MySurface{FT} <: AbstractSurfaceType
strength::FT
shape::FT
endKeep fields concrete when possible. Surface parameters are often part of Jacobian layouts, parser conversion, and GPU array setup, so preserving the configured floating-point type matters.
Implement Reflectance
For scalar BRDFs, follow the rpvSurfaceScalar pattern:
function reflectance(surface::MySurface{FT}, n, mu_i::FT, mu_r::FT, dphi::FT) where FT
n == 1 || return zero(FT)
return surface.strength * my_angular_shape(mu_i, mu_r, dphi, surface.shape)
endThe generic Fourier-moment method and inherited create_surface_layer! in src/CoreRT/Surfaces/analytic_surface.jl integrate over azimuth and assemble the surface operator. A scalar analytic BRDF needs only the reflectance hook above; specialize the layer builder only for a different operator construction.
Check the normalization before using this hook: its scalar angular value is π * BRDF, so a Lambertian albedo a returns a, not a/π. The Fourier builder applies (1/π)∫₀^π ... dφ at m=0 and twice that at higher moments. A Mueller BRDF implementation must follow the same normalization and Stokes sign convention. In a transparent atmosphere with unpolarized incident irradiance F₀, the direct surface radiance is μ₀ * F₀ * BRDF. This absolute check catches a missing factor of π that forward/linearized parity alone cannot.
Declare capabilities
Implement component_m_max(surface, ctx) so anisotropic surfaces receive enough Fourier moments. For retrieval support, declare surface_parameter_count(surface) and use result.layout for column mapping. The default count of one does not implement a new surface's derivative.
Check the full run, atmosphere/surface replay, and any advertised linearized path. External-solar, equivalent-source Jacobian adding, interior observers, SIF injection, and canopy coupling have separate support restrictions; a new BRDF method does not automatically enable them. Reject unsupported requested physics explicitly. GPU support requires checking the actual array/backend operations as well as the surface's floating-point type.
For a model with a closed-form quadrature matrix, implement create_surface_layer! directly. Use existing methods in src/CoreRT/Surfaces/lambertian_surface.jl as the reference for:
moving arrays through
array_type(architecture);filling
r⁻⁺,r⁺⁻,t⁺⁺, andt⁻⁻;applying the
m == 0rule for isotropic contributions;adding direct-beam source terms only when
SFIis true.
Register Scene Parsing
Scene files are parsed without eval. To expose MySurface(...) in YAML/TOML configs:
add an entry to
BRDF_MAPinsrc/IO/Parameters.jl;add a
_construct_surface(::Val{:MySurface}, FT, args)method;validate arity and argument types with
_require_nargsand_require_config.
Skeleton:
const BRDF_MAP = Dict{String, Function}(
# existing entries...
"MySurface" => (FT, args) -> _construct_surface(Val(:MySurface), FT, args),
)
function _construct_surface(::Val{:MySurface}, FT, args)
_require_nargs("MySurface", args, 2, "strength, shape")
return CoreRT.MySurface(FT(args[1]), FT(args[2]))
endThen a scene can use:
radiative_transfer:
surface:
- MySurface(0.2, 1.5)Add Focused Tests
Keep new tests small and local before adding an end-to-end RT case:
parser:
parse_surface_str("MySurface(...)", Float32)returnsMySurface{Float32};parser failure: bad arity throws
ArgumentError;reflectance: finite values, non-negative Stokes-I response to unpolarized light, and the appropriate physical polarization bounds (Mueller off-diagonal entries may be negative);
Fourier moment:
m = 0and at least one higher moment behave as expected;RT smoke: one tiny CPU scene runs with finite
RandT.
Use test/test_parameters_parser.jl, test/test_coxmunk.jl, and test/test_canopy.jl as the closest current patterns.
Linearized RT
If the new surface should contribute analytic Jacobians, add or extend the matching method under src/CoreRT/Surfaces/*_lin.jl. Check its parameter derivatives and atmospheric attenuation derivatives against centered finite differences of the complete forward output, including any exact direct-glint correction. Otherwise document the surface as forward-only and reject requests for its unimplemented tangent. See test/test_coxmunk_rt_contract.jl for absolute-radiance, source-scaling, replay, and tangent checks.
Further Reading
Engineering notes & derivation crib sheets: docs/dev_notes/ (see its README) — living technical references (verified theory crib sheet, design specs, linearization bug catalog) that go deeper than the public docs above. dev_notes/ lives outside docs/src/, so it is a GitHub link rather than a relative one.