PlantSimEngine.jlPlantSimEngine.jl

Use Observed Values In A Simulation​#

You can supply measured leaf area index (LAI) while calculating light interception. This is useful for testing the light model independently of a model that predicts LAI. Here LAI is m² leaf per m² ground, and incident PAR is an energy flux in W m⁻² ground.

One fixed observation​#

Supply a constant LAI in status, where the canopy stores its values, and use only the light model:

julia
using PlantSimEngine, Dates, DataFrames
using PlantSimEngine.Examples

fixed_model = CompositeModel(
    Beer(0.6);
    status=(LAI=2.0,), id=:canopy, scale=:Canopy,
    environment=(Ri_PAR_f=100.0, duration=Day(1)),
)
fixed_simulation = run!(fixed_model; outputs=:all)
final_state(fixed_simulation).aPPFD
319.35424515612357

If you add a model that calculates LAI, it will replace this supplied value when it runs. To keep the observed LAI fixed, use it without an LAI model.

A sequence of observations​#

For observations that change over time, use a model that reads each observation and supplies it as LAI. The small ObservedLAI model below only copies the value; it performs no fitting, interpolation, or unit conversion. Include it from the downloadable source to run the example:

julia
include("observed_lai.jl")

observations = DataFrame(
    duration=fill(Day(1), 3),
    measured_LAI=[1.0, 2.0, 3.0],
    Ri_PAR_f=fill(100.0, 3),
)
model = CompositeModel(
    ObservedLAI(), Beer(0.6);
    id=:canopy, scale=:Canopy, environment=observations,
)
simulation = run!(model; steps=nrow(observations), outputs=:all)
rows = collect_outputs(simulation; sink=DataFrame)
filter(row -> row.variable == :LAI || row.variable == :aPPFD, rows)
6×6 DataFrame
Rowtimesteptimeapplication_idobject_idvariablevalue
Int64Float64SymbolSymbolSymbolFloat64
111.0LAI_DynamiccanopyLAI1.0
211.0light_interceptioncanopyaPPFD206.193
322.0LAI_DynamiccanopyLAI2.0
422.0light_interceptioncanopyaPPFD319.354
533.0LAI_DynamiccanopyLAI3.0
633.0light_interceptioncanopyaPPFD381.458

PlantSimEngine passes LAI from ObservedLAI to Beer automatically: one model supplies it and the other needs it on the same canopy. To predict LAI instead, replace ObservedLAI() with ToyLAIModel() and supply the thermal time that model requires. A change of model can change the inputs you must provide; compare alternatives before making a replacement.

The observation model​#

The complete source file is short:

julia
struct ObservedLAI <: PlantSimEngine.Examples.AbstractLai_DynamicModel end

PlantSimEngine.inputs_(::ObservedLAI) = NamedTuple()
PlantSimEngine.outputs_(::ObservedLAI) = (LAI=0.0,)
PlantSimEngine.environment_inputs_(::ObservedLAI) = (measured_LAI=0.0,)

function PlantSimEngine.run!(::ObservedLAI, status, environment, constants, context)
    status.LAI = environment.measured_LAI
    return nothing
end

Before using real measurements, check their units, whether they refer to leaf or ground area, their time stamps, and any missing values. This example has one observation per day. If your measurements are less frequent, decide how to fill the gaps: for example, interpolate between measurements or keep the last measured value until the next one. Document that choice before using the data. This approach supplies a measured input directly. It does not perform data assimilation, which would use observations to estimate or correct the model state and could account for measurement uncertainty.

See Collect and plot results for displaying predictions alongside observations and Parameter fitting when the objective is to estimate model parameters.