Skip to content

Commit ab564e0

Browse files
committed
Define ModelMapping to replace the Dict for mapping + use the same one for ModelList
(TODO: make it work!!)
1 parent 725d339 commit ab564e0

78 files changed

Lines changed: 1450 additions & 913 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AI_PACKAGE_SUMMARY.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This document explains how PlantSimEngine is structured internally, how models a
77
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).
88

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

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

4141
### MultiScaleModel
42+
4243
File: `src/mtg/MultiScaleModel.jl`
4344
- Wrapper to attach a multiscale variable mapping to a model.
4445
- Supports scalar mapping (SingleNode), vector mapping (MultiNode), renaming, and `PreviousTimeStep`.

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ using PlantSimEngine
9292
using PlantSimEngine.Examples
9393

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

137137
### Model coupling
138138

139-
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:
139+
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:
140140

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

151151
# Define the list of models for coupling:
152-
model = ModelList(
152+
model = ModelMapping(
153153
ToyLAIModel(),
154154
Beer(0.6),
155155
status=(TT_cu=cumsum(meteo_day[:, :TT]),), # Pass the cumulated degree-days as input to `ToyLAIModel`, this could also be done using another model
156156
)
157157
```
158158

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

161161
```
162162
╭──── Dependency graph ──────────────────────────────────────────╮
@@ -223,7 +223,7 @@ fig
223223
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:
224224

225225
```julia
226-
mapping = Dict(
226+
mapping = ModelMapping(
227227
"Scene" => ToyDegreeDaysCumulModel(),
228228
"Plant" => (
229229
MultiScaleModel(
@@ -295,7 +295,7 @@ meteo = Weather(
295295
And run the simulation:
296296

297297
```julia
298-
out_vars = Dict(
298+
out_vars = ModelMapping(
299299
"Scene" => (:TT_cu,),
300300
"Plant" => (:carbon_allocation, :carbon_assimilation, :soil_water_content, :aPPFD, :TT_cu, :LAI),
301301
"Leaf" => (:carbon_demand, :carbon_allocation),

benchmark/test-PSE-benchmark.jl

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,16 @@ function PlantSimEngine.run!(m::ToyInternodeCrazyEmergence, models, status, mete
2727
#end
2828

2929
if length(MultiScaleTreeGraph.children(status.node)) == 1 && status.TT_cu - status.TT_cu_emergence >= m.TT_emergence
30-
30+
3131
status_new_internode = add_organ!(status.node, sim_object, "<", "Internode", 2, index=1)
3232
add_organ!(status_new_internode.node, sim_object, "+", "Leaf", 2, index=1)
3333
status_new_internode.TT_cu_emergence = status.TT_cu
34-
elseif (length(MultiScaleTreeGraph.children(status.node)) >= 2 && length(MultiScaleTreeGraph.children(status.node)) < 7) && status.TT_cu - status.TT_cu_emergence >= m.TT_emergence
34+
elseif (length(MultiScaleTreeGraph.children(status.node)) >= 2 && length(MultiScaleTreeGraph.children(status.node)) < 7) && status.TT_cu - status.TT_cu_emergence >= m.TT_emergence
3535
status_new_internode = add_organ!(status.node, sim_object, "<", "Internode", 2, index=1)
3636
add_organ!(status.node, sim_object, "+", "Leaf", 2, index=4)
3737
add_organ!(status.node, sim_object, "+", "Leaf", 2, index=5)
3838
status_new_internode.TT_cu_emergence = status.TT_cu
39-
elseif (length(MultiScaleTreeGraph.children(status.node)) >= 7 && length(MultiScaleTreeGraph.children(status.node)) < 30) && status.TT_cu - status.TT_cu_emergence >= m.TT_emergence
39+
elseif (length(MultiScaleTreeGraph.children(status.node)) >= 7 && length(MultiScaleTreeGraph.children(status.node)) < 30) && status.TT_cu - status.TT_cu_emergence >= m.TT_emergence
4040
add_organ!(status.node, sim_object, "+", "Leaf", 2, index=6)
4141
add_organ!(status.node, sim_object, "+", "Leaf", 2, index=7)
4242
add_organ!(status.node, sim_object, "+", "Leaf", 2, index=8)
@@ -53,13 +53,13 @@ end
5353
# Wrapped this into a function so that it doesn't plague the benchmark with variables on a global scope
5454
#@check_allocs
5555
function do_benchmark_on_heavier_mtg()
56-
mtg = import_mtg_example();
57-
56+
mtg = import_mtg_example()
57+
5858
# Example meteo, 365 timesteps :
5959
meteo_day = read_weather(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), duration=Day)
60-
60+
6161
#similar to the mtg growth test but with a much lower emergence threshold
62-
mapping = Dict(
62+
mapping = ModelMapping(
6363
"Scene" => ToyDegreeDaysCumulModel(),
6464
"Plant" => (
6565
MultiScaleModel(
@@ -110,13 +110,13 @@ function do_benchmark_on_heavier_mtg()
110110
ToySoilWaterModel(),
111111
),
112112
)
113-
113+
114114
out_vars = Dict(
115115
"Leaf" => (:carbon_assimilation, :carbon_demand, :soil_water_content, :carbon_allocation),
116116
"Internode" => (:carbon_allocation, :TT_cu_emergence),
117117
"Plant" => (:carbon_allocation,),
118118
"Soil" => (:soil_water_content,),
119119
)
120-
121-
out = run!(mtg, mapping, meteo_day, tracked_outputs=out_vars, executor=SequentialEx());
120+
121+
out = run!(mtg, mapping, meteo_day, tracked_outputs=out_vars, executor=SequentialEx())
122122
end

benchmark/test-multirate-buffer-benchmark.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ end
4545
function setup_multirate_buffer_benchmark(; nleaves=2000, ndays=30)
4646
mtg = _build_multirate_benchmark_mtg(nleaves)
4747

48-
mapping = Dict(
48+
mapping = ModelMapping(
4949
"Leaf" => (
5050
ModelSpec(MRBenchSourceModel(Ref(0))) |> TimeStepModel(1.0),
5151
),

benchmark/test-plantbiophysics.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ function benchmark_plantbiophysics()
6464
constants = Constants()
6565
#time_PB = Vector{Float64}(undef, N*microbenchmark_steps)
6666
for i = 1:N
67-
leaf = ModelList(
67+
leaf = ModelMapping(
6868
energy_balance=Monteith(),
6969
photosynthesis=Fvcb(
7070
VcMaxRef=set.VcMaxRef[i],
@@ -138,9 +138,9 @@ function setup_benchmark_plantbiophysics_multitimestep()
138138
@. set[!, :vpd] = e_sat(set.T) - vapor_pressure(set.T, set.Rh)
139139
@. set[!, :aPPFD] = set.Ra_SW_f * 0.48 * 4.57
140140

141-
leaf = Vector{ModelList}(undef, N)
141+
leaf = Vector{ModelMapping}(undef, N)
142142
for i = 1:N
143-
leaf[i] = ModelList(
143+
leaf[i] = ModelMapping(
144144
energy_balance=Monteith(),
145145
photosynthesis=Fvcb(
146146
VcMaxRef=set.VcMaxRef[i],

docs/src/FAQ/translate_a_model.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ meteo_day = to_daily(meteo, :TT => (x -> sum(x) / 24) => :TT)
146146
Then we can define our list of models, passing the values for `TT_cu` in the status at initialization:
147147

148148
```@example mymodel
149-
m = ModelList(
149+
m = ModelMapping(
150150
ToyLAIModel(),
151151
status = (TT_cu = cumsum(meteo_day.TT),),
152152
)

docs/src/developers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ It’s probably now safe to request a merge.
9494

9595
### Other helpful things
9696

97-
⁃ 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.
97+
⁃ 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.
9898
⁃ New downstream packages are worth adding to the integration and downstream package registry.
9999
⁃ Unusual corner-cases are worth giving their own unit tests. Newly fixed bugs as well, even if the fix is fairly trivial.
100100

docs/src/index.md

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,18 @@ using PlantSimEngine.Examples
1111
# Import the example meteorological data:
1212
meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18)
1313
14-
# Define the model:
15-
model = ModelList(
16-
ToyLAIModel(),
14+
# Define the model mapping:
15+
model = ModelMapping(
16+
ToyLAIModel();
1717
status=(TT_cu=1.0:2000.0,), # Pass the cumulated degree-days as input to the model
1818
)
1919
2020
out = run!(model)
2121
22-
# Define the list of models for coupling:
23-
model2 = ModelList(
22+
# Define the mapping for coupled models:
23+
model2 = ModelMapping(
2424
ToyLAIModel(),
25-
Beer(0.6),
25+
Beer(0.6);
2626
status=(TT_cu=cumsum(meteo_day[:, :TT]),), # Pass the cumulated degree-days as input to `ToyLAIModel`, this could also be done using another model
2727
)
2828
out2 = run!(model2, meteo_day)
@@ -118,9 +118,9 @@ using PlantSimEngine
118118
# Import the examples defined in the `Examples` sub-module
119119
using PlantSimEngine.Examples
120120
121-
# Define the model:
122-
model = ModelList(
123-
ToyLAIModel(),
121+
# Define the model mapping:
122+
model = ModelMapping(
123+
ToyLAIModel();
124124
status=(TT_cu=1.0:2000.0,), # Pass the cumulated degree-days as input to the model
125125
)
126126
@@ -141,7 +141,7 @@ lines(out[:TT_cu], out[:LAI], color=:green, axis=(ylabel="LAI (m² m⁻²)", xla
141141

142142
### Model coupling
143143

144-
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:
144+
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:
145145

146146
```@example readme
147147
# ] add PlantSimEngine, DataFrames, CSV
@@ -153,18 +153,18 @@ using PlantSimEngine.Examples
153153
# Import the example meteorological data:
154154
meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18)
155155
156-
# Define the list of models for coupling:
157-
model2 = ModelList(
156+
# Define the mapping for coupled models:
157+
model2 = ModelMapping(
158158
ToyLAIModel(),
159-
Beer(0.6),
159+
Beer(0.6);
160160
status=(TT_cu=cumsum(meteo_day[:, :TT]),), # Pass the cumulated degree-days as input to `ToyLAIModel`, this could also be done using another model
161161
)
162162
163163
# Run the simulation:
164164
out2 = run!(model2, meteo_day)
165165
```
166166

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

169169
```
170170
╭──── Dependency graph ──────────────────────────────────────────╮
@@ -205,7 +205,7 @@ fig
205205
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:
206206

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

296296
```@example readme
297297
using DataFrames
298-
df_dict = convert_outputs(out, DataFrame)
299-
sort!(df_dict["Leaf"], [:timestep, :node])
298+
df_outputs = convert_outputs(out, DataFrame)
299+
leaf_df = df_outputs isa AbstractDict ? df_outputs["Leaf"] : df_outputs
300+
sort!(leaf_df, [:timestep, :node])
300301
```
301302

302303
An example output of a multiscale simulation is shown in the documentation of PlantBiophysics.jl:

docs/src/model_coupling/model_coupling_user.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ using PlantSimEngine, PlantMeteo
55
# Import the example models defined in the `Examples` sub-module:
66
using PlantSimEngine.Examples
77
8-
m = ModelList(
8+
m = ModelMapping(
99
Process1Model(2.0),
1010
Process2Model(),
1111
Process3Model(),
@@ -44,7 +44,7 @@ using PlantSimEngine.Examples
4444
Here is how we can make the model coupling:
4545

4646
```@example usepkg
47-
m = ModelList(Process1Model(2.0), Process2Model(), Process3Model())
47+
m = ModelMapping(Process1Model(2.0), Process2Model(), Process3Model())
4848
nothing # hide
4949
```
5050

@@ -75,7 +75,7 @@ outputs(Process2Model())
7575
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):
7676

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

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

120120
```@example usepkg
121-
m = ModelList(
121+
m = ModelMapping(
122122
Process1Model(2.0),
123123
Process2Model(),
124124
Process3Model(),
@@ -139,7 +139,7 @@ to_initialize(m)
139139
We can initialize it like so:
140140

141141
```@example usepkg
142-
m = ModelList(
142+
m = ModelMapping(
143143
Process1Model(2.0),
144144
Process2Model(),
145145
Process3Model(),

docs/src/model_execution.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Simulation order
44

5-
`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:
5+
`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:
66

77
1. Independent models are run first. A model is independent if it can be run independently from other models, only using initializations (or nothing).
88
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:
@@ -93,7 +93,7 @@ aggregates over that civil day (including later timesteps from that day when ava
9393
### Hold-last coupling (default policy)
9494

9595
```julia
96-
mapping = Dict(
96+
mapping = ModelMapping(
9797
"Leaf" => (
9898
ModelSpec(LeafSourceModel()) |> TimeStepModel(1.0),
9999
ModelSpec(LeafConsumerModel()) |>
@@ -106,7 +106,7 @@ mapping = Dict(
106106
### Daily integration from hourly stream
107107

108108
```julia
109-
mapping = Dict(
109+
mapping = ModelMapping(
110110
"Leaf" => (
111111
ModelSpec(HourlyAssimModel()) |> TimeStepModel(1.0),
112112
),
@@ -121,7 +121,7 @@ mapping = Dict(
121121
### Interpolate slow producer to fast consumer
122122

123123
```julia
124-
mapping = Dict(
124+
mapping = ModelMapping(
125125
"Leaf" => (
126126
ModelSpec(SlowSourceModel()) |> TimeStepModel(ClockSpec(2.0, 1.0)),
127127
ModelSpec(FastConsumerModel()) |>

0 commit comments

Comments
 (0)