Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions AI_PACKAGE_SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This document explains how PlantSimEngine is structured internally, how models a
PlantSimEngine is a Julia framework for composing plant models as modular processes. Users or modelers define models that implement a process, declare inputs/outputs, and optionally declare hard dependencies (manual calls). The engine builds a dependency graph (soft dependencies via inputs/outputs and hard dependencies via explicit model calls) and executes models in dependency order. It supports single-scale model lists and multiscale model mappings on a plant graph (MTG).

Core modules (see `src/PlantSimEngine.jl`):
- `component_models`: `Status`, `RefVector`, `ModelList`, `TimeStepTable`
- `component_models`: `Status`, `RefVector`, `ModelMapping`, `TimeStepTable`
- `dependencies`: dependency graph types and builders
- `processes`: model interfaces, inputs/outputs/variables, process macro
- `mtg`: multiscale mapping, GraphSimulation, initialization, save results
Expand All @@ -29,7 +29,7 @@ File: `src/component_models/RefVector.jl`
- Used for multiscale aggregation where a higher scale references values from many lower-scale nodes (e.g., plant-level model reads all leaves).
- Updating a `RefVector` entry updates the referenced Status field.

### ModelList
### ModelList (deprecated)
File: `src/component_models/ModelList.jl`
- `ModelList` is the single-scale container: `models::NamedTuple`, `status::Status`, `dependency_graph::DependencyGraph`.
- Building a ModelList:
Expand All @@ -39,6 +39,7 @@ File: `src/component_models/ModelList.jl`
- `type_promotion` can upcast default model values (not user-specified ones).

### MultiScaleModel

File: `src/mtg/MultiScaleModel.jl`
- Wrapper to attach a multiscale variable mapping to a model.
- Supports scalar mapping (SingleNode), vector mapping (MultiNode), renaming, and `PreviousTimeStep`.
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ using PlantSimEngine
using PlantSimEngine.Examples

# Define the model:
model = ModelList(
model = ModelMapping(
ToyLAIModel(),
status=(TT_cu=1.0:2000.0,), # Pass the cumulated degree-days as input to the model
)
Expand Down Expand Up @@ -136,7 +136,7 @@ lines(model[:TT_cu], model[:LAI], color=:green, axis=(ylabel="LAI (m² m⁻²)",

### 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:
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 `ModelMapping`. For example, let's couple the `ToyLAIModel` with a model for light interception based on Beer's law:

```julia
# ] add PlantSimEngine, DataFrames, CSV
Expand All @@ -149,14 +149,14 @@ using PlantSimEngine.Examples
meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18)

# Define the list of models for coupling:
model = ModelList(
model = ModelMapping(
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
)
```

The `ModelList` couples the models by automatically computing the dependency graph of the models. The resulting dependency graph is:
The `ModelMapping` couples the models by automatically computing the dependency graph of the models. The resulting dependency graph is:

```
╭──── Dependency graph ──────────────────────────────────────────╮
Expand Down Expand Up @@ -223,7 +223,7 @@ fig
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:

```julia
mapping = Dict(
mapping = ModelMapping(
"Scene" => ToyDegreeDaysCumulModel(),
"Plant" => (
MultiScaleModel(
Expand Down Expand Up @@ -295,7 +295,7 @@ meteo = Weather(
And run the simulation:

```julia
out_vars = Dict(
out_vars = ModelMapping(
"Scene" => (:TT_cu,),
"Plant" => (:carbon_allocation, :carbon_assimilation, :soil_water_content, :aPPFD, :TT_cu, :LAI),
"Leaf" => (:carbon_demand, :carbon_allocation),
Expand Down
20 changes: 10 additions & 10 deletions benchmark/test-PSE-benchmark.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@ function PlantSimEngine.run!(m::ToyInternodeCrazyEmergence, models, status, mete
#end

if length(MultiScaleTreeGraph.children(status.node)) == 1 && status.TT_cu - status.TT_cu_emergence >= m.TT_emergence

status_new_internode = add_organ!(status.node, sim_object, "<", "Internode", 2, index=1)
add_organ!(status_new_internode.node, sim_object, "+", "Leaf", 2, index=1)
status_new_internode.TT_cu_emergence = status.TT_cu
elseif (length(MultiScaleTreeGraph.children(status.node)) >= 2 && length(MultiScaleTreeGraph.children(status.node)) < 7) && status.TT_cu - status.TT_cu_emergence >= m.TT_emergence
elseif (length(MultiScaleTreeGraph.children(status.node)) >= 2 && length(MultiScaleTreeGraph.children(status.node)) < 7) && status.TT_cu - status.TT_cu_emergence >= m.TT_emergence
status_new_internode = add_organ!(status.node, sim_object, "<", "Internode", 2, index=1)
add_organ!(status.node, sim_object, "+", "Leaf", 2, index=4)
add_organ!(status.node, sim_object, "+", "Leaf", 2, index=5)
status_new_internode.TT_cu_emergence = status.TT_cu
elseif (length(MultiScaleTreeGraph.children(status.node)) >= 7 && length(MultiScaleTreeGraph.children(status.node)) < 30) && status.TT_cu - status.TT_cu_emergence >= m.TT_emergence
elseif (length(MultiScaleTreeGraph.children(status.node)) >= 7 && length(MultiScaleTreeGraph.children(status.node)) < 30) && status.TT_cu - status.TT_cu_emergence >= m.TT_emergence
add_organ!(status.node, sim_object, "+", "Leaf", 2, index=6)
add_organ!(status.node, sim_object, "+", "Leaf", 2, index=7)
add_organ!(status.node, sim_object, "+", "Leaf", 2, index=8)
Expand All @@ -53,13 +53,13 @@ end
# Wrapped this into a function so that it doesn't plague the benchmark with variables on a global scope
#@check_allocs
function do_benchmark_on_heavier_mtg()
mtg = import_mtg_example();
mtg = import_mtg_example()

# Example meteo, 365 timesteps :
meteo_day = read_weather(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), duration=Day)

#similar to the mtg growth test but with a much lower emergence threshold
mapping = Dict(
mapping = ModelMapping(
"Scene" => ToyDegreeDaysCumulModel(),
"Plant" => (
MultiScaleModel(
Expand Down Expand Up @@ -110,13 +110,13 @@ function do_benchmark_on_heavier_mtg()
ToySoilWaterModel(),
),
)

out_vars = Dict(
"Leaf" => (:carbon_assimilation, :carbon_demand, :soil_water_content, :carbon_allocation),
"Internode" => (:carbon_allocation, :TT_cu_emergence),
"Plant" => (:carbon_allocation,),
"Soil" => (:soil_water_content,),
)
out = run!(mtg, mapping, meteo_day, tracked_outputs=out_vars, executor=SequentialEx());

out = run!(mtg, mapping, meteo_day, tracked_outputs=out_vars, executor=SequentialEx())
end
2 changes: 1 addition & 1 deletion benchmark/test-multirate-buffer-benchmark.jl
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ end
function setup_multirate_buffer_benchmark(; nleaves=2000, ndays=30)
mtg = _build_multirate_benchmark_mtg(nleaves)

mapping = Dict(
mapping = ModelMapping(
"Leaf" => (
ModelSpec(MRBenchSourceModel(Ref(0))) |> TimeStepModel(1.0),
),
Expand Down
6 changes: 3 additions & 3 deletions benchmark/test-plantbiophysics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ function benchmark_plantbiophysics()
constants = Constants()
#time_PB = Vector{Float64}(undef, N*microbenchmark_steps)
for i = 1:N
leaf = ModelList(
leaf = ModelMapping(
energy_balance=Monteith(),
photosynthesis=Fvcb(
VcMaxRef=set.VcMaxRef[i],
Expand Down Expand Up @@ -138,9 +138,9 @@ function setup_benchmark_plantbiophysics_multitimestep()
@. set[!, :vpd] = e_sat(set.T) - vapor_pressure(set.T, set.Rh)
@. set[!, :aPPFD] = set.Ra_SW_f * 0.48 * 4.57

leaf = Vector{ModelList}(undef, N)
leaf = Vector{ModelMapping}(undef, N)
for i = 1:N
leaf[i] = ModelList(
leaf[i] = ModelMapping(
energy_balance=Monteith(),
photosynthesis=Fvcb(
VcMaxRef=set.VcMaxRef[i],
Expand Down
2 changes: 1 addition & 1 deletion docs/src/FAQ/translate_a_model.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ meteo_day = to_daily(meteo, :TT => (x -> sum(x) / 24) => :TT)
Then we can define our list of models, passing the values for `TT_cu` in the status at initialization:

```@example mymodel
m = ModelList(
m = ModelMapping(
ToyLAIModel(),
status = (TT_cu = cumsum(meteo_day.TT),),
)
Expand Down
2 changes: 1 addition & 1 deletion docs/src/developers.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ It’s probably now safe to request a merge.

### Other helpful things

⁃ In the `/PlantSimEngine/test` folder, there are a few basic helper functions. One of them outputs vectors of modellists, weather data, and output variables, which are used as a test bank/matrix for some tests, and provides wide coverage. If you wrote new models, new combinations of models, or added some new weather data, it helps to add them to the banks.
⁃ In the `/PlantSimEngine/test` folder, there are a few basic helper functions. One of them outputs vectors of ModelMapping, weather data, and output variables, which are used as a test bank/matrix for some tests, and provides wide coverage. If you wrote new models, new combinations of models, or added some new weather data, it helps to add them to the banks.
⁃ New downstream packages are worth adding to the integration and downstream package registry.
⁃ Unusual corner-cases are worth giving their own unit tests. Newly fixed bugs as well, even if the fix is fairly trivial.

Expand Down
35 changes: 18 additions & 17 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ using PlantSimEngine.Examples
# Import the example meteorological data:
meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18)

# Define the model:
model = ModelList(
ToyLAIModel(),
# Define the model mapping:
model = ModelMapping(
ToyLAIModel();
status=(TT_cu=1.0:2000.0,), # Pass the cumulated degree-days as input to the model
)

out = run!(model)

# Define the list of models for coupling:
model2 = ModelList(
# Define the mapping for coupled models:
model2 = ModelMapping(
ToyLAIModel(),
Beer(0.6),
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
)
out2 = run!(model2, meteo_day)
Expand Down Expand Up @@ -118,9 +118,9 @@ using PlantSimEngine
# Import the examples defined in the `Examples` sub-module
using PlantSimEngine.Examples

# Define the model:
model = ModelList(
ToyLAIModel(),
# Define the model mapping:
model = ModelMapping(
ToyLAIModel();
status=(TT_cu=1.0:2000.0,), # Pass the cumulated degree-days as input to the model
)

Expand All @@ -141,7 +141,7 @@ lines(out[:TT_cu], out[:LAI], color=:green, axis=(ylabel="LAI (m² m⁻²)", xla

### 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:
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 `ModelMapping`. For example, let's couple the `ToyLAIModel` with a model for light interception based on Beer's law:

```@example readme
# ] add PlantSimEngine, DataFrames, CSV
Expand All @@ -153,18 +153,18 @@ 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(
# Define the mapping for coupled models:
model2 = ModelMapping(
ToyLAIModel(),
Beer(0.6),
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)
```

The `ModelList` couples the models by automatically computing the dependency graph of the models. The resulting dependency graph is:
The `ModelMapping` couples the models by automatically computing the dependency graph of the models. The resulting dependency graph is:

```
╭──── Dependency graph ──────────────────────────────────────────╮
Expand Down Expand Up @@ -205,7 +205,7 @@ fig
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:

```@example readme
mapping = Dict(
mapping = ModelMapping(
"Scene" => ToyDegreeDaysCumulModel(),
"Plant" => (
MultiScaleModel(
Expand Down Expand Up @@ -295,8 +295,9 @@ We can then extract the outputs and convert them to a `DataFrame` for each scale

```@example readme
using DataFrames
df_dict = convert_outputs(out, DataFrame)
sort!(df_dict["Leaf"], [:timestep, :node])
df_outputs = convert_outputs(out, DataFrame)
leaf_df = df_outputs isa AbstractDict ? df_outputs["Leaf"] : df_outputs
sort!(leaf_df, [:timestep, :node])
```

An example output of a multiscale simulation is shown in the documentation of PlantBiophysics.jl:
Expand Down
12 changes: 6 additions & 6 deletions docs/src/model_coupling/model_coupling_user.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ using PlantSimEngine, PlantMeteo
# Import the example models defined in the `Examples` sub-module:
using PlantSimEngine.Examples

m = ModelList(
m = ModelMapping(
Process1Model(2.0),
Process2Model(),
Process3Model(),
Expand Down Expand Up @@ -44,7 +44,7 @@ using PlantSimEngine.Examples
Here is how we can make the model coupling:

```@example usepkg
m = ModelList(Process1Model(2.0), Process2Model(), Process3Model())
m = ModelMapping(Process1Model(2.0), Process2Model(), Process3Model())
nothing # hide
```

Expand Down Expand Up @@ -75,7 +75,7 @@ outputs(Process2Model())
So considering those two models, we only need `var1` and `var2` to be initialized, as `var3` is computed. This is why we recommend [`to_initialize`](@ref) instead of [`inputs`](@ref), because it returns only the variables that need to be initialized, considering that some inputs are duplicated between models, and some are computed by other models (they are outputs of a model):

```@example usepkg
m = ModelList(
m = ModelMapping(
Process1Model(2.0),
Process2Model(),
Process3Model(),
Expand All @@ -88,7 +88,7 @@ to_initialize(m)
The most straightforward way of initializing a model list is by giving the initializations to the `status` keyword argument during instantiation:

```@example usepkg
m = ModelList(
m = ModelMapping(
Process1Model(2.0),
Process2Model(),
Process3Model(),
Expand Down Expand Up @@ -118,7 +118,7 @@ All following models (`Process4Model` to `Process7Model`) do not call explicitly
Let's make a new model list including the soft-coupled models:

```@example usepkg
m = ModelList(
m = ModelMapping(
Process1Model(2.0),
Process2Model(),
Process3Model(),
Expand All @@ -139,7 +139,7 @@ to_initialize(m)
We can initialize it like so:

```@example usepkg
m = ModelList(
m = ModelMapping(
Process1Model(2.0),
Process2Model(),
Process3Model(),
Expand Down
8 changes: 4 additions & 4 deletions docs/src/model_execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Simulation order

`PlantSimEngine.jl` uses the [`ModelList`](@ref) to automatically compute a dependency graph between the models and run the simulation in the correct order. When running a simulation with [`run!`](@ref), the models are then executed following this simple set of rules:
`PlantSimEngine.jl` uses the [`ModelMapping`](@ref) to automatically compute a dependency graph between the models and run the simulation in the correct order. When running a simulation with [`run!`](@ref), the models are then executed following this simple set of rules:

1. Independent models are run first. A model is independent if it can be run independently from other models, only using initializations (or nothing).
2. Then, models that have a dependency on other models are run. The first ones are the ones that depend on an independent model. Then the ones that are children of the second ones, and then their children ... until no children are found anymore. There are two types of children models (*i.e.* dependencies): hard and soft dependencies:
Expand Down Expand Up @@ -93,7 +93,7 @@ aggregates over that civil day (including later timesteps from that day when ava
### Hold-last coupling (default policy)

```julia
mapping = Dict(
mapping = ModelMapping(
"Leaf" => (
ModelSpec(LeafSourceModel()) |> TimeStepModel(1.0),
ModelSpec(LeafConsumerModel()) |>
Expand All @@ -106,7 +106,7 @@ mapping = Dict(
### Daily integration from hourly stream

```julia
mapping = Dict(
mapping = ModelMapping(
"Leaf" => (
ModelSpec(HourlyAssimModel()) |> TimeStepModel(1.0),
),
Expand All @@ -121,7 +121,7 @@ mapping = Dict(
### Interpolate slow producer to fast consumer

```julia
mapping = Dict(
mapping = ModelMapping(
"Leaf" => (
ModelSpec(SlowSourceModel()) |> TimeStepModel(ClockSpec(2.0, 1.0)),
ModelSpec(FastConsumerModel()) |>
Expand Down
3 changes: 1 addition & 2 deletions docs/src/multirate/multirate_tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ leaf_proc = process(TutorialLeafHourlyModel())
plant_daily_proc = process(TutorialPlantDailyModel())
plant_weekly_proc = process(TutorialPlantWeeklyModel())

mapping = Dict(
mapping = ModelMapping(
"Leaf" => (
ModelSpec(TutorialLeafHourlyModel()) |>
TimeStepModel(hourly) |>
Expand All @@ -123,7 +123,6 @@ mapping = Dict(
Ri_SW_q=(source=:Ri_SW_f, reducer=RadiationEnergy()),
) |>
InputBindings(; leaf_assim_h=(process=leaf_proc, var=:leaf_assim_h, scale="Leaf", policy=Integrate())),

ModelSpec(TutorialPlantWeeklyModel()) |>
ScopeModel(:plant) |>
TimeStepModel(weekly) |>
Expand Down
Loading
Loading