After the one-timestep example, let's simulate a leaf over six hours. A simple light model absorbs a fixed fraction of the incoming radiation at each hour. We will run the whole weather series in one call, collect the results, and plot the leaf's response.
Weather is a sequence of Atmosphere values, one for each timestep. Here we create it from a small table; you can also import measured weather as shown on the Micro-climate page.
using PlantBiophysics, PlantSimEngine, PlantMeteo, Dates, DataFrames
forcing = DataFrame(
timestep=1:6,
T=[20.0, 21.0, 23.0, 25.0, 24.0, 22.0],
Wind=[1.0, 1.0, 1.5, 2.0, 1.5, 1.0],
Rh=[0.65, 0.62, 0.58, 0.55, 0.58, 0.63],
Ri_PAR_f=[100.0, 200.0, 300.0, 360.0, 200.0, 80.0],
Ri_NIR_f=[120.0, 240.0, 360.0, 430.0, 240.0, 100.0],
P=fill(101.3, 6),
duration=fill(Hour(1), 6)
)
weather = Weather(forcing)
first(forcing, 3)| Row | timestep | T | Wind | Rh | Ri_PAR_f | Ri_NIR_f | P | duration |
|---|---|---|---|---|---|---|---|---|
| Int64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Hour | |
| 1 | 1 | 20.0 | 1.0 | 0.65 | 100.0 | 120.0 | 101.3 | 1 hour |
| 2 | 2 | 21.0 | 1.0 | 0.62 | 200.0 | 240.0 | 101.3 | 1 hour |
| 3 | 3 | 23.0 | 1.5 | 0.58 | 300.0 | 360.0 | 101.3 | 1 hour |
Weather supplies the appropriate row automatically at each timestep. Ri_PAR_f and Ri_NIR_f are incoming photosynthetically active and near-infrared radiation arriving at the leaf, both in W m⁻² of leaf area. The light model will calculate how much of this radiation the leaf absorbs.
The other units are °C for T, m s⁻¹ for Wind, and kPa for P. Rh is a fraction, not a percentage. These values describe an illustrative six-hour weather sequence.
Add ConstantAbsorption to the same leaf model as before. Here it absorbs 80% of incoming PAR and 20% of incoming NIR. These are illustrative fractions; choose values appropriate for your leaf and light source.
scene = CompositeModel(
ConstantAbsorption(α_PAR=0.8, α_NIR=0.2),
Monteith(),
Fvcb(),
Medlyn(0.03, 12.0);
status=Status(sky_fraction=1.0, d=0.03),
environment=weather,
)ConstantAbsorption supplies aPPFD to photosynthesis and Ra_SW_f to energy balance automatically. The fractions stay constant, but the absorbed radiation changes with each weather row. No LAI or manual radiation update is needed. See Light interception for the equations and an example with controlled lighting.
Pass the number of weather records to run! to simulate all six hours. PlantSimEngine recalculates light interception and the leaf response at each step, so there is no need to update radiation values by hand.
simulation = run!(scene; steps=length(weather), outputs=:all)
current_step(simulation)outputs=:all saves the calculated values at every step; the default outputs=:none only leaves the latest values on the objects.
The latest state is always available directly on the leaf:
leaf = only(model_objects(scene))
(Tₗ=leaf.status.Tₗ, A=leaf.status.A, Gₛ=leaf.status.Gₛ, λE=leaf.status.λE)And more generally, collect_outputs returns a long-form table of all recorded variables that were requested in run!. Each row corresponds to a single variable, object, application, and timestep:
sim_long_form = collect_outputs(simulation; sink=DataFrame)
first(sim_long_form, 3) # Only showing the first three rows for brevity| Row | timestep | datetime | application_id | object_id | variable | value |
|---|---|---|---|---|---|---|
| Int64 | DateTime | Symbol | Symbol | Symbol | Float64 | |
| 1 | 1 | 2026-10-01T09:33:24.488 | energy_balance | scene | A | 21.0687 |
| 2 | 1 | 2026-10-01T09:33:24.488 | energy_balance | scene | Cᵢ | 344.891 |
| 3 | 1 | 2026-10-01T09:33:24.488 | energy_balance | scene | Cₛ | 367.605 |
We can turn the long-form representation into a wide table with one row per timestep using unstack. Then we can join the meteorological data with the simulation results using leftjoin. This puts the inputs and results in the same table. We use timestep to match each result to its input row.
Note
Requiring all variables as outputs makes a slower simulation. If you only need a few variables, request them explicitly in run! to speed up the calculation. Else, you can filter the long-form table after the simulation to keep only the variables you need using subset.
# Selecting only the variables we need:
df_long_form_few = subset(
sim_long_form,
:variable => ByRow(in((:Tₗ, :A, :Gₛ, :λE))),
)
df_wide = unstack(
select(df_long_form_few, :timestep, :variable, :value),
:timestep,
:variable,
:value,
)
# Joining the weather and simulation results:
df = leftjoin(forcing, df_wide; on=:timestep)
# Selecting only the variables we need:
select(df, :timestep, :T, :Ri_PAR_f, :Ri_NIR_f, :Tₗ, :A, :Gₛ, :λE)| Row | timestep | T | Ri_PAR_f | Ri_NIR_f | Tₗ | A | Gₛ | λE |
|---|---|---|---|---|---|---|---|---|
| Int64 | Float64 | Float64 | Float64 | Float64? | Float64? | Float64? | Float64? | |
| 1 | 1 | 20.0 | 100.0 | 120.0 | 18.8992 | 21.0687 | 0.930119 | 163.07 |
| 2 | 2 | 21.0 | 200.0 | 240.0 | 20.5708 | 29.1811 | 1.16672 | 230.294 |
| 3 | 3 | 23.0 | 300.0 | 360.0 | 22.8 | 34.793 | 1.0901 | 323.47 |
| 4 | 4 | 25.0 | 360.0 | 430.0 | 24.3963 | 35.6007 | 1.16842 | 415.887 |
| 5 | 5 | 24.0 | 200.0 | 240.0 | 22.6379 | 29.9147 | 1.09635 | 294.163 |
| 6 | 6 | 22.0 | 80.0 | 100.0 | 20.4731 | 18.123 | 0.755468 | 166.753 |
The long-form representation also records the publishing application and object. Filter by application_id before reshaping whenever several applications may publish variables with the same name.
The first panel compares leaf and air temperature. The second shows how net assimilation changes with the supplied weather and light.
using CairoMakie
figure = Figure(size=(800, 330))
temperature_axis = Axis(figure[1, 1]; xlabel="Timestep (hour)", ylabel="Temperature (°C)")
lines!(temperature_axis, df.timestep, df.T; label="Air")
lines!(temperature_axis, df.timestep, Float64.(df.Tₗ); label="Leaf")
axislegend(temperature_axis; position=:lt)
assimilation_axis = Axis(figure[1, 2]; xlabel="Timestep (hour)", ylabel="A (µmol CO₂ m⁻² s⁻¹)")
scatterlines!(assimilation_axis, df.timestep, Float64.(df.A))
figure
Continue with Several objects to compare leaves receiving different amounts of light.