Tutorial: File-Based Workflow

This tutorial is the standard ARCHIMED workflow: a scene on disk, one or more model files, a meteo CSV, and a config.yml that ties them together.

It uses the deliberately small fixture under example_1/, because every file is compact enough to read directly. For the live figure in this page, the tutorial disables the dense automatic paving requested by plot_paving: 80 and replaces it with a compact explicit ground patch, so the soil interception is visible and the plant does not disappear in a sea of tiny tiles.

Directory Layout

The example folder contains:

example_1/
├── README.md
├── config.yml
├── meteo.csv
├── model_simple.yml
├── model_soil.yml
├── full_featured_example.jl
└── scene/
    ├── simple.ops
    └── opf/simple_OPF_shapes.opf

The roles of those files are:

  • config.yml: top-level wiring, global light options, and file paths
  • simple.ops: scene placement, plot bounds, and functional-group assignment
  • simple_OPF_shapes.opf: plant topology and meshes
  • model_simple.yml, model_soil.yml: optical properties by functional group and type
  • meteo.csv: one or more simulation time steps
  • full_featured_example.jl: the public read_simulation/run_light workflow

Step 1: Load Everything From The Config

using ArchimedLight
using PlantGeom

repo_root = normpath(joinpath(dirname(pathof(ArchimedLight)), ".."))
config = joinpath(repo_root, "example_1", "config.yml")
sim, meteo = read_simulation(config; plot_paving_override=0)
scene = sim.scene
add_ground!(
    scene;
    nx=4,
    ny=4,
    xy_bounds=(-0.18, -0.18, 0.18, 0.18),
    group="pavement",
    type="Cobblestone",
)
rows = collect(meteo)
1-element Vector{PlantMeteo.TimeStepRow{@NamedTuple{date::Union{Missing, Dates.Date}, hour_start::Dates.Time, hour_end::Dates.Time, temperature::Int64, relativeHumidity::Int64, wind::Int64, RI_PAR_f::Int64, RI_custom_f::Int64}}}:
 ╭──── TimeStepRow ─────────────────────────────────────────────────────────────╮
  Step 1: date=2016-06-12, hour_start=08:00:00, hour_end=08:30:00, temperat   
  ure=14, relativeHumidity=70, wind=1, RI_PAR_f=10, RI_custom_f=10            
╰──────────────────────────────────────────────────────────────────────────────╯

At this point:

  • options is a LightOptions
  • scene is a PlantGeom.SceneGeometry
  • meteo is a PlantMeteo.TimeStepTable
  • models is a LightModels

The soil optics still come from model_soil.yml. The only thing changed here is how the paving geometry is materialized for the demonstration figure.

Step 2: Run One Step Or A Whole Series

For a single time step:

row = first(rows)
step = run_light(sim, row)
LightStepResult
  ------------------------------------------------------------
  sky        PAR 10 W m^-2  |  NIR 10.833 W m^-2
  sun        azimuth 72°  |  elevation 36.6°
  mix        direct 0%  |  diffuse 100%  |  SW 20.833 W m^-2
  turtle     17 sectors (sky=16, sun=1)
  ------------------------------------------------------------
  incident   PAR 2.35 kJ  |  NIR 2.691 kJ
  absorbed   PAR 1.997 kJ  |  NIR 269.054 J
  scattering on  |  iterations 3  |  converged true
  added      PAR 16.736 J  |  NIR 163.102 J
  extra bandsCUSTOM 10 W m^-2
  ------------------------------------------------------------

For every meteo row:

series = run_light(sim, meteo)

Enabling LightOptions(cache_radiation=true) allows run_light to reuse directional responses across meteo rows on the same scene, substantially improving performance.

Step 3: Inspect The Budget

budget = step.budget

budget.incident_flux.initial.par
budget.incident_flux.total.par
budget.absorbed_energy.total.par

Common reading patterns are:

  • first-order only: budget.incident_flux.initial.par
  • with scattering: budget.incident_flux.total.par
  • incident energy: budget.incident_energy.total.par
  • absorbed quantities: budget.absorbed_flux.total.par

Incident light is the light that arrives on the object. This light can then be absorbed, transmitted, or reflected. Absorbed light is the portion of the incident light that is absorbed by the object, while transmitted light is the portion that passes through the object, and reflected light is the portion that bounces off the surface of the object. Both incident and absorbed quantities are important for understanding the energy balance and photosynthetic activity of plants. Incident flux is more relevant for visualisation and export, while absorbed energy is more relevant for photosynthesis and energy balance.

Flux values are in W m^-2 and energy values are in J per component per step. The wavebands are par for photosynthetically active radiation, nir for near-infrared, and any extra shortwave bands present in the meteo file.

Step 4: Write Results Back Onto The Scene

After computing the light interception, you can attach the results back onto the scene for visualization or export. This is done with the attach_light_step! function, which takes the scene, the light step result, and a list of fields to attach.

attach_light_step!(
    scene,
    step;
    fields=[:incident_par_flux, :incident_par_energy, :absorbed_par_energy],
);
/ 1: Scene
├─ / 2: Scene
│  └─ / 3: Individual
│     └─ / 4: Axis
│        ├─ / 5: Internode
│        │  └─ + 6: Leaf
│        └─ < 7: Internode
│           └─ + 8: Leaf
├─ + 9: Cobblestone
├─ + 10: Cobblestone
├─ + 11: Cobblestone
├─ + 12: Cobblestone
├─ + 13: Cobblestone
├─ + 14: Cobblestone
├─ + 15: Cobblestone
├─ + 16: Cobblestone
├─ + 17: Cobblestone
├─ + 18: Cobblestone
├─ + 19: Cobblestone
├─ + 20: Cobblestone
├─ + 21: Cobblestone
├─ + 22: Cobblestone
├─ + 23: Cobblestone
└─ + 24: Cobblestone
…

This keeps the historical ARCHIMED attribute names on the nodes:

  • Ri_PAR_f
  • Ri_PAR_q
  • Ra_PAR_q

The figure below uses that attached Ri_PAR_f field, so you can verify that the ground really intercepts light too:

using PlantGeom, CairoMakie
CairoMakie.activate!(type = "png") # Save as PNG to save sa-pace (wrt SVG)
par_max = maximum(values(step.budget.incident_flux.total.par))

fig, ax, p = plantviz(
    scene.mtg;
    color=:Ri_PAR_f,
    colormap=:thermal,
    colorrange=(0.0, par_max),
    # color_missing=:gray85,
    figure=(size=(900, 700),),
)

PlantGeom.colorbar(fig[1, 2], p, label="Intercepted PAR (W m⁻²)")
fig
Example block output

Then we can write the scene with attached results back to disk:

out_path = joinpath(mktempdir(), "example_1_scene_with_light.ops")
write_scene(out_path, scene)
"/tmp/jl_AaVYNk/example_1_scene_with_light.ops"

Reading The Input Files Together

The file workflow is easiest to understand when you see the four inputs as one linked dataset:

  1. config.yml selects the scene, meteo file, and model files, and defines global runtime options such as sky_sectors, pixel_size, and toricity based on the files content.
  2. simple.ops places the plant object into the plot and assigns it to the functional group simple_plant.
  3. model_simple.yml says how simple_plant components intercept and scatter PAR, NIR, and the custom waveband.
  4. meteo.csv provides the forcing for the simulation step.

The reference pages expand each one:

Current Julia Behavior Vs Historical ARCHIMED YAML

The examples intentionally preserve the old ARCHIMED config style. That is useful for compatibility, but not every historical key is active in the current package.

In particular:

  • scene, model, and meteo paths are active
  • the light options described on the configuration reference page are active
  • many Java-era output toggles remain as documentation or archival context
  • photosynthesis and energy-balance settings are not part of the current ArchimedLight.jl runtime, because they are delegated to PlantBiophysics.jl.