PlantSimEngine is a Julia framework for composing soil-plant-atmosphere simulations from reusable process models.
A modeler writes generic kernels with:
inputs_declarations usingRequired(T)orDefault(value)outputs_- optional
dep,timespec,output_policy,environment_inputs_,environment_outputs_, andvariable_contracts_traits run!(model, status, environment, constants, context)
A simulation author assembles those kernels on objects with CompositeModel,
Object, and one direct application constructor:
ModelSpec(
model;
name=:application,
on=Many(scale=:Leaf),
inputs=(...),
calls=(...),
every=Hour(1),
environment=Environment(...),
output_routing=(...),
updates=Updates(...),
)This is the package API for multiscale, multi-plant, soil, microclimate, and model-scale simulations.
The examples on this development branch use CompositeModel. Registered
releases through 0.14.1 use the previous mapping API; use their matching
stable documentation.
To run the examples below, install the development version in a project:
using Pkg
Pkg.activate("my_simulation")
Pkg.add(["PlantMeteo", "DataFrames"])
Pkg.add(url="https://github.com/VirtualPlantLab/PlantSimEngine.jl", rev="main")
using PlantSimEngineWhen following a pull-request preview, use its branch or commit instead of
"main". Keep Project.toml and Manifest.toml with your experiment to record
the package revisions. The installation guide
also installs the plotting tools used by the tutorials.
This example runs three existing toy models on one model object:
ToyDegreeDaysCumulModelcomputes daily thermal time.ToyLAIModelconsumes cumulative thermal time and computes LAI.Beerconsumes LAI and meteorology to compute absorbed PAR.
Run the full weather year to see canopy growth and senescence. The bundled
file contains daily radiation totals in MJ m⁻² d⁻¹ under historical _f
column names; convert them to mean fluxes in W m⁻² for Beer when reading.
using PlantSimEngine, PlantMeteo, Dates, DataFrames
using PlantSimEngine.Examples
meteo_day = read_weather(
joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"),
:Ri_SW_f => (x -> x .* 1e6 ./ 86_400) => :Ri_SW_f,
:Ri_PAR_f => (x -> x .* 1e6 ./ 86_400) => :Ri_PAR_f,
:Ri_NIR_f => (x -> x .* 1e6 ./ 86_400) => :Ri_NIR_f;
duration=Dates.Day,
)
model = CompositeModel(
ToyDegreeDaysCumulModel(),
ToyLAIModel(),
Beer(0.6);
environment=meteo_day,
)
sim = run!(model; steps=length(meteo_day), outputs=:all)
out = collect_outputs(sim; sink=DataFrame)
first(out, 6)The compiler infers the unambiguous same-object bindings from each model's
declared inputs and outputs: ToyLAIModel receives TT_cu from
:Degreedays, and Beer receives LAI from :LAI_Dynamic.
select(
DataFrame(Diagnostics.explain_bindings(model)),
:application_id,
:input,
:source_application_ids,
:carrier_kind,
:copy_semantics,
)Use the inputs keyword when a model needs values from selected objects. This
model-scale LAI application reads live references to the surface of every
plant in the model:
plant_scene = CompositeModel(
Object(:scene; scale=:Scene, kind=:scene),
Object(:plant_1; scale=:Plant, kind=:plant, parent=:scene,
status=Status(surface=12.0)),
Object(:plant_2; scale=:Plant, kind=:plant, parent=:scene,
status=Status(surface=8.0));
applications=(
ModelSpec(
ToyLAIfromLeafAreaModel(100.0);
name=:scene_lai,
on=One(scale=:Scene),
inputs=(
:plant_surfaces => Many(
scale=:Plant,
within=SceneScope(),
var=:surface,
),
),
),
),
)
run!(plant_scene)
scene_status = only(model_objects(plant_scene; scale=:Scene)).status
(total_surface=scene_status.total_surface, LAI=scene_status.LAI)Use within=Subtree() for plant-local descendant aggregations, for example a
plant allocation model summing only the leaves below the current plant.
Self() selects only the object where the consumer runs. Use
within=SceneScope() for model-wide aggregation.
Use the calls keyword when a parent model must directly run selected child models,
for example a model energy-balance solver that iterates leaf temperatures:
ModelSpec(
SceneEnergyBalance();
name=:scene_energy,
on=One(scale=:Scene),
calls=(
:leaf_energy => Many(
kind=:plant,
scale=:Leaf,
within=SceneScope(),
application=:energy_balance,
),
:soil => One(
kind=:soil,
scale=:Soil,
within=SceneScope(),
application=:soil_water,
),
),
every=Hour(1),
)Scenario-level inputs and calls should usually name the concrete producer
or callee with application=.... Use process identities in model-level
contracts such as dep(model), where the model author cannot know application
names chosen by future scenarios.
Inside the parent model, run_call!(context, :leaf_energy) executes every target
and returns a vector-like collection. For iterative control,
call_targets(context, :leaf_energy) returns the collection without executing
it. run_call!(target; publish=false) is the default for trial iterations, and
run_call!(target; publish=true) publishes the accepted state.
- object graphs with arbitrary plant architecture;
- several plant species and repeated plant instances through templates;
- same-rate reference wiring and typed many-object carriers;
- multirate scheduling with
Dates.Periodvalues; - temporal policies such as
HoldLast,Interpolate,Integrate, andAggregate; - automatic global or spatial environment binding;
- mutable microclimate outputs through
environment_outputs_; - growth, pruning, reparenting, and movement with binding-cache refresh;
- structured explanations for users and agents.
Useful inspection helpers include:
Authoring.describe_model(model_instance)
Authoring.validate_model(model_instance; strict=true)
Authoring.validate_scenario(model)
Diagnostics.explain_objects(model)
Diagnostics.explain_scopes(model)
Diagnostics.explain_bindings(model)
Diagnostics.explain_calls(model)
Diagnostics.explain_environment_bindings(model)
Diagnostics.explain_schedule(model)
Diagnostics.explain_execution_plan(model)- Stable documentation
- Development documentation
- Run and couple models
- Implement a model
- AI agent skill
- CompositeModel/object migration guide
- Public API reference
- PlantBiophysics.jl for plant biophysical processes such as photosynthesis, conductance, energy fluxes, and temperature.
- XPalm, an experimental crop model for oil palm.
PlantSimEngine keeps model kernels close to regular Julia functions while the runtime handles dependency scheduling, object selection, temporal aggregation, and environment sampling. On an M1 MacBook Pro, toy daily simulations run in hundreds of microseconds, and PlantBiophysics.jl models using PlantSimEngine have been measured much faster than equivalent implementations in typical scientific scripting languages.
For performance-sensitive composite models, inspect the compiled representation with
Diagnostics.explain_execution_plan(model) to see homogeneous batches and
concrete carrier types.
PlantSimEngine is distributed under the MIT license. Questions and bug reports are welcome on GitHub issues or the FSPM discourse.