Skip to content

Repository files navigation

PlantSimEngine

Stable Dev Build Status Coverage ColPrac: Contributor's Guide on Collaborative Practices for Community Packages Aqua QA DOI JOSS

PlantSimEngine is a Julia framework for composing soil-plant-atmosphere simulations from reusable process models.

A modeler writes generic kernels with:

  • inputs_ declarations using Required(T) or Default(value)
  • outputs_
  • optional dep, timespec, output_policy, environment_inputs_, environment_outputs_, and variable_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.

Installation

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 PlantSimEngine

When 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.

Quickstart

This example runs three existing toy models on one model object:

  1. ToyDegreeDaysCumulModel computes daily thermal time.
  2. ToyLAIModel consumes cumulative thermal time and computes LAI.
  3. Beer consumes 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,
)

Multi-Object Coupling

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.

Manual Calls

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.

What PlantSimEngine Handles

  • 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.Period values;
  • temporal policies such as HoldLast, Interpolate, Integrate, and Aggregate;
  • 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)

Documentation

Projects That Use PlantSimEngine

  • PlantBiophysics.jl for plant biophysical processes such as photosynthesis, conductance, energy fluxes, and temperature.
  • XPalm, an experimental crop model for oil palm.

Performance

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.

License And Contributions

PlantSimEngine is distributed under the MIT license. Questions and bug reports are welcome on GitHub issues or the FSPM discourse.

About

A framework to build plant models at the scales that matter

Topics

Resources

Contributing

Stars

24 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages