PlantSimEngine
Overview
PlantSimEngine
is a comprehensive framework for building models of the soil-plant-atmosphere continuum. It includes everything you need to prototype, evaluate, test, and deploy plant/crop models at any scale, with a strong emphasis on performance and efficiency, so you can focus on building and refining your models.
Why choose PlantSimEngine?
- Simplicity: Write less code, focus on your model's logic, and let the framework handle the rest.
- Modularity: Each model component can be developed, tested, and improved independently. Assemble complex simulations by reusing pre-built, high-quality modules.
- Standardisation: Clear, enforceable guidelines ensure that all models adhere to best practices. This built-in consistency means that once you implement a model, it works seamlessly with others in the ecosystem.
- Optimised Performance: Don't re-invent the wheel. Delegating low-level tasks to PlantSimEngine guarantees that your model will benefit from every improvement in the framework. Enjoy faster prototyping, robust simulations, and efficient execution using Julia's high-performance capabilities.
Unique Features
Automatic Model Coupling
Seamless Integration: PlantSimEngine leverages Julia's multiple-dispatch capabilities to automatically compute the dependency graph between models. This allows researchers to effortlessly couple models without writing complex connection code or manually managing dependencies.
Intuitive Multi-Scale Support: The framework naturally handles models operating at different scales—from organelle to ecosystem—connecting them with minimal effort and maintaining consistency across scales.
Flexibility with Precision Control
Effortless Model Switching: Researchers can switch between different component models using a simple syntax without rewriting the underlying model code. This enables rapid comparison between different hypotheses and model versions, accelerating the scientific discovery process.
Batteries included
- Automated Management: Seamlessly handle inputs, outputs, time-steps, objects, and dependency resolution.
- Iterative Development: Fast and interactive prototyping of models with built-in constraints to avoid errors and sensible defaults to streamline the model writing process.
- Control Your Degrees of Freedom: Fix variables to constant values or force to observations, use simpler models for specific processes to reduce complexity.
- High-Speed Computations: Achieve impressive performance with benchmarks showing operations in the 100th of nanoseconds range for complex models (see this benchmark script).
- Parallelize and Distribute Computing: Out-of-the-box support for sequential, multi-threaded, or distributed computations over objects, time-steps, and independent processes, thanks to Floops.jl.
- Scale Effortlessly: Methods for computing over objects, time-steps, and Multi-Scale Tree Graphs.
- Compose Freely: Use any types as inputs, including Unitful for unit propagation and MonteCarloMeasurements.jl for measurement error propagation.
Performance
PlantSimEngine delivers impressive performance for plant modeling tasks. On an M1 MacBook Pro:
- A toy model for leaf area over a year at daily time-scale took only 260 μs (about 688 ns per day)
- The same model coupled to a light interception model took 275 μs (756 ns per day)
These benchmarks demonstrate performance on par with compiled languages like Fortran or C, far outpacing typical interpreted language implementations. For example, PlantBiophysics.jl, which implements ecophysiological models using PlantSimEngine, has been measured to run up to 38,000 times faster than equivalent implementations in other scientific computing languages.
Ask Questions
If you have any questions or feedback, open an issue or ask on discourse.
Installation
To install the package, enter the Julia package manager mode by pressing ]
in the REPL, and execute the following command:
add PlantSimEngine
To use the package, execute this command from the Julia REPL:
using PlantSimEngine
Example usage
The package is designed to be easy to use, and to help users avoid errors when implementing, coupling and simulating models.
Simple example
Here's a simple example of a model that simulates the growth of a plant, using a simple exponential growth model:
# ] add PlantSimEngine
using PlantSimEngine
# Import the examples defined in the `Examples` sub-module
using PlantSimEngine.Examples
# Define the model:
model = ModelList(
ToyLAIModel(),
status=(TT_cu=1.0:2000.0,), # Pass the cumulated degree-days as input to the model
)
out = run!(model) # run the model and extract its outputs
TimeStepTable{Status{(:TT_cu, :LAI), Tuple{...}(2000 x 2):
╭─────┬─────────┬────────────╮
│ Row │ TT_cu │ LAI │
│ │ Float64 │ Float64 │
├─────┼─────────┼────────────┤
│ 1 │ 1.0 │ 0.00560052 │
│ 2 │ 2.0 │ 0.00565163 │
│ 3 │ 3.0 │ 0.00570321 │
│ 4 │ 4.0 │ 0.00575526 │
│ 5 │ 5.0 │ 0.00580778 │
│ 6 │ 6.0 │ 0.00586078 │
│ 7 │ 7.0 │ 0.00591426 │
│ 8 │ 8.0 │ 0.00596823 │
│ 9 │ 9.0 │ 0.00602269 │
│ 10 │ 10.0 │ 0.00607765 │
│ 11 │ 11.0 │ 0.00613311 │
│ 12 │ 12.0 │ 0.00618908 │
│ 13 │ 13.0 │ 0.00624556 │
│ 14 │ 14.0 │ 0.00630255 │
│ 15 │ 15.0 │ 0.00636006 │
│ ⋮ │ ⋮ │ ⋮ │
╰─────┴─────────┴────────────╯
1985 rows omitted
Note The
ToyLAIModel
is available from the examples folder, and is a simple exponential growth model. It is used here for the sake of simplicity, but you can use any model you want, as long as it followsPlantSimEngine
interface.
Of course you can plot the outputs quite easily:
# ] add CairoMakie
using CairoMakie
lines(out[:TT_cu], out[:LAI], color=:green, axis=(ylabel="LAI (m² m⁻²)", xlabel="Cumulated growing degree days since sowing (°C)"))

Model coupling
Model coupling is done automatically by the package, and is based on the dependency graph between the models. To couple models, we just have to add them to the ModelList
. For example, let's couple the ToyLAIModel
with a model for light interception based on Beer's law:
# ] add PlantSimEngine, DataFrames, CSV
using PlantSimEngine, PlantMeteo, DataFrames, CSV
# Import the examples defined in the `Examples` sub-module
using PlantSimEngine.Examples
# Import the example meteorological data:
meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18)
# Define the list of models for coupling:
model2 = ModelList(
ToyLAIModel(),
Beer(0.6),
status=(TT_cu=cumsum(meteo_day[:, :TT]),), # Pass the cumulated degree-days as input to `ToyLAIModel`, this could also be done using another model
)
# Run the simulation:
out2 = run!(model2, meteo_day)
TimeStepTable{Status{(:TT_cu, :LAI, :aPPFD)...}(365 x 3):
╭─────┬──────────┬────────────┬───────────╮
│ Row │ TT_cu │ LAI │ aPPFD │
│ │ Float64 │ Float64 │ Float64 │
├─────┼──────────┼────────────┼───────────┤
│ 1 │ 0.0 │ 0.00554988 │ 0.0476221 │
│ 2 │ 0.0 │ 0.00554988 │ 0.0260688 │
│ 3 │ 0.0 │ 0.00554988 │ 0.0377774 │
│ 4 │ 0.0 │ 0.00554988 │ 0.0468871 │
│ 5 │ 0.0 │ 0.00554988 │ 0.0545266 │
│ 6 │ 0.0 │ 0.00554988 │ 0.0567055 │
│ 7 │ 0.0 │ 0.00554988 │ 0.0521376 │
│ 8 │ 0.0 │ 0.00554988 │ 0.0563642 │
│ 9 │ 0.0 │ 0.00554988 │ 0.0349947 │
│ 10 │ 0.0 │ 0.00554988 │ 0.0168016 │
│ 11 │ 0.0 │ 0.00554988 │ 0.0606171 │
│ 12 │ 0.0 │ 0.00554988 │ 0.0486197 │
│ 13 │ 0.5625 │ 0.00557831 │ 0.0357278 │
│ 14 │ 0.945833 │ 0.00559777 │ 0.0519777 │
│ 15 │ 0.979167 │ 0.00559946 │ 0.0564167 │
│ ⋮ │ ⋮ │ ⋮ │ ⋮ │
╰─────┴──────────┴────────────┴───────────╯
350 rows omitted
The ModelList
couples the models by automatically computing the dependency graph of the models. The resulting dependency graph is:
╭──── Dependency graph ──────────────────────────────────────────╮
│ ╭──── LAI_Dynamic ─────────────────────────────────────────╮ │
│ │ ╭──── Main model ────────╮ │ │
│ │ │ Process: LAI_Dynamic │ │ │
│ │ │ Model: ToyLAIModel │ │ │
│ │ │ Dep: nothing │ │ │
│ │ ╰────────────────────────╯ │ │
│ │ │ ╭──── Soft-coupled model ─────────╮ │ │
│ │ │ │ Process: light_interception │ │ │
│ │ └──│ Model: Beer │ │ │
│ │ │ Dep: (LAI_Dynamic = (:LAI,),) │ │ │
│ │ ╰─────────────────────────────────╯ │ │
│ ╰──────────────────────────────────────────────────────────╯ │
╰────────────────────────────────────────────────────────────────╯
We can plot the results by indexing the outputs with the variable name (e.g. out2[:LAI]
):
using CairoMakie
fig = Figure(resolution=(800, 600))
ax = Axis(fig[1, 1], ylabel="LAI (m² m⁻²)")
lines!(ax, out2[:TT_cu], out2[:LAI], color=:mediumseagreen)
ax2 = Axis(fig[2, 1], xlabel="Cumulated growing degree days since sowing (°C)", ylabel="aPPFD (mol m⁻² d⁻¹)")
lines!(ax2, out2[:TT_cu], out2[:aPPFD], color=:firebrick1)
fig

Multi-scale modeling
See the Multi-scale modeling section for more details.
The package is designed to be easily scalable, and can be used to simulate models at different scales. For example, you can simulate a model at the leaf scale, and then couple it with models at any other scale, e.g. internode, plant, soil, scene scales. Here's an example of a simple model that simulates plant growth using sub-models operating at different scales:
mapping = Dict(
"Scene" => ToyDegreeDaysCumulModel(),
"Plant" => (
MultiScaleModel(
model=ToyLAIModel(),
mapped_variables=[
:TT_cu => "Scene",
],
),
Beer(0.6),
MultiScaleModel(
model=ToyAssimModel(),
mapped_variables=[:soil_water_content => "Soil"],
),
MultiScaleModel(
model=ToyCAllocationModel(),
mapped_variables=[
:carbon_demand => ["Leaf", "Internode"],
:carbon_allocation => ["Leaf", "Internode"]
],
),
MultiScaleModel(
model=ToyPlantRmModel(),
mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],],
),
),
"Internode" => (
MultiScaleModel(
model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0),
mapped_variables=[:TT => "Scene",],
),
MultiScaleModel(
model=ToyInternodeEmergence(TT_emergence=20.0),
mapped_variables=[:TT_cu => "Scene"],
),
ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004),
Status(carbon_biomass=1.0)
),
"Leaf" => (
MultiScaleModel(
model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0),
mapped_variables=[:TT => "Scene",],
),
ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025),
Status(carbon_biomass=1.0)
),
"Soil" => (
ToySoilWaterModel(),
),
);
We can import an example plant from the package:
mtg = import_mtg_example()
/ 1: Scene
├─ / 2: Soil
└─ + 3: Plant
└─ / 4: Internode
├─ + 5: Leaf
└─ < 6: Internode
└─ + 7: Leaf
Make a fake meteorological data:
meteo = Weather(
[
Atmosphere(T=20.0, Wind=1.0, Rh=0.65, Ri_PAR_f=300.0),
Atmosphere(T=25.0, Wind=0.5, Rh=0.8, Ri_PAR_f=500.0)
]
);
And run the simulation:
out_vars = Dict(
"Scene" => (:TT_cu,),
"Plant" => (:carbon_allocation, :carbon_assimilation, :soil_water_content, :aPPFD, :TT_cu, :LAI),
"Leaf" => (:carbon_demand, :carbon_allocation),
"Internode" => (:carbon_demand, :carbon_allocation),
"Soil" => (:soil_water_content,),
)
out = run!(mtg, mapping, meteo, tracked_outputs=out_vars, executor=SequentialEx());
We can then extract the outputs in a DataFrame
and sort them:
using DataFrames
df_out = convert_outputs(out, DataFrame)
sort!(df_out, [:timestep, :node])
Row | timestep | organ | node | carbon_allocation | TT_cu | carbon_assimilation | aPPFD | LAI | soil_water_content | carbon_demand |
---|---|---|---|---|---|---|---|---|---|---|
Int64 | String | Int64 | Union… | Union… | Union… | Union… | Union… | Union… | Union… | |
1 | 1 | Scene | 1 | 10.0 | ||||||
2 | 1 | Soil | 2 | 0.5 | ||||||
3 | 1 | Plant | 3 | 10.0 | 0.499037 | 4.99037 | 0.00607765 | 0.5 | ||
4 | 1 | Internode | 4 | 0.124183 | 0.5 | |||||
5 | 1 | Leaf | 5 | 0.124183 | 0.5 | |||||
6 | 1 | Internode | 6 | 0.124183 | 0.5 | |||||
7 | 1 | Leaf | 7 | 0.124183 | 0.5 | |||||
8 | 2 | Scene | 1 | 25.0 | ||||||
9 | 2 | Soil | 2 | 0.5 | ||||||
10 | 2 | Plant | 3 | 25.0 | 0.952884 | 9.52884 | 0.00696482 | 0.5 | ||
11 | 2 | Internode | 4 | 0.157992 | 0.75 | |||||
12 | 2 | Leaf | 5 | 0.157992 | 0.75 | |||||
13 | 2 | Internode | 6 | 0.157992 | 0.75 | |||||
14 | 2 | Leaf | 7 | 0.157992 | 0.75 | |||||
15 | 2 | Internode | 8 | 0.157992 | 0.75 | |||||
16 | 2 | Leaf | 9 | 0.157992 | 0.75 |
An example output of a multiscale simulation is shown in the documentation of PlantBiophysics.jl:
State of the field
PlantSimEngine is a state-of-the-art plant simulation software that offers significant advantages over existing tools such as OpenAlea, STICS, APSIM, or DSSAT.
The use of Julia programming language in PlantSimEngine allows for:
- Quick and easy prototyping compared to compiled languages
- Significantly better performance than typical interpreted languages
- No need for translation into another compiled language
Julia's features enable PlantSimEngine to provide:
- Multiple-dispatch for automatic computation of model dependency graphs
- Type stability for optimized performance
- Seamless compatibility with powerful tools like MultiScaleTreeGraph.jl for multi-scale computations
PlantSimEngine's approach streamlines the process of model development by automatically managing:
- Model coupling with automated dependency graph computation
- Time-steps and parallelization
- Input and output variables
- Various types of objects used for simulations (vectors, dictionaries, multi-scale tree graphs)
Projects that use PlantSimEngine
Take a look at these projects that use PlantSimEngine:
Make it yours
The package is developed so anyone can easily implement plant/crop models, use it freely and as you want thanks to its MIT license.
If you develop such tools and it is not on the list yet, please make a PR or contact me so we can add it! 😃