Compare daily and weekly water uptake from the same hourly leaf rates. The rates below are deliberately constant teaching values: 1 and 2 mg of water per leaf per second. They show how to calculate totals over different time intervals; they do not predict plant water demand.
To calculate each total, multiply the hourly rates by the duration they represent, then add the two leaves' amounts over a day or a week. Both totals use the hourly values directly. This avoids counting the same water twice by adding daily totals whose periods might overlap.
The reusable teaching models return a supplied rate and add supplied amounts. Their definitions are included at the end of this page; load them now to configure this simulation.
using Dates, DataFrames, PlantSimEngine
include(joinpath(pkgdir(PlantSimEngine), "docs", "src", "guides", "time", "teaching_models.jl"))
using .TeachingTimeModelsevery says how often a model runs; window says how far back it looks for input values. Hourly, daily and weekly durations all fit an hourly base step. The function passed to Integrate multiplies each rate by the duration it represents, in seconds. This converts mg s⁻¹ into mg before the plant adds the leaf amounts.
integrate_rate = Integrate((values, durations_seconds) -> sum(values .* durations_seconds))
model = CompositeModel(
Object(:plant; scale=:Plant),
Object(:leaf_1; scale=:Leaf, parent=:plant, status=Status(rate_mg_s=1.0)),
Object(:leaf_2; scale=:Leaf, parent=:plant, status=Status(rate_mg_s=2.0));
applications=(
ModelSpec(HourlyWaterRate(); name=:hourly, on=Many(scale=:Leaf), every=Hour(1)),
ModelSpec(
SumWaterAmounts(); name=:daily, on=One(scale=:Plant),
inputs=(amounts_mg=Many(
scale=:Leaf, within=Subtree(), application=:hourly,
var=:water_rate_mg_s, policy=integrate_rate, window=Day(1),
),),
every=Day(1),
),
ModelSpec(
WeeklyWaterAmount(); name=:weekly, on=One(scale=:Plant),
inputs=(amounts_mg=Many(
scale=:Leaf, within=Subtree(), application=:hourly,
var=:water_rate_mg_s, policy=integrate_rate, window=Week(1),
),),
every=Week(1),
),
),
environment=[(duration=Hour(1),) for _ in 1:169],
)
simulation = run!(model; steps=169, outputs=:all)
rows = collect_outputs(simulation; sink=DataFrame)
totals = rows[in.(rows.application_id, Ref((:daily, :weekly))),
[:timestep, :application_id, :variable, :value]]
totals| Row | timestep | application_id | variable | value |
|---|---|---|---|---|
| Int64 | Symbol | Symbol | Float64 | |
| 1 | 1 | daily | water_amount_mg | 10800.0 |
| 2 | 1 | weekly | weekly_water_mg | 10800.0 |
| 3 | 25 | daily | water_amount_mg | 259200.0 |
| 4 | 49 | daily | water_amount_mg | 259200.0 |
| 5 | 73 | daily | water_amount_mg | 259200.0 |
| 6 | 97 | daily | water_amount_mg | 259200.0 |
| 7 | 121 | daily | water_amount_mg | 259200.0 |
| 8 | 145 | daily | water_amount_mg | 259200.0 |
| 9 | 169 | daily | water_amount_mg | 259200.0 |
| 10 | 169 | weekly | weekly_water_mg | 1.8144e6 |
Both totals are calculated at step 1, when only one hour of values is available: (1 + 2) × 3600 = 10800 mg. This first result does not represent a complete day or week. The daily total is calculated again at steps 25, 49, and so on; the weekly total is calculated again at step 169.
first_daily = only(totals[(totals.application_id .== :daily) .& (totals.timestep .== 1), :value])
full_day = only(totals[(totals.application_id .== :daily) .& (totals.timestep .== 25), :value])
full_week = only(totals[(totals.application_id .== :weekly) .& (totals.timestep .== 169), :value])
@assert first_daily == 3 * 3600
@assert full_day == 3 * 24 * 3600
@assert full_week == 3 * 7 * 24 * 3600
(startup_mg=first_daily, daily_mg=full_day, weekly_mg=full_week)Each window looks back over its specified duration. It does not automatically start at midnight or at the beginning of a calendar week. Each model's cadence must be a whole number of base steps; choose a smaller common step if needed. Calendar months and timesteps that change during a run are not supported.
Integrate() without a function only adds values. Use it when the values already represent amounts, and check that the selected window includes each intended amount once. For rates, multiply by duration as above. Use Aggregate(reducer) to calculate a mean or another statistic, supplying your calculation as the reducer function.
These teaching models do not declare VariableContracts, which describe a variable's units and physical meaning. The rules for combining values over time do not change these declarations. If your models declare a rate contract on one side and an amount contract on the other, connect them through a small model that performs the conversion and declares both meanings. See Coupling models.
PlantSimEngine keeps the earlier values needed for each input window. Choose separately which results to save for your own analysis: see Collecting And Plotting Outputs. Check the schedule to find how many daily or weekly results to expect, including the first partial result.
These simple equations are shared with the cadence tutorial. They copy each leaf's supplied rate and add the converted leaf amounts. The weekly model uses a different output name so the plant can store both totals.
module TeachingTimeModels
using PlantSimEngine
export HourlyWaterRate, SumWaterAmounts, WeeklyWaterAmount
PlantSimEngine.@process "teaching_water_rate" verbose=false
PlantSimEngine.@process "teaching_water_amount" verbose=false
PlantSimEngine.@process "teaching_weekly_water_amount" verbose=false
"""Publish a supplied constant uptake rate, in mg of water per leaf per second."""
struct HourlyWaterRate <: AbstractTeaching_Water_RateModel end
PlantSimEngine.inputs_(::HourlyWaterRate) = (rate_mg_s=Required(Real),)
PlantSimEngine.outputs_(::HourlyWaterRate) = (water_rate_mg_s=0.0,)
function PlantSimEngine.run!(::HourlyWaterRate, status, environment, constants, context)
status.water_rate_mg_s = status.rate_mg_s
return nothing
end
"""Add already converted water amounts, in mg, from the selected sources."""
struct SumWaterAmounts <: AbstractTeaching_Water_AmountModel end
PlantSimEngine.inputs_(::SumWaterAmounts) = (amounts_mg=Required(AbstractVector{<:Real}),)
PlantSimEngine.outputs_(::SumWaterAmounts) = (water_amount_mg=0.0,)
function PlantSimEngine.run!(::SumWaterAmounts, status, environment, constants, context)
status.water_amount_mg = sum(status.amounts_mg)
return nothing
end
"""Keep a separately named weekly total, in mg of water per plant."""
struct WeeklyWaterAmount <: AbstractTeaching_Weekly_Water_AmountModel end
PlantSimEngine.inputs_(::WeeklyWaterAmount) = (amounts_mg=Required(AbstractVector{<:Real}),)
PlantSimEngine.outputs_(::WeeklyWaterAmount) = (weekly_water_mg=0.0,)
function PlantSimEngine.run!(::WeeklyWaterAmount, status, environment, constants, context)
status.weekly_water_mg = sum(status.amounts_mg)
return nothing
end
# These constant-rate teaching models isolate temporal sampling. Their variable
# contracts are deliberately undeclared: the pages explain the explicit adapter
# required when a contracted rate is converted to a contracted amount.
end # module