The light solver does not fundamentally need a model file. It needs a mapping from scene components to radiative behavior.
Concretely, a model description must answer these questions:
given a scene node (group, type), which radiative model applies?
how transparent is that component at first interception?
how strongly does it scatter light in each waveband?
is it a virtual sensor?
does it emit light itself?
Historically this information often comes from YAML model files. In interactive workflows, the same semantics are built directly with GroupModel, TypeModel, InterceptionModel, and EmitterModel.
Whatever the source format, the runtime ultimately wants a LightModels object that maps scene (group, type) pairs to model behavior.
The rest of this page is organized around those semantics rather than around the YAML syntax.
Model matching is driven by the scene node (group, type) pair.
The runtime resolves a node with this precedence:
exact group and exact type
exact group and wildcard type *
wildcard group * and exact type
wildcard group * and wildcard type *
This is true whether models come from files or from Julia objects.
Only nodes with geometry need a model. Non-geometric topology nodes such as plant, axis, phytomer, or grouping nodes are ignored by model validation.
For interactive workflows, the compact constructor is usually easiest:
models = models_for(
"coffee" => (
"Leaf" => translucent(par=0.15, nir=0.90),
"Stem" => translucent(par=0.20, nir=0.50),
),
"soil" => (
"ground" => opaque(par=0.10, nir=0.40),
),
)Use "*" as a type fallback:
models = models_for(
"coffee" => (
"*" => translucent(par=0.15, nir=0.90),
"Stem" => translucent(par=0.20, nir=0.50),
),
)Historically, one YAML file usually describes one functional group:
---
Group: coffee
Type:
Metamer:
Interception:
model: Translucent
transparency: 0.1
optical_properties:
PAR: 0.15
NIR: 0.9
Leaf:
Interception:
model: Translucent
transparency: 0.0
optical_properties:
PAR: 0.15
NIR: 0.9
...The correspondences are:
Group -> functional group name
Type -> mapping from component types to model definitions
Interception -> first-order interception and scattering behavior
LightEmitter -> optional emitted radiance
The same model can be built directly in memory:
models = prepare_models([
GroupModel(
"coffee";
types=OrderedDict(
"Leaf" => TypeModel(
interception=InterceptionModel(
model="Translucent",
transparency=0.0,
optical_properties=OpticalProperties(0.15, 0.90),
),
),
"Metamer" => TypeModel(
interception=InterceptionModel(
model="Translucent",
transparency=0.1,
optical_properties=OpticalProperties(0.15, 0.90),
),
),
),
),
])This is the direct equivalent of the YAML file above.
Ground in both workflows uses opaque(par=..., nir=...), which returns the same TypeModel type with zero transmission and outward reflection. The voxel-only ground_materials keyword maps terrain material IDs to full soil models or partial (par=..., nir=...) overrides. Triangle scenes instead use their ordinary group/type mappings; they do not use ground_materials. See the Ground Materials example.
The dynamic correspondences are:
Group: coffee -> GroupModel("coffee"; ...)
Type: Leaf: ... -> "Leaf" => TypeModel(...)
Interception: ... -> InterceptionModel(...)
LightEmitter: ... -> EmitterModel(...)
The core light package mainly uses the Interception block.
model#The standard historical model is Translucent:
Interception:
model: Translucentand dynamically:
InterceptionModel(model="Translucent")Translucent is the historical model for plant organs and other ordinary components. For terrain, select Opaque explicitly or use opaque(par=..., nir=...); its PAR/NIR coefficients are reflectances, its transmission is zero and the absorbed fraction is 1 - reflectance.
Opaque currently supports terrain made of nonvertical facets, with normals oriented upward regardless of input winding. Geometry must represent terrain; it is not a general opaque model for vertical stems, arbitrary leaves or closed solids. Model matching still uses ordinary group/type labels.
transparency#transparency controls first-order pass-through in Translucent. It reduces the fraction intercepted by the visible surface in both the fast upper-hit path and the full per-pixel stack path. It is distinct from diffuse transmission during scattering: even with transparency=0, Translucent splits its scattering coefficient equally between reflection and transmission. Opaque instead has zero first-order and diffuse transmission.
For first-order-only interception, ArchimedLight follows the historical ARCHIMED upper-hit rule: only the first visible surface is retained for that pixel. It intercepts the fraction 1 - transparency; the transmitted fraction is not assigned to lower objects. This preserves the compact upper-hit pixel table instead of switching transparent scenes to a full-stack traversal.
File-based:
Interception:
transparency: 0.1Dynamic equivalent:
InterceptionModel(transparency=0.1)Typical values are in [0, 1]:
0.0 for a fully intercepting component
larger values for partially transmitting components
optical_properties#optical_properties stores waveband-dependent coefficients. For Translucent, these are total diffuse scattering fractions, split equally between reflection and transmission. For Opaque, they are reflectances with zero transmission. In both cases, the remaining intercepted fraction is absorbed.
File-based:
Interception:
optical_properties:
PAR: 0.15
NIR: 0.90Dynamic equivalent:
InterceptionModel(
optical_properties=OpticalProperties(0.15, 0.90),
)For the current package:
PAR and NIR are the built-in bands
extra shortwave bands are also supported
Example with one custom band in YAML:
optical_properties:
PAR: 0.15
NIR: 0.90
custom: 0.15The optical coefficient is the scattered fraction. When no more detailed information is available, absorptance is then derived approximately as:
absorptance = 1 - scattering_coefficient
Virtual sensors receive light but remain transparent in the transfer logic. Their scattering coefficient is zero for every waveband, including custom bands.
File-based:
Interception:
model: VirtualSensoror
Interception:
sensor: trueDynamic equivalent:
InterceptionModel(model="VirtualSensor")or
InterceptionModel(sensor=true)Use this for diagnostic surfaces that should observe the radiative field without perturbing it like an opaque object.
Historical ARCHIMED files sometimes store several named parameter sets under one process block and select one with use.
File-based example:
Interception:
use: Translucent_1
Translucent_1:
model: Translucent
transparency: 0.0
optical_properties:
PAR: 0.15
NIR: 0.90
Translucent_2:
model: Translucent
transparency: 0.2
optical_properties:
PAR: 0.20
NIR: 0.85The dynamic equivalent is not usually written by hand, but the runtime preserves the same structure internally through the use and variants fields of InterceptionModel.
Components can also emit light.
File-based:
LightEmitter:
model: LambertianEmitter
radiance: 20.0
gamma:
PAR: 0.48
NIR: 0.52Dynamic equivalent:
TypeModel(
light_emitter=EmitterModel(
model="LambertianEmitter",
radiance=20.0,
gamma=OpticalProperties(0.48, 0.52),
),
)This is useful for artificial lighting or diagnostic scenes. Most canopy workflows still rely mainly on meteo forcing. radiance is expressed per emitting surface area and steradian, and gamma values are independent spectral coefficients rather than fractions that are renormalized. Custom entries in gamma are transported under the same band name; they do not inherit or reuse the PAR coefficient. An emitter-only custom band is included in the result even when the meteo table has no matching RI_<band>_f column. For the source notation and how emitters enter the light algorithm, see Artificial Light Emitters.
Wildcards are often the most useful part of interactive model construction, because they let you start from a broad fallback and refine only the important cases.
File-based:
Group: coffee
Type:
"*":
Interception:
model: Translucent
transparency: 0.0
optical_properties:
PAR: 0.15
NIR: 0.90Dynamic equivalent:
GroupModel(
"coffee";
types=OrderedDict(
"*" => TypeModel(
interception=InterceptionModel(
model="Translucent",
transparency=0.0,
optical_properties=OpticalProperties(0.15, 0.90),
),
),
),
)This means: any type inside the coffee group uses this fallback unless a more specific type is declared.
Dynamic equivalent:
models = prepare_models([
GroupModel(
"*";
types=OrderedDict(
"*" => TypeModel(
interception=InterceptionModel(
model="Translucent",
transparency=0.0,
optical_properties=OpticalProperties(0.15, 0.30),
),
),
),
),
])This is useful for:
synthetic scenes
tests
progressive setup before writing exact (group, type) rules
Soil and paving use ordinary group/type matching. Choose Opaque to reflect all scattered energy outward from the terrain rather than splitting it between the two sides. Existing Translucent inputs keep their two-sided law; a group name such as soil or pavement does not select opacity automatically:
Group: pavement
Type:
Cobblestone:
Interception:
model: Opaque
transparency: 0
optical_properties:
PAR: 0.15
NIR: 0.9
plot_paving: 80Dynamic equivalent:
GroupModel(
"pavement";
types=OrderedDict(
"Cobblestone" => opaque(par=0.15, nir=0.90),
),
)The plot_paving hint belongs to the file-based convenience layer used by read_simulation; the radiative behavior itself is still just an ordinary interception model.
When a Translucent model omits a waveband in optical_properties, the runtime falls back to the corresponding option in LightOptions:
missing PAR -> options.scattering_coeff_par
missing NIR -> options.scattering_coeff_nir
Opaque requires explicit PAR and NIR reflectances; missing coefficients are rejected rather than replaced with those fallbacks.
That is why the global scattering coefficients still matter even when most groups are explicitly parameterized.
Historical model files may still contain:
Photosynthesis
StomatalConductance
many energy-balance parameters
Those sections remain useful archival context, but they are not part of the active light API of ArchimedLight.jl. For light simulations, the important parts are the group/type mapping, interception behavior, optional emitters, and wildcard rules.