Sometimes we need leaf temperature and photosynthesis every hour, but a summary only once a day. This tutorial runs the coupled leaf model for 48 hours and produces one summary for each day:
total net CO₂ assimilation per unit leaf area;
transpiration expressed in mm;
mean, minimum, and maximum leaf temperature.
Start with Several time steps if you only need models that all run at the same interval. This page adds a small summary model to show how calculations at different intervals can work together.
Don't have time? Ask you AI agent
This section is a bit advanced. If you find it too hard to follow, just ask your AI agent to do the work for you. First, install PlantSimEngine's skill (you can aslo point your agent to this page and ask it to install the skill), then just ask it what you need and it will be able to produce all the code for you.
We keep air temperature and absorbed light constant. Humidity and wind change within each day, and the second day is drier and windier. These are illustrative conditions for comparing daily summaries, not a realistic day–night light cycle.
using PlantBiophysics, PlantSimEngine, PlantMeteo, Dates, DataFrames
rh_day1 = [0.75 - 0.20 * max(0.0, sin((hour - 6.0) / 12.0 * pi)) for hour in 0:23]
wind_day1 = [0.8 + 0.4 * max(0.0, sin((hour - 6.0) / 12.0 * pi)) for hour in 0:23]
Rh = vcat(rh_day1, rh_day1 .- 0.10)
Wind = vcat(wind_day1, wind_day1 .+ 0.2)
weather = Weather([
Atmosphere(
T=25.0, Wind=Wind[i], P=101.3, Rh=Rh[i], Cₐ=400.0,
Ri_SW_f=300.0, duration=Hour(1),
) for i in 1:48
])
λ_ref = weather[1].λA is a rate (µmol CO₂ m⁻² s⁻¹). To obtain the amount assimilated over a day, we multiply each hourly rate by its duration in seconds, then add the amounts. For transpiration, integrating λE (W m⁻²) gives energy per leaf area; dividing by the latent heat of vaporization λ_ref (J kg⁻¹) gives kg m⁻², numerically equal to mm of water. This is water per leaf area, not per ground area. Using one λ_ref is appropriate here because air temperature is constant.
The summary model simply stores the values calculated from the hourly history. This is a small user-defined model; the model implementation tutorial explains these declarations in detail. Required(Real) means that each input must be supplied by the simulation as a real-valued number.
PlantSimEngine.@process "dailyleafsummary" verbose=false
struct DailyLeafSummary <: AbstractDailyleafsummaryModel end
PlantSimEngine.inputs_(::DailyLeafSummary) = (
A_integrated=Required(Real),
transpiration_integrated=Required(Real),
Tₗ_mean=Required(Real),
Tₗ_min=Required(Real),
Tₗ_max=Required(Real),
)
PlantSimEngine.outputs_(::DailyLeafSummary) = (
A_daily=-Inf,
transpiration_daily=-Inf,
Tₗ_mean_daily=-Inf,
Tₗ_min_daily=-Inf,
Tₗ_max_daily=-Inf,
)
function PlantSimEngine.run!(::DailyLeafSummary, status, environment, constants, context)
status.A_daily = status.A_integrated
status.transpiration_daily = status.transpiration_integrated
status.Tₗ_mean_daily = status.Tₗ_mean
status.Tₗ_min_daily = status.Tₗ_min
status.Tₗ_max_daily = status.Tₗ_max
return nothing
endThe DailyLeafSummary model is just a very simple model used only for teaching purposes. The only thing it does is taking some inputs, and renaming them. This is because a model is defined to run for one time-step, one object, without any knowledge about other models time-steps and objects. The integration over several time-steps (or objects) is done at the time we define the simulation setup to build a composite model. You'll understand how in the next sections.
A ModelSpec says where and how often to use a model. The three leaf models run every hour. The daily model reads their preceding 24 hours of results: Integrate applies the duration conversions above, and Aggregate calculates a mean or an extreme value.
integrate_rate = Integrate((values, seconds) -> sum(values .* seconds))
integrate_water = Integrate((values, seconds) -> sum(values .* seconds) / λ_ref)
scene = CompositeModel(
Object(:scene; status=Status(
d=0.03, Ra_SW_f=150.0, sky_fraction=1.0, aPPFD=1200.0,
));
applications=(
ModelSpec(Monteith(); name=:energy_balance, on=One(), every=Hour(1)),
ModelSpec(Fvcb(); name=:photosynthesis, on=One(), every=Hour(1)),
ModelSpec(Medlyn(0.03, 12.0); name=:stomatal_conductance, on=One(), every=Hour(1)),
ModelSpec(
DailyLeafSummary();
name=:daily_summary,
on=One(),
inputs=(
A_integrated=One(within=Self(), application=:energy_balance,
var=:A, policy=integrate_rate, window=Day(1)),
transpiration_integrated=One(within=Self(), application=:energy_balance,
var=:λE, policy=integrate_water, window=Day(1)),
Tₗ_mean=One(within=Self(), application=:energy_balance,
var=:Tₗ, policy=Aggregate(), window=Day(1)),
Tₗ_min=One(within=Self(), application=:energy_balance,
var=:Tₗ, policy=Aggregate(MinReducer()), window=Day(1)),
Tₗ_max=One(within=Self(), application=:energy_balance,
var=:Tₗ, policy=Aggregate(MaxReducer()), window=Day(1)),
),
every=ClockSpec(24.0, 24.0),
),
),
environment=weather,
)on=One() applies each model to the only object in the scene, our leaf. Self() selects that same leaf, and application=:energy_balance selects results saved by the coupled energy-balance calculation. window=Day(1) chooses the period to summarize. With an hourly base step, ClockSpec(24.0, 24.0) runs the summary every 24 steps, starting at step 24. This avoids a partial-day summary at startup. every=Day(1) alone would start at step 1, then run at steps 25, 49, and so on.
Integrating rates
Integrate() without a function adds values; it does not multiply them by time. Always include the duration conversion when integrating a rate. A time window also does not automatically align with midnight: choose the start of your weather series accordingly.
simulation = run!(scene; steps=length(weather), outputs=:all)
rows = collect_outputs(simulation; sink=DataFrame)
daily_rows = subset(rows, :application_id => ByRow(==(:daily_summary)))
daily_results = unstack(
select(daily_rows, :timestep, :variable, :value),
:timestep, :variable, :value,
)
sort!(daily_results, :timestep)| Row | timestep | A_daily | Tₗ_max_daily | Tₗ_mean_daily | Tₗ_min_daily | transpiration_daily |
|---|---|---|---|---|---|---|
| Int64 | Float64? | Float64? | Float64? | Float64? | Float64? | |
| 1 | 24 | 2.97372e6 | 24.3728 | 23.8114 | 22.6418 | 7.5803 |
| 2 | 48 | 2.9536e6 | 23.4556 | 22.9559 | 21.9105 | 9.50098 |
The table contains one result at hour 24 and one at hour 48. A_daily is in µmol CO₂ m⁻² of leaf over the day, transpiration_daily is in mm over the day per leaf area, and the three temperature statistics are in °C. The different humidity and wind conditions give different daily fluxes even though absorbed light and air temperature stay fixed.
We can also check the first daily assimilation total directly against the saved hourly rates:
hourly_A = subset(
rows,
:application_id => ByRow(==(:energy_balance)),
:variable => ByRow(==(:A)),
:timestep => ByRow(<=(24)),
)
manual_total = sum(hourly_A.value) * 3600
@assert isapprox(daily_results.A_daily[1], manual_total)
(manual_total=manual_total, daily_model=daily_results.A_daily[1])For other combinations of model intervals and history windows, see PlantSimEngine's time guide.