Outputs
The current Julia package exposes light results in three increasingly concrete forms:
- in memory as
LightBudget - attached back onto MTG nodes as ARCHIMED-style attributes
- written to disk when you export the enriched scene
This page documents those three layers and then explains the ARCHIMED-style CSV outputs used by the public writer and regression harness.
1. In-Memory Outputs: LightBudget
The core output of run_light for one meteo row is shown below. This example uses 1 cm pixels for a quick demonstration and also requests sky visibility.
using ArchimedLight
repo_root = normpath(joinpath(dirname(pathof(ArchimedLight)), ".."))
config = joinpath(repo_root, "example_2", "config.yml")
sim, meteo = read_simulation(config)
update_options!(sim, LightOptions(sim.options; pixel_size=0.01, include_sky_fraction=true))
row = first(meteo)
step = run_light(sim, row)
budget = step.budget;LightBudget(ArchimedLight.InitialTotalSpectralNodeValues(ArchimedLight.SpectralNodeValues(Dict(3988 => 280.3093995578335, 601 => 10.950028780648013, 1356 => 32.55262679578977, 4651 => 387.380895057733, 3233 => 281.9231003155426, 960 => 161.94745736974994, 2874 => 112.46632777079627, 205 => 210.81342572403287, 5010 => 400.20542252255376, 5406 => 418.21289346772767…), Dict(3988 => 303.6685161876528, 601 => 11.862531179035352, 1356 => 35.265345695438924, 4651 => 419.66263631254407, 3233 => 305.4166920085045, 960 => 175.44307881722918, 2874 => 121.83852175169599, 205 => 228.38121120103565, 5010 => 433.5558743994332, 5406 => 453.0639679233716…)), ArchimedLight.SpectralNodeValues(Dict(3988 => 307.2035548943684, 601 => 24.573573375608866, 1356 => 48.92617341030917, 4651 => 394.0977431748425, 3233 => 305.3229538907195, 960 => 183.7425303933672, 2874 => 131.29404398014577, 205 => 236.33386064456994, 5010 => 404.1768827127145, 5406 => 423.269305274249…), Dict(3988 => 670.7169033446817, 601 => 359.308349438029, 1356 => 358.10352422075954, 4651 => 543.0669201473554, 3233 => 639.3888681154193, 960 => 505.74679271221703, 2874 => 456.09844250154856, 205 => 596.7168583229376, 5010 => 509.4749848943051, 5406 => 549.8000620317188…))), ArchimedLight.InitialTotalSpectralNodeValues(ArchimedLight.SpectralNodeValues(Dict(3988 => 90.1833845673988, 601 => 1.642833147949383, 1356 => 173.26603676054538, 4651 => 1440.6728360853435, 3233 => 2399.157671942166, 960 => 863.5815662746736, 2874 => 598.6182056884031, 205 => 1280.5076585244308, 5010 => 1488.366386707674, 5406 => 1555.334240542503…), Dict(3988 => 97.698666614682, 601 => 1.7797359102784989, 1356 => 187.70487315725748, 4651 => 1560.728905759122, 3233 => 2599.0874779373466, 960 => 935.5466967975635, 2874 => 648.5030561624369, 205 => 1387.2166300681336, 5010 => 1612.3969189333134, 5406 => 1684.9454272543778…)), ArchimedLight.SpectralNodeValues(Dict(3988 => 98.83598757377669, 601 => 3.686773954088875, 1356 => 260.41659291716076, 4651 => 1465.6528512329373, 3233 => 2598.2897691856137, 960 => 979.8033557642934, 2874 => 698.831433219229, 205 => 1435.5220379567963, 5010 => 1503.1362711733195, 5406 => 1574.1390419721195…), Dict(3988 => 215.78841282383925, 601 => 53.907042494261056, 1356 => 1906.0574982458836, 4651 => 2019.670484565899, 3233 => 5441.181324185927, 960 => 2696.8846222243315, 2874 => 2427.649561244488, 205 => 3624.5343693309724, 5010 => 1894.7405500042926, 5406 => 2044.7070745233054…))), ArchimedLight.InitialTotalSpectralNodeValues(ArchimedLight.SpectralNodeValues(Dict(3988 => 238.26298962415848, 601 => 9.307524463550811, 1356 => 27.669732776421302, 4651 => 329.27376079907305, 3233 => 239.6346352682112, 960 => 137.65533876428745, 2874 => 95.59637860517682, 205 => 179.19141186542794, 5010 => 340.1746091441707, 5406 => 355.4809594475685…), Dict(3988 => 30.366851618765274, 601 => 1.186253117903535, 1356 => 3.5265345695438914, 4651 => 41.9662636312544, 3233 => 30.541669200850446, 960 => 17.544307881722915, 2874 => 12.183852175169596, 205 => 22.838121120103562, 5010 => 43.355587439943314, 5406 => 45.30639679233715…)), ArchimedLight.SpectralNodeValues(Dict(3988 => 261.1230216602131, 601 => 20.887537369267537, 1356 => 41.58724739876279, 4651 => 334.9830816986161, 3233 => 259.52451080711154, 960 => 156.1811508343621, 2874 => 111.5999373831239, 205 => 200.88378154788444, 5010 => 343.5503503058073, 5406 => 359.77890948311165…), Dict(3988 => 67.07169033446816, 601 => 35.93083494380289, 1356 => 35.810352422075944, 4651 => 54.30669201473553, 3233 => 63.93888681154192, 960 => 50.57467927122169, 2874 => 45.60984425015484, 205 => 59.67168583229375, 5010 => 50.9474984894305, 5406 => 54.98000620317186…))), ArchimedLight.InitialTotalSpectralNodeValues(ArchimedLight.SpectralNodeValues(Dict(3988 => 76.65587688228898, 601 => 1.3964081757569755, 1356 => 147.27613124646356, 4651 => 1224.5719106725421, 3233 => 2039.2840211508408, 960 => 734.0443313334724, 2874 => 508.8254748351426, 205 => 1088.431509745766, 5010 => 1265.111428701523, 5406 => 1322.0341044611275…), Dict(3988 => 9.769866661468196, 601 => 0.17797359102784985, 1356 => 18.770487315725745, 4651 => 156.07289057591217, 3233 => 259.9087477937346, 960 => 93.55466967975633, 2874 => 64.85030561624367, 205 => 138.72166300681332, 5010 => 161.2396918933313, 5406 => 168.49454272543775…)), ArchimedLight.SpectralNodeValues(Dict(3988 => 84.01058943771018, 601 => 3.133757860975544, 1356 => 221.35410397958665, 4651 => 1245.8049235479966, 3233 => 2208.5463038077714, 960 => 832.8328523996494, 2874 => 594.0067182363447, 205 => 1220.1937322632768, 5010 => 1277.6658304973214, 5406 => 1338.0181856763015…), Dict(3988 => 21.578841282383923, 601 => 5.390704249426104, 1356 => 190.60574982458832, 4651 => 201.96704845658985, 3233 => 544.1181324185926, 960 => 269.6884622224331, 2874 => 242.76495612444873, 205 => 362.4534369330972, 5010 => 189.47405500042922, 5406 => 204.47070745233052…))), Dict{String, Dict{Int64, Float64}}(), Dict{String, Dict{Int64, Float64}}(), Dict("PAR" => Dict(), "NIR" => Dict()))The main grouped fields are:
budget.incident_fluxbudget.incident_energybudget.absorbed_fluxbudget.absorbed_energy
Each one is then split into:
initial: first-order interception onlytotal: first-order plus scattering
and by waveband, for example:
parnir
Typical accesses:
budget.incident_flux.initial.par;
budget.incident_flux.total.par;
budget.absorbed_energy.total.nir;Dict{Int64, Float64} with 5968 entries:
3988 => 21.5788
601 => 5.3907
1356 => 190.606
4651 => 201.967
3233 => 544.118
960 => 269.688
2874 => 242.765
205 => 362.453
5010 => 189.474
5406 => 204.471
2478 => 37.5016
2082 => 370.782
5369 => 198.967
5802 => 204.714
564 => 36.1288
2837 => 25.8185
4255 => 145.701
3629 => 25.1197
3196 => 46.5342
⋮ => ⋮Each leaf of that structure is a dictionary keyed by node id.
For artificial emitters, step.first_order.emitter_escaped_power.par and .nir report emitted power which left the represented scene without a geometric hit. These dictionaries are keyed by emitting source node. Together with emitter-contributed incident power, they provide the explicit received-plus-escaped power closure (in W) described in Artificial Light Emitters. Virtual sensors are non-consuming observations: their reported incident power is not subtracted from the ray and is excluded from this physical received-plus-escaped closure. A sensor may therefore report the same ray that is subsequently received by a physical component or escapes the scene. first_order.incident_power combines artificial emitters with sky and sun sources. To verify the closure from public fields, external sky/sun input must be zero and incident power must be summed over physical (non-sensor) receivers; sensor entries are observations, not consumed power. The emitter transfer is accounted separately before it is merged into total incident power. emitter_escaped_power exposes instantaneous PAR and NIR power. The integrated step budget exposes every emitted band, including custom bands, through step.budget.emitter_escaped_energy_per_band[band]. For PAR and NIR this is the corresponding escaped power multiplied by the step duration in seconds.
If you need canopy-view metadata for coupled models, step.sky_fraction stores the per-node visible-sky fraction when options.include_sky_fraction=true. When using a YAML config, read_options enables that option when component_variables.sky_fraction or opf_variables.sky_fraction is true.
2. Query Values By Scene Metadata
light_metric_values combines a light result with its scene metadata and returns a Tables.jl-compatible column table:
coffee_leaves = light_metric_values(
sim,
step,
:absorbed_par_energy;
species="coffee",
object_id=1,
symbol=:Leaf,
)
propertynames(coffee_leaves)(:step_number, :node_id, :source_topology_id, :object_id, :item_id, :component_id, :group, :type, :symbol, :scale, :value)The table includes the timestep, runtime and source node identifiers, object and output grouping identifiers, group/type, MTG symbol and scale, and the selected value. A series produces the same columns in long form:
coffee_series = light_metric_values(
sim,
[step],
:Ra_PAR_q;
species="coffee",
)
unique(coffee_series.step_number)1-element Vector{Int64}:
1All filters combine. group and species are aliases, object_id identifies one placed scene object, source_topology_id identifies its source component, and node_ids accepts one runtime node id or a collection. symbol, scale, type, exact attributes, and a custom where node predicate provide progressively more specific selection.
Selections can be reused:
leaf_ids = light_node_ids(sim; species="coffee", symbol=:Leaf)
leaf_par = light_metric_values(sim, step, :Ra_PAR_q; node_ids=leaf_ids)
leaf_nir = light_metric_values(sim, step, :Ra_NIR_q; node_ids=leaf_ids)
length(leaf_par.value) == length(leaf_nir.value)trueUse reduce=sum for a total. For example, these compute the whole-scene absorbed PAR and the plant-only total after excluding paving:
scene_absorbed_par = light_metric_values(
sim,
step,
:absorbed_par_energy;
reduce=sum,
)
plant_ids = setdiff(
light_node_ids(sim),
light_node_ids(sim; group="pavement"),
)
plant_absorbed_par = light_metric_values(
sim,
step,
:absorbed_par_energy;
node_ids=plant_ids,
reduce=sum,
)
scene_absorbed_par >= plant_absorbed_partrueAdding by, such as by=:object_id or by=(:group, :type), returns a grouped table. For a series, step_number is retained automatically. sink=DataFrame can materialize detailed or grouped results when DataFrames.jl is loaded.
Summing per-component energy (*_energy or historical *_q) gives an energy total. Flux values are normalized independently by each component's area, so their direct sum generally is not a meaningful scene-scale flux.
Dynamic scenes
Each result retains a lightweight node metadata snapshot by default. All steps computed without changing the scene share the same snapshot. After update_scene!, the next result receives a new snapshot, allowing one series to contain several runtime node-id layouts safely:
steps = LightStepResult[]
for (new_scene, row) in zip(scenes, meteo)
update_scene!(sim, new_scene)
push!(steps, run_light(sim, row))
end
second_coffee_leaf = light_metric_values(
sim,
steps,
:absorbed_par_energy;
species="coffee",
object_id=2,
source_topology_id=42,
)The stable component identity is normally (object_id, source_topology_id); runtime node_id remains local to one scene version. To retain additional scalar identifiers, construct options with node_metadata_attributes=(:organ_id, ...) and filter with attributes=(organ_id=...,). Set store_node_metadata=false when results are queried immediately and minimum retained memory is more important.
3. Attached Outputs: ARCHIMED Attribute Names
The convenience layer for visual inspection is attach_light_step!:
attach_light_step!(
sim.scene,
step;
fields=[:incident_par_flux, :incident_par_energy, :absorbed_par_energy],
);/ 1: Scene
├─ / 2: Individual
│ └─ / 3: Axis
│ ├─ / 4: Metamer
│ ├─ < 5: Metamer
│ ├─ < 6: Metamer
│ ├─ < 7: Metamer
│ ├─ < 8: Metamer
│ ├─ < 9: Metamer
│ ├─ < 10: Metamer
│ ├─ < 11: Metamer
│ │ ├─ + 12: Axis
│ │ │ ├─ / 13: Metamer
│ │ │ ├─ < 14: Metamer
│ │ │ ├─ < 15: Metamer
│ │ │ ├─ < 16: Metamer
│ │ │ ├─ < 17: Metamer
│ │ │ ├─ < 18: Metamer
│ │ │ ├─ < 19: Metamer
│ │ │ │ └─ + 20: Axis
│ │ │ │ ├─ / 21: Metamer
│ │ │ │ ├─ < 22: Metamer
│ │ │ │ ├─ < 23: Metamer
│ │ │ │ ├─ < 24: Metamer
…The default mappings are:
| Field selector | Attached attribute |
|---|---|
:area | area |
:incident_par_initial_flux | Ri_PAR_0_f |
:incident_nir_initial_flux | Ri_NIR_0_f |
:incident_par_flux | Ri_PAR_f |
:incident_nir_flux | Ri_NIR_f |
:incident_par_initial_energy | Ri_PAR_0_q |
:incident_nir_initial_energy | Ri_NIR_0_q |
:incident_par_energy | Ri_PAR_q |
:incident_nir_energy | Ri_NIR_q |
:absorbed_par_initial_flux | Ra_PAR_0_f |
:absorbed_nir_initial_flux | Ra_NIR_0_f |
:absorbed_par_flux | Ra_PAR_f |
:absorbed_nir_flux | Ra_NIR_f |
:absorbed_shortwave_flux | Ra_SW_f (Ra_PAR_f + Ra_NIR_f) |
:absorbed_par_initial_energy | Ra_PAR_0_q |
:absorbed_nir_initial_energy | Ra_NIR_0_q |
:absorbed_par_energy | Ra_PAR_q |
:absorbed_nir_energy | Ra_NIR_q |
:sky_fraction | sky_fraction |
The meaning follows the historical ARCHIMED naming:
area: prepared object surface area inm^2Ri: intercepted radiationRa: absorbed radiation_0_: first-order only- no
_0_: after scattering has been added _f: irradiance-like quantity inW m^-2_q: energy per component and per step inJ
Shortwave radiation is the sum of its PAR and NIR wavebands. In particular, Ra_SW_f is not an alias for Ra_NIR_f. The typed component and source-owner tables derive it as
\[Ra\_SW\_f = Ra\_PAR\_f + Ra\_NIR\_f.\]
The table also derives aPPFD from absorbed PAR using the requested PAR-energy-to-photon conversion factor:
owner_light = component_values(step; level=:source_owner)
all(isapprox.(
owner_light.Ra_SW_f,
owner_light.Ra_PAR_f .+ owner_light.Ra_NIR_f,
))trueIf a downstream package reads attributes attached to the scene, attach the PAR and NIR values under distinct names. Request :absorbed_shortwave_flux to attach their sum as Ra_SW_f. The historical Dict(:absorbed_nir_flux => :Ra_SW_f) mapping remains accepted and now produces the same PAR+NIR sum, with a deprecation warning.
4. Disk Outputs: Exported Scenes
Once results are attached, you can export the enriched scene:
scene_path = joinpath(mktempdir(), "scene_with_light.opf")
write_scene(scene_path, sim.scene)
isfile(scene_path)trueSupported export formats are:
.ops.opf.gwa
The export path determines the format.
This is currently the main disk output mechanism of ArchimedLight.jl: attach node attributes, then write the scene back out.
Example Output Images


ARCHIMED-Style CSV Tables
The public writer produces component_values.csv. Historical fixture datasets and internal regression tools may also contain files such as:
component_values.csvscene_values.csvsummary.csv
The example writes a fresh component_values.csv on demand, while the regression and release harnesses under test/ keep their own isolated reference data.
component_values.csv
Component-scale outputs, typically one row per scene node and step. Common columns are:
step_numberobject_idsource_topology_idgrouptypeareaRi_*Ra_*
You can write this table from already-computed results:
series = run_light(sim, meteo)
component_path = joinpath(mktempdir(), "component_values.csv")
write_component_values(component_path, sim, series)
isfile(component_path)truestep_index_base=0 is available for compatibility with historical harness outputs.
scene_values.csv
Scene-scale summary per meteo step.
summary.csv
Grouped aggregation, usually by item, group, and type.
Which Output Form Should You Use
- use
LightBudgetwhen staying inside Julia - use
attach_light_step!when you want visual inspection or downstream scene export - use
component_values.csvwhen you need a portable component-scale table