Skip to content

Commit a7840c4

Browse files
authored
Merge pull request #137 from VirtualPlantLab/dev
Dev
2 parents 48789d8 + 05ef744 commit a7840c4

25 files changed

Lines changed: 339 additions & 1342 deletions

.github/workflows/CI.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: CI
22
on:
33
push:
44
branches:
5-
- main
5+
- dev
66
tags: "*"
77
pull_request:
88
workflow_dispatch:

.github/workflows/Integration.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Integration
22
on:
33
push:
44
branches:
5-
- main
5+
- dev
66
tags: "*"
77
pull_request:
88
workflow_dispatch:
@@ -32,8 +32,8 @@ jobs:
3232
arch:
3333
- x64
3434
package:
35-
- {user: PalmStudio, repo: XPalm.jl, branch: PSE-API-changes}
36-
- {user: VEZY, repo: PlantBioPhysics.jl, branch: ModelList-outputs-filtering-changes}
35+
- {user: PalmStudio, repo: XPalm.jl, branch: dev}
36+
- {user: VEZY, repo: PlantBioPhysics.jl, branch: dev}
3737
steps:
3838
- uses: actions/checkout@v4
3939
- uses: julia-actions/setup-julia@v2

.github/workflows/benchmarks_and_downstream.yml

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
name: BenchmarksAndDownstream
1+
name: Benchmarks
22
on:
33
push:
44
branches:
5-
- main
5+
- dev
66
- benchmarks-github-action
77
tags: "*"
8-
workflow-dispatch:
8+
pull_request:
9+
workflow_dispatch:
910
permissions:
1011
# deployments permission to deploy GitHub pages website
1112
deployments: write
@@ -16,8 +17,6 @@ jobs:
1617
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }}
1718
runs-on: ${{ matrix.os }}
1819
timeout-minutes: 60
19-
env:
20-
GROUP: ${{ matrix.package.group }}
2120
strategy:
2221
fail-fast: false
2322
matrix:
@@ -30,6 +29,7 @@ jobs:
3029
arch:
3130
- x64
3231
package:
32+
# the group setting is unused atm
3333
- {user: VEZY, repo: PlantSimEngine.jl, group: Downstream}
3434
steps:
3535
- uses: actions/checkout@v4
@@ -43,8 +43,7 @@ jobs:
4343
run: |
4444
cd test/downstream
4545
julia --project --threads 4 --color=yes -e '
46-
using Pkg;
47-
Pkg.instantiate();
46+
using Pkg;
4847
include("test-all-benchmarks.jl")'
4948
rm Manifest.toml
5049
- name: Store benchmark result

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
.DS_Store
66
docs/Manifest.toml
77
test/Manifest.toml
8-
docs/build/
8+
docs/build/
9+
test/downstream/Manifest.toml

docs/src/index.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,12 +291,12 @@ out = run!(mtg, mapping, meteo, tracked_outputs=out_vars, executor=SequentialEx(
291291
nothing # hide
292292
```
293293

294-
We can then extract the outputs in a `DataFrame` and sort them:
294+
We can then extract the outputs and convert them to a `DataFrame` for each scale and sort them:
295295

296296
```@example readme
297297
using DataFrames
298-
df_out = convert_outputs(out, DataFrame)
299-
sort!(df_out, [:timestep, :node])
298+
df_dict = convert_outputs(out, DataFrame)
299+
sort!(df_dict["Leaf"], [:timestep, :node])
300300
```
301301

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

docs/src/multiscale/multiscale.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,11 +245,11 @@ outputs_sim = run!(mtg, mapping, meteo, tracked_outputs = outs);
245245
nothing # hide
246246
```
247247

248-
And that's it! We can now access the outputs for each scale as a dictionary of vectors of values per variable and scale.
248+
And that's it! We can now access the outputs for each scale as a dictionary of vectors of NamedTuple objects.
249249

250-
Or as a `DataFrame` using the [`DataFrames`](https://dataframes.juliadata.org) package:
250+
Or as a `DataFrame` dictionary using the [`DataFrames`](https://dataframes.juliadata.org) package:
251251

252252
```@example usepkg
253253
using DataFrames
254-
convert_outputs(outputs_sim, DataFrame)
254+
df_dict = convert_outputs(outputs_sim, DataFrame)
255255
```

docs/src/multiscale/multiscale_considerations.md

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -83,39 +83,65 @@ Instead of a [`ModelList`](@ref), it takes an MTG and a mapping. The optional `m
8383

8484
## Multi-scale output data structure
8585

86-
The output structure, like the mapping, is a Julia `Dict` structure indexed by scale. In each scale, another `Dict` maps variables to their values per timestep, per node. This makes the structure a little bulkier and a little more verbose to inspect than in single-scale, but the general usage is similar. Multiscale Tree Graph nodes are also added to the output data, as a `:node` entry.
86+
87+
The output structure, like the mapping, is a Julia `Dict` structure indexed by the scale name. Values are a per-scale `Vector{NamedTuple}` which lists the requested variables for every node at that scale, for every timestep in the simulation. Timestep and Multiscale Tree Graph nodes are also added to the output data, as a `:timestep`and a `:node` entry.
88+
89+
This dictionary structure makes the outputs as-is a little more verbose to inspect than in single-scale, but the general usage is similar, and it is both compact, and fast to convert to a `Dict{String, DataFrame}` which can make queries easier.
90+
91+
!!! note
92+
Some of the mapped variables -those that map from scalar to vector- will not be added to the outputs to save some memory and space since they are redundant.
93+
8794

8895
To illustrate, here's an example output from part 3 of the Toy plant tutorial, zeroing in on a variable at the "Root" scale: [Fixing bugs in the plant simulation](@ref):
8996

9097
```julia
9198
julia> outs
9299

93-
Dict{String, Dict{Symbol, Vector}} with 5 entries:
94-
"Internode" => Dict(:carbon_root_creation_consumed=>[[50.0, 50.0], [50.0, 50.0], [50.0, 50.0], [50.0, 50.0], [50.0,
95-
"Root" => Dict(:carbon_root_creation_consumed=>[[50.0, 50.0], [50.0, 50.0, 50.0], [50.0, 50.0, 50.0, 50.0], [50
96-
"Scene" => Dict(:TT_cu=>[[0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0] [2099.61], [20
97-
"Plant" => Dict(:carbon_root_creation_consumed=>[[50.0], [50.0], [50.0], [50.0], [50.0], [50.0], [50.0], [50.0],
98-
"Leaf" => Dict(:node=>Vector{Node{NodeMTG, Dict{Symbol, Any}}}[[+ 4: Leaf
100+
Dict{String, Vector} with 5 entries:
101+
"Internode" => @NamedTuple{timestep::Int64, node::Node{NodeMTG, Dict{Symbol, Any}}, carbon_root_creation_consumed::Float64, TT_cu::Float64, carbon_
102+
"Root" => @NamedTuple{timestep::Int64, node::Node{NodeMTG, Dict{Symbol, Any}}, carbon_root_creation_consumed::Float64, water_absorbed::Float64
103+
"Scene" => @NamedTuple{timestep::Int64, node::Node{NodeMTG, Dict{Symbol, Any}}, TT_cu::Float64, TT::Float64}[(timestep = 1, node = / 1: Scene
104+
"Plant" => @NamedTuple{timestep::Int64, node::Node{NodeMTG, Dict{Symbol, Any}}, carbon_root_creation_consumed::Float64, carbon_stock::Float64,
105+
"Leaf" => @NamedTuple{timestep::Int64, node::Node{NodeMTG, Dict{Symbol, Any}}, carbon_captured::Float64}[(timestep = 1, node = + 4: Leaf
99106

100107
julia> outs["Root"]
101-
Dict{Symbol, Vector} with 4 entries:
102-
:carbon_root_creation_consumed => [[50.0, 50.0], [50.0, 50.0, 50.0], [50.0, 50.0, 50.0, 50.0], [50.0, 50.0, 50.0, 50
103-
:node => Vector{Node{NodeMTG, Dict{Symbol, Any}}}[[+ 9: Root
104-
:water_absorbed => [[0.5, 0.0], [1.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0], [1.1, 1.1, 1.1, 1.1, 0.0], [0.
105-
:root_water_assimilation => [[1.0, 1.0], [1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0, 1.0], [1.
106-
107-
julia> outs["Root"][:carbon_root_creation_consumed]
108-
365-element Vector{Vector{Float64}}:
109-
[50.0, 50.0] # timestep 1: two root nodes
110-
[50.0, 50.0, 50.0]
111-
[50.0, 50.0, 50.0, 50.0]
112-
[50.0, 50.0, 50.0, 50.0, 50.0]
113-
[50.0, 50.0, 50.0, 50.0, 50.0, 50.0]
114-
[50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0] # timestep 6: 7 root nodes
108+
3257-element Vector{@NamedTuple{timestep::Int64, node::Node{NodeMTG, Dict{Symbol, Any}}, carbon_root_creation_consumed::Float64, water_absorbed::Float64, root_water_assimilation::Float64}}:
109+
(timestep = 1, node = + 9: Root
110+
└─ < 10: Root
111+
└─ < 11: Root
112+
└─ < 12: Root
113+
└─ < 13: Root
114+
└─ < 14: Root
115+
└─ < 15: Root
116+
└─ < 16: Root
117+
└─ < 17: Root
118+
, carbon_root_creation_consumed = 50.0, water_absorbed = 0.5, root_water_assimilation = 1.0)
115119
116120
```
117121
118-
As more roots get added in this simulation, the vectors expand to list the values of all the nodes for every variable for every timestep.
122+
Values are more complex to query than in a single-scale simulation since the indexing isn't straightforward to map to a timestep:
123+
124+
```julia
125+
julia> [Pair(outs["Root"][i][:timestep], outs["Root"][i][:carbon_root_creation_consumed]) for i in 1:length(outs["Root"])]
126+
3257-element Vector{Pair{Int64, Float64}}:
127+
1 => 50.0
128+
1 => 50.0
129+
2 => 50.0
130+
2 => 50.0
131+
2 => 50.0
132+
133+
365 => 50.0
134+
365 => 50.0
135+
365 => 50.0
136+
365 => 50.0
137+
365 => 50.0
138+
365 => 50.0
139+
365 => 50.0
140+
365 => 50.0
141+
365 => 50.0
142+
```
143+
144+
Converting to a dictionary of DataFrame objects can make such queries easier to write.
119145
120146
!!! warning
121147
Currently, the `:node` entry only shallow copies nodes. The `:node` values at each scale for every timestep actually reflect the final state of the node, meaning attribute values may not correspond to the value at that timestep. You may need to output these values via a dedicated model to keep track of them properly.

docs/src/multiscale/multiscale_example_3.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,13 +244,19 @@ Depth = 3
244244

245245
There is one quirk you may have noticed when inspecting the data : when a root expands, the new root is immediately active, and some models may act on it immediately... including the root growth model. Meaning this new root may also sprout another root in the same timestep, and so on.
246246

247-
You can notice this by looking at the simulation's state after the first timestep:
247+
You can notice this by looking at the simulation's state during the first two timesteps:
248248

249249
```@example usepkg
250250
outs = run!(mtg, mapping, first(meteo_day, 2))
251-
nodes_per_timestep = outs["Root"][:node]
252-
root_lengths_per_timestep = [length(nodes_per_timestep[i]) for i in 1:length(nodes_per_timestep)]
253251
252+
root_nodes_per_timestep = [0, 0]
253+
for i in 1:length(outs["Root"])
254+
if outs["Root"][i].timestep < 3
255+
root_nodes_per_timestep[outs["Root"][i].timestep] += 1
256+
end
257+
end
258+
259+
root_nodes_per_timestep
254260
```
255261

256262
Our root grew to full length within one timestep. Oops.

docs/src/multiscale/single_to_multiscale.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,20 +203,18 @@ We can access the output variables at the "Scene" scale by indexing our outputs:
203203
```@example usepkg
204204
outputs_multiscale["Scene"]
205205
```
206-
and then the computed `:TT_cu`:
206+
We have a `Vector{NamedTuple}`structure. Our single-scale output is a `Vector{T}`:
207207
```@example usepkg
208-
outputs_multiscale["Scene"][:TT_cu]
208+
outputs_singlescale.TT_cu
209209
```
210210

211-
As you can see, it is a `Vector{Vector{T}}`, whereas our single-scale output is a `Vector{T}`:
211+
Let's extract the multi-scale `:TT_cu`:
212212
```@example usepkg
213-
outputs_singlescale.TT_cu
213+
computed_TT_cu_multiscale = [outputs_multiscale["Scene"][i].TT_cu for i in 1:length(outputs_multiscale["Scene"])]
214214
```
215215

216-
To compare them value-by-value, we can flatten the multiscale Vector and then do a piecewise approximate equality test :
216+
We can now compare them value-by-value and do a piecewise approximate equality test :
217217
```@example usepkg
218-
computed_TT_cu_multiscale = collect(Base.Iterators.flatten(outputs_multiscale["Scene"][:TT_cu]))
219-
220218
for i in 1:length(computed_TT_cu_multiscale)
221219
if !(computed_TT_cu_multiscale[i] ≈ outputs_singlescale.TT_cu[i])
222220
println(i)

docs/src/working_with_data/floating_point_accumulation_error.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,11 @@ mtg_multiscale = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Scen
9595
plant = MultiScaleTreeGraph.Node(mtg_multiscale, MultiScaleTreeGraph.NodeMTG("+", "Plant", 1, 1))
9696
9797
outputs_multiscale = run!(mtg_multiscale, mapping_multiscale, meteo_day)
98-
computed_TT_cu_multiscale = collect(Base.Iterators.flatten(outputs_multiscale["Scene"][:TT_cu]))
9998
```
10099

101100
```@example usepkg
102101
103-
computed_TT_cu_multiscale = collect(Base.Iterators.flatten(outputs_multiscale["Scene"][:TT_cu]))
104-
102+
computed_TT_cu_multiscale = [outputs_multiscale["Scene"][i].TT_cu for i in 1:length(outputs_multiscale["Scene"])]
105103
is_approx_equal = length(unique(computed_TT_cu_multiscale .≈ outputs_singlescale.TT_cu)) == 1
106104
```
107105

@@ -110,8 +108,7 @@ Why was the comparison only approximate ? Why `≈` instead of `==`?
110108
Let's try it out. What if write instead:
111109

112110
```@example usepkg
113-
computed_TT_cu_multiscale = collect(Base.Iterators.flatten(outputs_multiscale["Scene"][:TT_cu]))
114-
111+
computed_TT_cu_multiscale = [outputs_multiscale["Scene"][i].TT_cu for i in 1:length(outputs_multiscale["Scene"])]
115112
is_perfectly_equal = length(unique(computed_TT_cu_multiscale .== outputs_singlescale.TT_cu)) == 1
116113
```
117114

0 commit comments

Comments
 (0)