From ea9262e6b9a4a9d6f67fbabfd9792d706dd0de46 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Wed, 22 Jan 2025 16:35:29 +0100 Subject: [PATCH 001/147] Extra mapping with status vector test --- test/test-mapping.jl | 54 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/test/test-mapping.jl b/test/test-mapping.jl index b5921d9fe..364c8b24f 100755 --- a/test/test-mapping.jl +++ b/test/test-mapping.jl @@ -277,4 +277,58 @@ mapping_without_vectors = PlantSimEngine.replace_mapping_status_vectors_with_gen check=true, executor=SequentialEx() ) + + #replace a value with a constant vector and ensure no changes happen in the simulation + carbon_biomass_vec = Vector{Float64}(undef, nsteps) + for i in nsteps + carbon_biomass_vec[i] = 2.0 + end + mapping_with_two_vectors = Dict("Plant" => ( + MultiScaleModel( + model=ToyCAllocationModel(), + mapping=[ + # inputs + :carbon_assimilation => ["Leaf"], + :carbon_demand => ["Leaf", "Internode"], + # outputs + :carbon_allocation => ["Leaf", "Internode"] + ], + ), + MultiScaleModel( + model=ToyPlantRmModel(), + mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], + ), + ), + "Internode" => ( + ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), + ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004), + Status(TT=TT_v, carbon_biomass=1.0) + ), + "Leaf" => ( + MultiScaleModel( + model=ToyAssimModel(), + mapping=[:soil_water_content => "Soil",], + ), + ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), + ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), + Status(aPPFD=1300.0, carbon_biomass=carbon_biomass_vec, TT=10.0), # Replaced with vector here + ), + "Soil" => ( + ToySoilWaterModel(), + ), + ) + + mtg = import_mtg_example() + mapping_without_vectors_2 = PlantSimEngine.replace_mapping_status_vectors_with_generated_models(mapping_with_two_vectors, "Soil", nsteps) + graph_sim_multiscale_2 = @test_nowarn PlantSimEngine.GraphSimulation(mtg, mapping_without_vectors_2, nsteps=nsteps, check=true, outputs=out_multiscale) + + sim_multiscale_2 = run!(graph_sim_multiscale_2, + meteo_day, + PlantMeteo.Constants(), + nothing; + check=true, + executor=SequentialEx() + ) + + @test compare_outputs_graphsim(graph_sim_multiscale, graph_sim_multiscale_2) end \ No newline at end of file From cff3325524cfe0410c8ab970c96490e7708d9339 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 10:53:41 +0100 Subject: [PATCH 002/147] XPalm downstream test/benchmark --- test/downstream/test-xpalm.jl | 48 ++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/test/downstream/test-xpalm.jl b/test/downstream/test-xpalm.jl index 5e199e395..e03fc0059 100644 --- a/test/downstream/test-xpalm.jl +++ b/test/downstream/test-xpalm.jl @@ -1,5 +1,47 @@ +#using Pkg +#Pkg.develop("PlantSimEngine") +#using PlantSimEngine + +# no release of XPalm yet, so can't just add it to the .toml +using Pkg +Pkg.add(url="https://github.com/PalmStudio/XPalm.jl") + using Test +using PlantMeteo#, MultiScaleTreeGraph +#using CairoMakie, AlgebraOfGraphics +using DataFrames, CSV, Statistics +using Dates +using XPalm +using BenchmarkTools + +function xpalm_default_param_run() + meteo = CSV.read("../XPalm.jl/0-data/Meteo_Nigeria_PR.txt", DataFrame) + meteo.duration = [Dates.Day(i[1:1]) for i in meteo.duration] + m = Weather(meteo) + + out_vars = Dict{String,Any}( + "Scene" => (:lai,), + # "Scene" => (:lai, :scene_leaf_area, :aPPFD, :TEff), + # "Plant" => (:plant_age, :ftsw, :newPhytomerEmergence, :aPPFD, :plant_leaf_area, :carbon_assimilation, :carbon_offer_after_rm, :Rm, :TT_since_init, :TEff, :phytomer_count, :newPhytomerEmergence), + "Leaf" => (:Rm, :potential_area, :TT_since_init, :TEff, :A, :carbon_demand, :carbon_allocation,), + # "Leaf" => (:Rm, :potential_area), + # "Internode" => (:Rm, :carbon_allocation, :carbon_demand), + "Male" => (:Rm,), + # "Female" => (:biomass,), + # "Soil" => (:TEff, :ftsw, :root_depth), + ) + + # Example 1: Run the model with the default parameters (but output as a DataFrame): + #df = xpalm(m; vars=out_vars, sink=DataFrame) + df = XPalm.xpalm(m, DataFrame; vars=out_vars) +end + +#=@testset "XPalm simple test" begin + # default number of seconds is 5 + b_XP = @benchmark xpalm_default_param_run() seconds = 120 + + #N = length(b_XP.times) -@testset "XPalm dummy test TODO" begin - @test true -end \ No newline at end of file + @test mean(b_XP.times*1e-9) > 10 + @test mean(b_XP.times*1e-9) < 15 +end =# \ No newline at end of file From 926cbad138d0a2b0ec4a6c57fd070d8c16a582ab Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 11:01:41 +0100 Subject: [PATCH 003/147] Attempt at adding a github action to run several benchmarks and tracking results in a page --- .../workflows/benchmarks_and_downstream.yml | 63 +++++++++ test/Project.toml | 2 + test/downstream/Project.toml | 3 + test/downstream/test-PSE-benchmark.jl | 122 +++++++++++++++++ test/downstream/test-all-benchmarks.jl | 42 ++++++ test/downstream/test-plantbiophysics.jl | 19 +-- test/runtests.jl | 9 ++ test/test-performance.jl | 126 +----------------- 8 files changed, 252 insertions(+), 134 deletions(-) create mode 100644 .github/workflows/benchmarks_and_downstream.yml create mode 100644 test/downstream/test-PSE-benchmark.jl create mode 100644 test/downstream/test-all-benchmarks.jl diff --git a/.github/workflows/benchmarks_and_downstream.yml b/.github/workflows/benchmarks_and_downstream.yml new file mode 100644 index 000000000..27d8b5e35 --- /dev/null +++ b/.github/workflows/benchmarks_and_downstream.yml @@ -0,0 +1,63 @@ +name: BenchmarksAndDownstream +on: + push: + branches: + - main + tags: "*" + pull_request: + workflow_dispatch: +jobs: + test: + name: ${{ matrix.package.repo }}/${{ matrix.package.group }}/${{ matrix.julia-version }} + runs-on: ${{ matrix.os }} + env: + GROUP: ${{ matrix.package.group }} + strategy: + fail-fast: false + matrix: + version: + - "1.9" + - "1" + os: + - ubuntu-latest + - macOS-latest + - windows-latest + arch: + - x64 + package: + - {user: VEZY, repo: PlantSimEngine.jl, group: Downstream} + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: ${{ matrix.julia-version }} + arch: x64 +#TODO handle breaking changes the way downstream tests do ? + - name: Run benchmarks + run: | + cd test/downstream + julia --project --color=yes -e ' + using Pkg; + Pkg.instantiate(); + include("test-all-benchmarks.jl")' + - name: Store benchmark result + uses: benchmark-action/github-action-benchmark@v1 + with: + name: Julia benchmark result + tool: 'julia' + output-file-path: test/downstream/output.json + # Use personal access token instead of GITHUB_TOKEN due to https://github.community/t/github-action-not-triggering-gh-pages-upon-push/16096 + github-token: ${{ secrets.GITHUB_TOKEN }} + auto-push: true + # Show alert with commit comment on detecting possible performance regression + alert-threshold: '130%' + comment-on-alert: true + fail-on-alert: true + alert-comment-cc-users: '@Samuel-AMAP' + + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: lcov.info + fail_ci_if_error: false \ No newline at end of file diff --git a/test/Project.toml b/test/Project.toml index 6ae7f9b2f..3db4d22b7 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,10 +1,12 @@ [deps] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" MultiScaleTreeGraph = "dd4a991b-8a45-4075-bede-262ee62d5583" PlantMeteo = "4630fe09-e0fb-4da5-a846-781cb73437b6" +PlantSimEngine = "9a576370-710b-4269-adf9-4f603a9c6423" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/downstream/Project.toml b/test/downstream/Project.toml index f3acf78ca..783efd236 100644 --- a/test/downstream/Project.toml +++ b/test/downstream/Project.toml @@ -2,8 +2,11 @@ BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +MultiScaleTreeGraph = "dd4a991b-8a45-4075-bede-262ee62d5583" PlantBiophysics = "7ae8fcfa-76ad-4ec6-9ea7-5f8f5e2d6ec9" PlantMeteo = "4630fe09-e0fb-4da5-a846-781cb73437b6" +PlantSimEngine = "9a576370-710b-4269-adf9-4f603a9c6423" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +XPalm = "6b523e1e-d512-416c-8e51-a8fbef0064e7" diff --git a/test/downstream/test-PSE-benchmark.jl b/test/downstream/test-PSE-benchmark.jl new file mode 100644 index 000000000..f4d4ea455 --- /dev/null +++ b/test/downstream/test-PSE-benchmark.jl @@ -0,0 +1,122 @@ +############################################# +### Simulation with many organs in the MTG (but only a few different types of organs) + + +PlantSimEngine.@process "organ_crazy_emergence" verbose = false + +""" + ToyInternodeCrazyEmergence(;init_TT=0.0, TT_emergence = 300) + +Computes the organ emergence based on cumulated thermal time since last event. +""" +struct ToyInternodeCrazyEmergence <: AbstractOrgan_Crazy_EmergenceModel + TT_emergence::Float64 +end + +ToyInternodeCrazyEmergence(; TT_emergence=300.0) = ToyInternodeCrazyEmergence(TT_emergence) + +PlantSimEngine.inputs_(m::ToyInternodeCrazyEmergence) = (TT_cu=-Inf,) +PlantSimEngine.outputs_(m::ToyInternodeCrazyEmergence) = (TT_cu_emergence=0.0,) + +function PlantSimEngine.run!(m::ToyInternodeCrazyEmergence, models, status, meteo, constants=nothing, sim_object=nothing) + + #root = get_root(status.node) + + #if nleaves(root) > 10000 + # return nothing + #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 + 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 + 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) + add_organ!(status.node, sim_object, "+", "Leaf", 2, index=9) + add_organ!(status.node, sim_object, "+", "Leaf", 2, index=10) + add_organ!(status.node, sim_object, "+", "Leaf", 2, index=11) + + end + + return nothing +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(); + + # 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( + "Scene" => ToyDegreeDaysCumulModel(), + "Plant" => ( + MultiScaleModel( + model=ToyLAIModel(), + mapping=[ + :TT_cu => "Scene", + ], + ), + PlantSimEngine.Examples.Beer(0.6), + MultiScaleModel( + model=ToyCAllocationModel(), + mapping=[ + :carbon_assimilation => ["Leaf"], + :carbon_demand => ["Leaf", "Internode"], + :carbon_allocation => ["Leaf", "Internode"] + ], + ), + MultiScaleModel( + model=ToyPlantRmModel(), + mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], + ), + ), + "Internode" => ( + MultiScaleModel( + model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), + mapping=[:TT => "Scene",], + ), + MultiScaleModel( + model=ToyInternodeCrazyEmergence(TT_emergence=1.0), + mapping=[:TT_cu => "Scene"], + ), + ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004), + Status(carbon_biomass=1.0) + ), + "Leaf" => ( + MultiScaleModel( + model=ToyAssimModel(), + mapping=[:soil_water_content => "Soil", :aPPFD => "Plant"], + ), + MultiScaleModel( + model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), + mapping=[:TT => "Scene",], + ), + ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), + Status(carbon_biomass=1.0) + ), + "Soil" => ( + 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, outputs=out_vars, executor=SequentialEx()); +end \ No newline at end of file diff --git a/test/downstream/test-all-benchmarks.jl b/test/downstream/test-all-benchmarks.jl new file mode 100644 index 000000000..0ecb185c7 --- /dev/null +++ b/test/downstream/test-all-benchmarks.jl @@ -0,0 +1,42 @@ +using Pkg +Pkg.activate(dirname(@__FILE__)) +Pkg.develop(PackageSpec(path=dirname(dirname(@__DIR__)))) +Pkg.instantiate() + +using PlantSimEngine +using PlantSimEngine.Examples +#using Test, Aqua +using DataFrames, CSV +using MultiScaleTreeGraph +using PlantMeteo, Statistics +#using Documenter # for doctests + +# Include the example dummy processes: +using PlantSimEngine.Examples + +using BenchmarkTools +using Dates + + suite = BenchmarkGroup() + suite["bench"]=BenchmarkGroup(["PSE", "PBP", "XPalm"]) + + # "PSE benchmark" + include("test-PSE-benchmark.jl") + suite["bench"]["PSE"] = @benchmarkable do_benchmark_on_heavier_mtg() + + #BenchmarkTools.save("test/downstream/output.json", median(b_PSE)) + + #activate_downstream_env() + # "PBP benchmark" + include("test-plantbiophysics.jl") + suite["bench"]["PBP"] = @benchmarkable benchmark_plantbiophysics() + #BenchmarkTools.save("test/downstream/output.json", median(b_PBP)) + + + # "XPalm benchmark" + include("test-xpalm.jl") + suite["bench"]["XPalm"] = @benchmarkable xpalm_default_param_run() seconds = 120 + + tune!(suite) + results = run(suite, verbose = true) + BenchmarkTools.save("test/downstream/output.json", median(results)) \ No newline at end of file diff --git a/test/downstream/test-plantbiophysics.jl b/test/downstream/test-plantbiophysics.jl index f8c1dd47c..53c829ffb 100644 --- a/test/downstream/test-plantbiophysics.jl +++ b/test/downstream/test-plantbiophysics.jl @@ -78,20 +78,21 @@ function benchmark_plantbiophysics() d=set.d[i], ), ) - deps = PlantSimEngine.dep(leaf) + #deps = PlantSimEngine.dep(leaf) meteo = Atmosphere(T=set.T[i], Wind=set.Wind[i], P=set.P[i], Rh=set.Rh[i], Cₐ=set.Ca[i]) - st = PlantMeteo.row_struct(leaf.status[1]) - b_PB = @benchmark run!($leaf, $deps, 1, $st, $meteo, $constants, nothing; executor = ThreadedEx()) evals = microbenchmark_evals samples = microbenchmark_steps - + #st = PlantMeteo.row_struct(leaf.status[1]) + #b_PB = @benchmark run!($leaf, $meteo, $constants, nothing; executor = ThreadedEx()) evals = microbenchmark_evals samples = microbenchmark_steps + run!(leaf, meteo, constants, nothing; executor = ThreadedEx()) + # transform in seconds - for j in 1:microbenchmark_steps + #=for j in 1:microbenchmark_steps time_PB[microbenchmark_steps*(i-1) + j] = b_PB.times[j]*1e-9 - end + end=# end - return time_PB + #return time_PB end -@testset "PlantBiophysics benchmark" begin +#=@testset "PlantBiophysics benchmark" begin time_PB = benchmark_plantbiophysics() N = length(time_PB) @@ -103,7 +104,7 @@ end @test mean(time_PB) > 5e-7 @test mean(time_PB) < 5e-6 #TODO -end +end=# #= function run_plantbiophysics() diff --git a/test/runtests.jl b/test/runtests.jl index 5d443737b..6cf550f70 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -9,6 +9,11 @@ using Documenter # for doctests include("helper-functions.jl") +# There are 3 kinds of tests : +# PSE functionality/feature tests +# Integration tests (launched in Github Actions, they run PBP and XPalm tests) +# Benchmarks both internal and downstream, located in the downstream folder, and run in another Github Action + @testset "Testing PlantSimEngine" begin Aqua.test_all(PlantSimEngine, ambiguities=false) Aqua.test_ambiguities([PlantSimEngine]) @@ -63,6 +68,10 @@ include("helper-functions.jl") include("test-corner-cases.jl") end + @testset "Multithreading" begin + include("test-performance.jl") + end + if VERSION >= v"1.10" # Some formating changed in Julia 1.10, e.g. @NamedTuple instead of NamedTuple. @testset "Doctests" begin diff --git a/test/test-performance.jl b/test/test-performance.jl index 781a27ac7..69b22cb83 100644 --- a/test/test-performance.jl +++ b/test/test-performance.jl @@ -47,128 +47,4 @@ PlantSimEngine.TimeStepDependencyTrait(::Type{<:ToySleepModel}) = PlantSimEngine # todo DataFrame equals @test status(models1) == status(models2) -end - - -############################################# -### Simulation with many organs in the MTG (but only a few different types of organs) - - -PlantSimEngine.@process "organ_crazy_emergence" verbose = false - -""" - ToyInternodeCrazyEmergence(;init_TT=0.0, TT_emergence = 300) - -Computes the organ emergence based on cumulated thermal time since last event. -""" -struct ToyInternodeCrazyEmergence <: AbstractOrgan_Crazy_EmergenceModel - TT_emergence::Float64 -end - -ToyInternodeCrazyEmergence(; TT_emergence=300.0) = ToyInternodeCrazyEmergence(TT_emergence) - -PlantSimEngine.inputs_(m::ToyInternodeCrazyEmergence) = (TT_cu=-Inf,) -PlantSimEngine.outputs_(m::ToyInternodeCrazyEmergence) = (TT_cu_emergence=0.0,) - -function PlantSimEngine.run!(m::ToyInternodeCrazyEmergence, models, status, meteo, constants=nothing, sim_object=nothing) - - #root = get_root(status.node) - - #if nleaves(root) > 10000 - # return nothing - #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 - 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 - 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) - add_organ!(status.node, sim_object, "+", "Leaf", 2, index=9) - add_organ!(status.node, sim_object, "+", "Leaf", 2, index=10) - add_organ!(status.node, sim_object, "+", "Leaf", 2, index=11) - - end - - return nothing -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(); - - # 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( - "Scene" => ToyDegreeDaysCumulModel(), - "Plant" => ( - MultiScaleModel( - model=ToyLAIModel(), - mapping=[ - :TT_cu => "Scene", - ], - ), - Beer(0.6), - MultiScaleModel( - model=ToyCAllocationModel(), - mapping=[ - :carbon_assimilation => ["Leaf"], - :carbon_demand => ["Leaf", "Internode"], - :carbon_allocation => ["Leaf", "Internode"] - ], - ), - MultiScaleModel( - model=ToyPlantRmModel(), - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], - ), - ), - "Internode" => ( - MultiScaleModel( - model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), - mapping=[:TT => "Scene",], - ), - MultiScaleModel( - model=ToyInternodeCrazyEmergence(TT_emergence=1.0), - mapping=[:TT_cu => "Scene"], - ), - ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004), - Status(carbon_biomass=1.0) - ), - "Leaf" => ( - MultiScaleModel( - model=ToyAssimModel(), - mapping=[:soil_water_content => "Soil", :aPPFD => "Plant"], - ), - MultiScaleModel( - model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), - mapping=[:TT => "Scene",], - ), - ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), - Status(carbon_biomass=1.0) - ), - "Soil" => ( - 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, outputs=out_vars, executor=SequentialEx()); -end +end \ No newline at end of file From 9f5c36e7a0e9581511e45e6fb90da48a56a3fb62 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 11:04:57 +0100 Subject: [PATCH 004/147] Minor change to the PSE benchmark to try and force GH action to trigger and look for differences --- test/downstream/test-PSE-benchmark.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/downstream/test-PSE-benchmark.jl b/test/downstream/test-PSE-benchmark.jl index f4d4ea455..156de01d3 100644 --- a/test/downstream/test-PSE-benchmark.jl +++ b/test/downstream/test-PSE-benchmark.jl @@ -36,7 +36,7 @@ function PlantSimEngine.run!(m::ToyInternodeCrazyEmergence, models, status, mete 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)) < 31) && 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) From eb02b868bda76c533650b1461f4dc785f1e70c3b Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 11:07:56 +0100 Subject: [PATCH 005/147] Fix yml typo --- .github/workflows/benchmarks_and_downstream.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/benchmarks_and_downstream.yml b/.github/workflows/benchmarks_and_downstream.yml index 27d8b5e35..666e07485 100644 --- a/.github/workflows/benchmarks_and_downstream.yml +++ b/.github/workflows/benchmarks_and_downstream.yml @@ -10,11 +10,12 @@ jobs: test: name: ${{ matrix.package.repo }}/${{ matrix.package.group }}/${{ matrix.julia-version }} runs-on: ${{ matrix.os }} + timeout-minutes: 60 env: GROUP: ${{ matrix.package.group }} strategy: fail-fast: false - matrix: + matrix: version: - "1.9" - "1" From 34fcc74f82e78133393189974ece253daaef8933 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 11:10:35 +0100 Subject: [PATCH 006/147] Fix more yml typos --- .github/workflows/benchmarks_and_downstream.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/benchmarks_and_downstream.yml b/.github/workflows/benchmarks_and_downstream.yml index 666e07485..8a1acd1f9 100644 --- a/.github/workflows/benchmarks_and_downstream.yml +++ b/.github/workflows/benchmarks_and_downstream.yml @@ -31,8 +31,8 @@ jobs: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v2 with: - version: ${{ matrix.julia-version }} - arch: x64 + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} #TODO handle breaking changes the way downstream tests do ? - name: Run benchmarks run: | From 215d24b3e6c83afbe874a4827c6ecc131a86627b Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 11:14:49 +0100 Subject: [PATCH 007/147] deactivate XPalm benchmark for now --- test/downstream/test-all-benchmarks.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/downstream/test-all-benchmarks.jl b/test/downstream/test-all-benchmarks.jl index 0ecb185c7..dea8c5eb2 100644 --- a/test/downstream/test-all-benchmarks.jl +++ b/test/downstream/test-all-benchmarks.jl @@ -18,7 +18,7 @@ using BenchmarkTools using Dates suite = BenchmarkGroup() - suite["bench"]=BenchmarkGroup(["PSE", "PBP", "XPalm"]) + suite["bench"]=BenchmarkGroup(["PSE", "PBP"])#, "XPalm"]) # "PSE benchmark" include("test-PSE-benchmark.jl") @@ -34,8 +34,8 @@ using Dates # "XPalm benchmark" - include("test-xpalm.jl") - suite["bench"]["XPalm"] = @benchmarkable xpalm_default_param_run() seconds = 120 + #include("test-xpalm.jl") + #suite["bench"]["XPalm"] = @benchmarkable xpalm_default_param_run() seconds = 120 tune!(suite) results = run(suite, verbose = true) From 7da246c3479a430b1105b71e72d4db7243b83c0f Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 11:17:47 +0100 Subject: [PATCH 008/147] Also remove XPalm from the project toml, woops --- test/downstream/Project.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/test/downstream/Project.toml b/test/downstream/Project.toml index 783efd236..397e54380 100644 --- a/test/downstream/Project.toml +++ b/test/downstream/Project.toml @@ -9,4 +9,3 @@ PlantSimEngine = "9a576370-710b-4269-adf9-4f603a9c6423" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -XPalm = "6b523e1e-d512-416c-8e51-a8fbef0064e7" From 2359f87bedff25e1e9d285374bd5648c72fe394b Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 11:29:10 +0100 Subject: [PATCH 009/147] Attempt to fix output.json path --- test/downstream/test-all-benchmarks.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/downstream/test-all-benchmarks.jl b/test/downstream/test-all-benchmarks.jl index dea8c5eb2..e6d05b981 100644 --- a/test/downstream/test-all-benchmarks.jl +++ b/test/downstream/test-all-benchmarks.jl @@ -39,4 +39,4 @@ using Dates tune!(suite) results = run(suite, verbose = true) - BenchmarkTools.save("test/downstream/output.json", median(results)) \ No newline at end of file + BenchmarkTools.save(dirname(@__FILE__)*"output.json", median(results)) \ No newline at end of file From 2b5f0c3182a2ce040bcad9c9d340b42044220ecc Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 11:34:09 +0100 Subject: [PATCH 010/147] Better job naming --- .github/workflows/benchmarks_and_downstream.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmarks_and_downstream.yml b/.github/workflows/benchmarks_and_downstream.yml index 8a1acd1f9..37d4af084 100644 --- a/.github/workflows/benchmarks_and_downstream.yml +++ b/.github/workflows/benchmarks_and_downstream.yml @@ -8,7 +8,7 @@ on: workflow_dispatch: jobs: test: - name: ${{ matrix.package.repo }}/${{ matrix.package.group }}/${{ matrix.julia-version }} + name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} runs-on: ${{ matrix.os }} timeout-minutes: 60 env: From 7a43f53872083ac5a28f30a72065f55e213e6768 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 12:53:37 +0100 Subject: [PATCH 011/147] Attempt to fix output file path on GH actions --- .github/workflows/benchmarks_and_downstream.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmarks_and_downstream.yml b/.github/workflows/benchmarks_and_downstream.yml index 37d4af084..1d22f9c0a 100644 --- a/.github/workflows/benchmarks_and_downstream.yml +++ b/.github/workflows/benchmarks_and_downstream.yml @@ -46,7 +46,7 @@ jobs: with: name: Julia benchmark result tool: 'julia' - output-file-path: test/downstream/output.json + output-file-path: ${{ (GITHUB_WORKSPACE }}/test/downstream/output.json # Use personal access token instead of GITHUB_TOKEN due to https://github.community/t/github-action-not-triggering-gh-pages-upon-push/16096 github-token: ${{ secrets.GITHUB_TOKEN }} auto-push: true From b1daba91054de7ad1f7a3a5ecf45f1e799987ee3 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 12:55:42 +0100 Subject: [PATCH 012/147] Typo fix --- .github/workflows/benchmarks_and_downstream.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmarks_and_downstream.yml b/.github/workflows/benchmarks_and_downstream.yml index 1d22f9c0a..daaae631d 100644 --- a/.github/workflows/benchmarks_and_downstream.yml +++ b/.github/workflows/benchmarks_and_downstream.yml @@ -46,7 +46,7 @@ jobs: with: name: Julia benchmark result tool: 'julia' - output-file-path: ${{ (GITHUB_WORKSPACE }}/test/downstream/output.json + output-file-path: ${{ (github.workspace }}/test/downstream/output.json # Use personal access token instead of GITHUB_TOKEN due to https://github.community/t/github-action-not-triggering-gh-pages-upon-push/16096 github-token: ${{ secrets.GITHUB_TOKEN }} auto-push: true From 8da1013e9cb73c54dc788ff76b4e88b228782023 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 13:03:36 +0100 Subject: [PATCH 013/147] typo fix --- .github/workflows/benchmarks_and_downstream.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmarks_and_downstream.yml b/.github/workflows/benchmarks_and_downstream.yml index daaae631d..0a3675f78 100644 --- a/.github/workflows/benchmarks_and_downstream.yml +++ b/.github/workflows/benchmarks_and_downstream.yml @@ -46,7 +46,7 @@ jobs: with: name: Julia benchmark result tool: 'julia' - output-file-path: ${{ (github.workspace }}/test/downstream/output.json + output-file-path: ${{ github.workspace }}/test/downstream/output.json # Use personal access token instead of GITHUB_TOKEN due to https://github.community/t/github-action-not-triggering-gh-pages-upon-push/16096 github-token: ${{ secrets.GITHUB_TOKEN }} auto-push: true From 5dc410499450b9d494e8d5b91b7875af26cd4003 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 13:32:51 +0100 Subject: [PATCH 014/147] Fix json output file path... --- test/downstream/test-all-benchmarks.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/downstream/test-all-benchmarks.jl b/test/downstream/test-all-benchmarks.jl index e6d05b981..2b218833f 100644 --- a/test/downstream/test-all-benchmarks.jl +++ b/test/downstream/test-all-benchmarks.jl @@ -39,4 +39,4 @@ using Dates tune!(suite) results = run(suite, verbose = true) - BenchmarkTools.save(dirname(@__FILE__)*"output.json", median(results)) \ No newline at end of file + BenchmarkTools.save(dirname(@__FILE__)*"/output.json", median(results)) \ No newline at end of file From d43ea5457ec2fc0620ff0d8295783cd2f80beca5 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 14:06:08 +0100 Subject: [PATCH 015/147] Make the CI job run with 4 threads instead of single-threaded --- .github/workflows/CI.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 364ddc81a..77b3a2d80 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -40,6 +40,8 @@ jobs: - uses: julia-actions/cache@v2 - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 + with: + test_args: '--threads 4' - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v5 with: From 0f93474a8dca4190086dc3524521038f3b7109f2 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 14:24:55 +0100 Subject: [PATCH 016/147] Remove Manifest.toml preventing branch switch to gh-pages after benchmarks are done --- .github/workflows/benchmarks_and_downstream.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/benchmarks_and_downstream.yml b/.github/workflows/benchmarks_and_downstream.yml index 0a3675f78..60f94f949 100644 --- a/.github/workflows/benchmarks_and_downstream.yml +++ b/.github/workflows/benchmarks_and_downstream.yml @@ -4,8 +4,11 @@ on: branches: - main tags: "*" - pull_request: - workflow_dispatch: + permissions: + # deployments permission to deploy GitHub pages website + deployments: write + # contents permission to update benchmark contents in gh-pages branch + contents: write jobs: test: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} @@ -41,6 +44,7 @@ jobs: using Pkg; Pkg.instantiate(); include("test-all-benchmarks.jl")' + rm Manifest.toml - name: Store benchmark result uses: benchmark-action/github-action-benchmark@v1 with: From 2482cbb06912069630dacbf9c9d53ed14a2c4db3 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 14:26:20 +0100 Subject: [PATCH 017/147] Add Dates to the test environment for MT tests --- test/Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Project.toml b/test/Project.toml index 3db4d22b7..f806effc7 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -3,6 +3,7 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" MultiScaleTreeGraph = "dd4a991b-8a45-4075-bede-262ee62d5583" PlantMeteo = "4630fe09-e0fb-4da5-a846-781cb73437b6" From 20af2ae9700014fa518a578a441b1903bbcd6c55 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 14:29:24 +0100 Subject: [PATCH 018/147] Typo fix --- .github/workflows/benchmarks_and_downstream.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmarks_and_downstream.yml b/.github/workflows/benchmarks_and_downstream.yml index 60f94f949..397d97f1a 100644 --- a/.github/workflows/benchmarks_and_downstream.yml +++ b/.github/workflows/benchmarks_and_downstream.yml @@ -4,7 +4,7 @@ on: branches: - main tags: "*" - permissions: +permissions: # deployments permission to deploy GitHub pages website deployments: write # contents permission to update benchmark contents in gh-pages branch From ea108a465bc344b80738c067cc11e643254fe64e Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 14:35:55 +0100 Subject: [PATCH 019/147] Reintroduce testing on pull-request --- .github/workflows/benchmarks_and_downstream.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/benchmarks_and_downstream.yml b/.github/workflows/benchmarks_and_downstream.yml index 397d97f1a..eddd8a59b 100644 --- a/.github/workflows/benchmarks_and_downstream.yml +++ b/.github/workflows/benchmarks_and_downstream.yml @@ -4,6 +4,7 @@ on: branches: - main tags: "*" + pull-request: permissions: # deployments permission to deploy GitHub pages website deployments: write From 4fe6ba2dcd86baa591ca515acd2e6d60e3d8593e Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 14:41:37 +0100 Subject: [PATCH 020/147] Fix MT test with module/environment issue --- test/test-performance.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test-performance.jl b/test/test-performance.jl index 69b22cb83..f5a00cbb1 100644 --- a/test/test-performance.jl +++ b/test/test-performance.jl @@ -17,6 +17,9 @@ end PlantSimEngine.TimeStepDependencyTrait(::Type{<:ToySleepModel}) = PlantSimEngine.IsTimeStepIndependent() +models1 = ModelList(process1=ToySleepModel(), status=(a=vc,)) +models2 = ModelList(process1=ToySleepModel(), status=(a=vc,)) + @testset begin "Check number of threads" nthr = Threads.nthreads() @test nthr == 4 @@ -26,9 +29,6 @@ PlantSimEngine.TimeStepDependencyTrait(::Type{<:ToySleepModel}) = PlantSimEngine vc = [0 for i in 1:nrows] - models1 = ModelList(process1=ToySleepModel(), status=(a=vc,)) - models2 = ModelList(process1=ToySleepModel(), status=(a=vc,)) - t_seq = @benchmark run!(models1, meteo_day; executor = SequentialEx()) #t_seq = run!(models1, meteo_day; executor = SequentialEx()) min_time_seq = minimum(t_seq).time From b071640d7a168947a9adea65e5cb3b24d5e5710c Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 14:46:51 +0100 Subject: [PATCH 021/147] Reenable manual workflow triggering, and for pushes on the test branch --- .github/workflows/benchmarks_and_downstream.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/benchmarks_and_downstream.yml b/.github/workflows/benchmarks_and_downstream.yml index eddd8a59b..6d98089ba 100644 --- a/.github/workflows/benchmarks_and_downstream.yml +++ b/.github/workflows/benchmarks_and_downstream.yml @@ -3,8 +3,9 @@ on: push: branches: - main + - benchmarks-github-action tags: "*" - pull-request: + workflow-dispatch: permissions: # deployments permission to deploy GitHub pages website deployments: write From 09837a16a80413c84de000f427ecacaa0d8356ba Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 15:03:13 +0100 Subject: [PATCH 022/147] MT test fix, slightly pertube PSE benchmark --- test/downstream/test-PSE-benchmark.jl | 2 +- test/test-performance.jl | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/downstream/test-PSE-benchmark.jl b/test/downstream/test-PSE-benchmark.jl index 156de01d3..f4d4ea455 100644 --- a/test/downstream/test-PSE-benchmark.jl +++ b/test/downstream/test-PSE-benchmark.jl @@ -36,7 +36,7 @@ function PlantSimEngine.run!(m::ToyInternodeCrazyEmergence, models, status, mete 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)) < 31) && 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) diff --git a/test/test-performance.jl b/test/test-performance.jl index f5a00cbb1..af514e3dd 100644 --- a/test/test-performance.jl +++ b/test/test-performance.jl @@ -17,6 +17,11 @@ end PlantSimEngine.TimeStepDependencyTrait(::Type{<:ToySleepModel}) = PlantSimEngine.IsTimeStepIndependent() +meteo_day = read_weather(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), duration=Day) + nrows = nrow(meteo_day) + + vc = [0 for i in 1:nrows] + models1 = ModelList(process1=ToySleepModel(), status=(a=vc,)) models2 = ModelList(process1=ToySleepModel(), status=(a=vc,)) @@ -24,11 +29,6 @@ models2 = ModelList(process1=ToySleepModel(), status=(a=vc,)) nthr = Threads.nthreads() @test nthr == 4 - meteo_day = read_weather(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), duration=Day) - nrows = nrow(meteo_day) - - vc = [0 for i in 1:nrows] - t_seq = @benchmark run!(models1, meteo_day; executor = SequentialEx()) #t_seq = run!(models1, meteo_day; executor = SequentialEx()) min_time_seq = minimum(t_seq).time From 4ebf71e6203f506de18f4112b4c6cc87a0777ba1 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 15:21:01 +0100 Subject: [PATCH 023/147] Fix attempt for launching with 4 threads --- .github/workflows/CI.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 77b3a2d80..bc5bc8891 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -40,8 +40,8 @@ jobs: - uses: julia-actions/cache@v2 - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - with: - test_args: '--threads 4' + env: + JULIA_NUM_THREADS: 4 - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v5 with: From d0c81ba4334b06f58d9dc92b5d9f0046f77f9549 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 15:44:01 +0100 Subject: [PATCH 024/147] Switch min to median in MT test (more reliable) --- test/test-performance.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test-performance.jl b/test/test-performance.jl index af514e3dd..958c77962 100644 --- a/test/test-performance.jl +++ b/test/test-performance.jl @@ -31,19 +31,19 @@ models2 = ModelList(process1=ToySleepModel(), status=(a=vc,)) t_seq = @benchmark run!(models1, meteo_day; executor = SequentialEx()) #t_seq = run!(models1, meteo_day; executor = SequentialEx()) - min_time_seq = minimum(t_seq).time + med_time_seq = median(t_seq).time #time is in nanoseconds @test min_time_seq > nrows * 1000000 t_mt = @benchmark run!(models2, meteo_day; executor = ThreadedEx()) #t_mt = run!(models2, meteo_day; executor = ThreadedEx()) - min_time_mt = minimum(t_mt).time + med_time_mt = median(t_mt).time - @test min_time_mt > nrows * 1000000 / nthr + @test med_time_mt > nrows * 1000000 / nthr # expecting mt to have some overhead - @test nthr * min_time_mt > min_time_seq + @test nthr * med_time_mt > med_time_seq # todo DataFrame equals @test status(models1) == status(models2) From 338cff7a9ca73a51c4cc05de2d042e0b0d2d7c4c Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 15:48:51 +0100 Subject: [PATCH 025/147] Attempt to distinguish between OSes for benchmark tracking and avoid confusion between runners with different performance characteristics --- test/downstream/test-all-benchmarks.jl | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/downstream/test-all-benchmarks.jl b/test/downstream/test-all-benchmarks.jl index 2b218833f..9bbbcebb9 100644 --- a/test/downstream/test-all-benchmarks.jl +++ b/test/downstream/test-all-benchmarks.jl @@ -17,19 +17,28 @@ using PlantSimEngine.Examples using BenchmarkTools using Dates + suite_name = "bench_" + + if Sys.iswindows() + suite_name = suite_name * "windows" + elseif Sys.isapple() + suite_name = suite_name * "mac" + elseif Sys.islinux() + suite_name = suite_name * "linux" + end suite = BenchmarkGroup() - suite["bench"]=BenchmarkGroup(["PSE", "PBP"])#, "XPalm"]) + suite[suite_name]=BenchmarkGroup(["PSE", "PBP"])#, "XPalm"]) # "PSE benchmark" include("test-PSE-benchmark.jl") - suite["bench"]["PSE"] = @benchmarkable do_benchmark_on_heavier_mtg() + suite[suite_name]["PSE"] = @benchmarkable do_benchmark_on_heavier_mtg() #BenchmarkTools.save("test/downstream/output.json", median(b_PSE)) #activate_downstream_env() # "PBP benchmark" include("test-plantbiophysics.jl") - suite["bench"]["PBP"] = @benchmarkable benchmark_plantbiophysics() + suite[suite_name]["PBP"] = @benchmarkable benchmark_plantbiophysics() #BenchmarkTools.save("test/downstream/output.json", median(b_PBP)) From 80c3c28075748104bf155c9c55ef610cc2da9a0e Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 15:55:29 +0100 Subject: [PATCH 026/147] Fix typos --- test/downstream/test-all-benchmarks.jl | 2 +- test/test-performance.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/downstream/test-all-benchmarks.jl b/test/downstream/test-all-benchmarks.jl index 9bbbcebb9..3f3e13b39 100644 --- a/test/downstream/test-all-benchmarks.jl +++ b/test/downstream/test-all-benchmarks.jl @@ -44,7 +44,7 @@ using Dates # "XPalm benchmark" #include("test-xpalm.jl") - #suite["bench"]["XPalm"] = @benchmarkable xpalm_default_param_run() seconds = 120 + #suite[suite_name]["XPalm"] = @benchmarkable xpalm_default_param_run() seconds = 120 tune!(suite) results = run(suite, verbose = true) diff --git a/test/test-performance.jl b/test/test-performance.jl index 958c77962..704aee208 100644 --- a/test/test-performance.jl +++ b/test/test-performance.jl @@ -34,7 +34,7 @@ models2 = ModelList(process1=ToySleepModel(), status=(a=vc,)) med_time_seq = median(t_seq).time #time is in nanoseconds - @test min_time_seq > nrows * 1000000 + @test med_time_seq > nrows * 1000000 t_mt = @benchmark run!(models2, meteo_day; executor = ThreadedEx()) #t_mt = run!(models2, meteo_day; executor = ThreadedEx()) From 445892c8fb7487e547efd5f5465930c9ba04645c Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 16:01:56 +0100 Subject: [PATCH 027/147] Remove 1.9 tests. There currently is a precompilation issue with Statistics, but it seems like overkill anyway as CI already has 1.9 tests --- .github/workflows/benchmarks_and_downstream.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/benchmarks_and_downstream.yml b/.github/workflows/benchmarks_and_downstream.yml index 6d98089ba..f487a315f 100644 --- a/.github/workflows/benchmarks_and_downstream.yml +++ b/.github/workflows/benchmarks_and_downstream.yml @@ -22,7 +22,6 @@ jobs: fail-fast: false matrix: version: - - "1.9" - "1" os: - ubuntu-latest @@ -38,7 +37,8 @@ jobs: with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} -#TODO handle breaking changes the way downstream tests do ? + # TODO handle breaking changes the way downstream tests do ? + # NOTE : manifest toml file is removed otherwise git whines about untracked changes when switching branches for the gh-pages commit - name: Run benchmarks run: | cd test/downstream From 38ff559ce9c1312bfbd860c04e97b1d2396b1141 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 27 Jan 2025 16:16:13 +0100 Subject: [PATCH 028/147] Add small margin on MT vs ST comparison to avoid false positives on windows CI runners --- test/test-performance.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/test-performance.jl b/test/test-performance.jl index 704aee208..8c789f2e8 100644 --- a/test/test-performance.jl +++ b/test/test-performance.jl @@ -42,8 +42,9 @@ models2 = ModelList(process1=ToySleepModel(), status=(a=vc,)) @test med_time_mt > nrows * 1000000 / nthr - # expecting mt to have some overhead - @test nthr * med_time_mt > med_time_seq + # Expecting mt to have some overhead, but add a margin regardless as windows CI runners seem inconsistent + # Dunno why. Threads sleep/wakeup scheduling overhead ? + @test nthr * med_time_mt > med_time_seq *1.1 # todo DataFrame equals @test status(models1) == status(models2) From f8c971f627370df249fe0ad2664fcb6fc39a5fd4 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Tue, 28 Jan 2025 10:37:29 +0100 Subject: [PATCH 029/147] Modify MT test to avoid more runner false positives --- test/test-performance.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test-performance.jl b/test/test-performance.jl index 8c789f2e8..32a193411 100644 --- a/test/test-performance.jl +++ b/test/test-performance.jl @@ -42,9 +42,9 @@ models2 = ModelList(process1=ToySleepModel(), status=(a=vc,)) @test med_time_mt > nrows * 1000000 / nthr - # Expecting mt to have some overhead, but add a margin regardless as windows CI runners seem inconsistent - # Dunno why. Threads sleep/wakeup scheduling overhead ? - @test nthr * med_time_mt > med_time_seq *1.1 + # Threads sleep/wakeup scheduling overhead causing inconsistencies ? + # In any case, sometimes MT beats ST on CI runners + @test abs(nthr * med_time_mt - med_time_seq) < 0.2 * med_time_seq # todo DataFrame equals @test status(models1) == status(models2) From 1adb53f754c215e9078185ef750f60ba476daee0 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Tue, 28 Jan 2025 10:40:35 +0100 Subject: [PATCH 030/147] =?UTF-8?q?Add=20R=C3=A9mi=20to=20the=20cc=20alert?= =?UTF-8?q?=20email=20sending=20on=20performance=20drop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/benchmarks_and_downstream.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmarks_and_downstream.yml b/.github/workflows/benchmarks_and_downstream.yml index f487a315f..126d8a3c1 100644 --- a/.github/workflows/benchmarks_and_downstream.yml +++ b/.github/workflows/benchmarks_and_downstream.yml @@ -60,7 +60,7 @@ jobs: alert-threshold: '130%' comment-on-alert: true fail-on-alert: true - alert-comment-cc-users: '@Samuel-AMAP' + alert-comment-cc-users: '@Samuel-AMAP, @VEZY' - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v5 From 53e3c6d730b0d154e2aa36bc5aedce3909ceae48 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Tue, 28 Jan 2025 10:41:06 +0100 Subject: [PATCH 031/147] Deliberately crash PSE benchmark, for science --- test/downstream/test-PSE-benchmark.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/downstream/test-PSE-benchmark.jl b/test/downstream/test-PSE-benchmark.jl index f4d4ea455..0112fcfab 100644 --- a/test/downstream/test-PSE-benchmark.jl +++ b/test/downstream/test-PSE-benchmark.jl @@ -31,7 +31,7 @@ function PlantSimEngine.run!(m::ToyInternodeCrazyEmergence, models, status, mete 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) @@ -42,7 +42,7 @@ function PlantSimEngine.run!(m::ToyInternodeCrazyEmergence, models, status, mete add_organ!(status.node, sim_object, "+", "Leaf", 2, index=8) add_organ!(status.node, sim_object, "+", "Leaf", 2, index=9) add_organ!(status.node, sim_object, "+", "Leaf", 2, index=10) - add_organ!(status.node, sim_object, "+", "Leaf", 2, index=11) + add_organ!(status.node, sim_object, "+", "Leaf", 2, index=11)=# end @@ -55,6 +55,7 @@ end function do_benchmark_on_heavier_mtg() mtg = import_mtg_example(); + crash # Example meteo, 365 timesteps : meteo_day = read_weather(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), duration=Day) From 6babbb60154aa5d31d0f06959327032496ec6612 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Tue, 28 Jan 2025 10:53:36 +0100 Subject: [PATCH 032/147] Restore PSE benchmark, with a somewhat faster-running model --- test/downstream/test-PSE-benchmark.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/downstream/test-PSE-benchmark.jl b/test/downstream/test-PSE-benchmark.jl index 0112fcfab..9971f065b 100644 --- a/test/downstream/test-PSE-benchmark.jl +++ b/test/downstream/test-PSE-benchmark.jl @@ -54,8 +54,7 @@ end #@check_allocs function do_benchmark_on_heavier_mtg() mtg = import_mtg_example(); - - crash + # Example meteo, 365 timesteps : meteo_day = read_weather(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), duration=Day) From ef330c52465f7d2f8ea71face76d970d9116bbae Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Tue, 28 Jan 2025 11:18:08 +0100 Subject: [PATCH 033/147] Restore PSE benchmark to full, expecting email from the GH benchmark tracking action --- test/downstream/test-PSE-benchmark.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/downstream/test-PSE-benchmark.jl b/test/downstream/test-PSE-benchmark.jl index 9971f065b..ec5c14f4c 100644 --- a/test/downstream/test-PSE-benchmark.jl +++ b/test/downstream/test-PSE-benchmark.jl @@ -31,7 +31,7 @@ function PlantSimEngine.run!(m::ToyInternodeCrazyEmergence, models, status, mete 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) @@ -42,7 +42,7 @@ function PlantSimEngine.run!(m::ToyInternodeCrazyEmergence, models, status, mete add_organ!(status.node, sim_object, "+", "Leaf", 2, index=8) add_organ!(status.node, sim_object, "+", "Leaf", 2, index=9) add_organ!(status.node, sim_object, "+", "Leaf", 2, index=10) - add_organ!(status.node, sim_object, "+", "Leaf", 2, index=11)=# + add_organ!(status.node, sim_object, "+", "Leaf", 2, index=11) end From 16d4bf28b5ef83cda780f5335286187e32498202 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Tue, 28 Jan 2025 15:39:42 +0100 Subject: [PATCH 034/147] Add multitimestep PBP benchmarks (single-threaded and multi-threaded) --- test/downstream/test-all-benchmarks.jl | 65 ++++++----- test/downstream/test-plantbiophysics.jl | 142 ++++++++++++++---------- 2 files changed, 115 insertions(+), 92 deletions(-) diff --git a/test/downstream/test-all-benchmarks.jl b/test/downstream/test-all-benchmarks.jl index 3f3e13b39..f9635775a 100644 --- a/test/downstream/test-all-benchmarks.jl +++ b/test/downstream/test-all-benchmarks.jl @@ -5,11 +5,9 @@ Pkg.instantiate() using PlantSimEngine using PlantSimEngine.Examples -#using Test, Aqua using DataFrames, CSV using MultiScaleTreeGraph using PlantMeteo, Statistics -#using Documenter # for doctests # Include the example dummy processes: using PlantSimEngine.Examples @@ -17,35 +15,34 @@ using PlantSimEngine.Examples using BenchmarkTools using Dates - suite_name = "bench_" - - if Sys.iswindows() - suite_name = suite_name * "windows" - elseif Sys.isapple() - suite_name = suite_name * "mac" - elseif Sys.islinux() - suite_name = suite_name * "linux" - end - suite = BenchmarkGroup() - suite[suite_name]=BenchmarkGroup(["PSE", "PBP"])#, "XPalm"]) - - # "PSE benchmark" - include("test-PSE-benchmark.jl") - suite[suite_name]["PSE"] = @benchmarkable do_benchmark_on_heavier_mtg() - - #BenchmarkTools.save("test/downstream/output.json", median(b_PSE)) - - #activate_downstream_env() - # "PBP benchmark" - include("test-plantbiophysics.jl") - suite[suite_name]["PBP"] = @benchmarkable benchmark_plantbiophysics() - #BenchmarkTools.save("test/downstream/output.json", median(b_PBP)) - - - # "XPalm benchmark" - #include("test-xpalm.jl") - #suite[suite_name]["XPalm"] = @benchmarkable xpalm_default_param_run() seconds = 120 - - tune!(suite) - results = run(suite, verbose = true) - BenchmarkTools.save(dirname(@__FILE__)*"/output.json", median(results)) \ No newline at end of file +suite_name = "bench_" + +if Sys.iswindows() + suite_name = suite_name * "windows" +elseif Sys.isapple() + suite_name = suite_name * "mac" +elseif Sys.islinux() + suite_name = suite_name * "linux" +end +suite = BenchmarkGroup() +suite[suite_name] = BenchmarkGroup(["PSE", "PBP"])#, "XPalm"]) + +# "PSE benchmark" +include("test-PSE-benchmark.jl") +suite[suite_name]["PSE"] = @benchmarkable do_benchmark_on_heavier_mtg() + +# "PBP benchmark" +include("test-plantbiophysics.jl") +suite[suite_name]["PBP"] = @benchmarkable benchmark_plantbiophysics() + +leaf, meteo = setup_benchmark_plantbiophysics_multitimestep() +suite[suite_name]["PBP_multiple_timesteps_MT"] = @benchmarkable benchmark_plantbiophysics_multitimestep_MT($leaf, $meteo) +suite[suite_name]["PBP_multiple_timesteps_ST"] = @benchmarkable benchmark_plantbiophysics_multitimestep_ST($leaf, $meteo) + +# "XPalm benchmark" +#include("test-xpalm.jl") +#suite[suite_name]["XPalm"] = @benchmarkable xpalm_default_param_run() seconds = 120 + +tune!(suite) +results = run(suite, verbose=true) +BenchmarkTools.save(dirname(@__FILE__) * "/output.json", median(results)) \ No newline at end of file diff --git a/test/downstream/test-plantbiophysics.jl b/test/downstream/test-plantbiophysics.jl index 53c829ffb..8f026c30b 100644 --- a/test/downstream/test-plantbiophysics.jl +++ b/test/downstream/test-plantbiophysics.jl @@ -1,15 +1,15 @@ -#TODO Cleanup +# For local testing : #using Pkg #Pkg.develop("PlantSimEngine") #using PlantSimEngine using Statistics -using DataFrames -using CSV +#using DataFrames +#using CSV using Random using PlantBiophysics -using BenchmarkTools -using Test -using PlantMeteo +#using BenchmarkTools +#using Test +#using PlantMeteo function benchmark_plantbiophysics() @@ -60,7 +60,7 @@ function benchmark_plantbiophysics() constants = Constants() - time_PB = Vector{Float64}(undef, N*microbenchmark_steps) + #time_PB = Vector{Float64}(undef, N*microbenchmark_steps) for i = 1:N leaf = ModelList( energy_balance=Monteith(), @@ -92,64 +92,90 @@ function benchmark_plantbiophysics() #return time_PB end -#=@testset "PlantBiophysics benchmark" begin - - time_PB = benchmark_plantbiophysics() - N = length(time_PB) - #statsPB = (mean(time_PB), median(time_PB), Statistics.std(time_PB), findmin(time_PB), findmax(time_PB)) - min__ = findmin(time_PB)[1] - max__ = findmin(time_PB)[1] - @test min__ > 1e-7 - @test max__ < 1e-5 - @test mean(time_PB) > 5e-7 - @test mean(time_PB) < 5e-6 - #TODO -end=# - -#= -function run_plantbiophysics() - - - Rs = 10.0 - Ta = 18.0 - Wind = 0.5 - P = 90.0 - Rh = 0.1 - Ca = 360.0 - skyF = 0.0 - d = 0.001 - Jmax = 200.0 - Vmax = 150.0 - Rd = 0.3 - TPU = 5.0 - g0 = 0.001 - g1 = 0.5 - vpd = e_sat(Ta) - vapor_pressure(Ta, Rh) - PPFD = Rs*0.48*4.57 +function setup_benchmark_plantbiophysics_multitimestep() - constants = Constants() + Random.seed!(1) # Set random seed + N = 100 # Number of timesteps simulated for each microbenchmark step - leaf = ModelList( + length_range = 10000 + Rs = range(10, 500, length=length_range) + Ta = range(18, 40, length=length_range) + Wind = range(0.5, 20, length=length_range) + P = range(90, 101, length=length_range) + Rh = range(0.1, 0.98, length=length_range) + Ca = range(360, 900, length=length_range) + skyF = range(0.0, 1.0, length=length_range) + d = range(0.001, 0.5, length=length_range) + Jmax = range(200.0, 300.0, length=length_range) + Vmax = range(150.0, 250.0, length=length_range) + Rd = range(0.3, 2.0, length=length_range) + TPU = range(5.0, 20.0, length=length_range) + g0 = range(0.001, 2.0, length=length_range) + g1 = range(0.5, 15.0, length=length_range) + vars = hcat([Ta, Wind, P, Rh, Ca, Jmax, Vmax, Rd, Rs, skyF, d, TPU, g0, g1]) + + set = [rand.(vars) for i = 1:N] + set = reshape(vcat(set...), (length(set[1]), length(set)))' + name = [ + "T", + "Wind", + "P", + "Rh", + "Ca", + "JMaxRef", + "VcMaxRef", + "RdRef", + "Rs", + "sky_fraction", + "d", + "TPURef", + "g0", + "g1", + ] + set = DataFrame(set, name) + @. set[!, :vpd] = e_sat(set.T) - vapor_pressure(set.T, set.Rh) + @. set[!, :PPFD] = set.Rs * 0.48 * 4.57 + + leaf = Vector{ModelList}(undef, N) + for i = 1:N + + leaf[i] = ModelList( energy_balance=Monteith(), photosynthesis=Fvcb( - VcMaxRef=Vmax, - JMaxRef=Jmax, - RdRef=Rd, - TPURef=TPU, + VcMaxRef=set.VcMaxRef[i], + JMaxRef=set.JMaxRef[i], + RdRef=set.RdRef[i], + TPURef=set.TPURef[i], ), - stomatal_conductance=Medlyn(g0, g1), + stomatal_conductance=Medlyn(set.g0[i], set.g1[i]), status=( - Rₛ=Rs, - sky_fraction=skyF, - PPFD=PPFD, - d=d, + Rₛ=set.Rs, + sky_fraction=set.sky_fraction, + PPFD=set.PPFD, + d=set.d, ), ) - deps = PlantSimEngine.dep(leaf) - meteo = Atmosphere(T=Ta, Wind=Wind, P=P, Rh=Rh, Cₐ=Ca) - st = PlantMeteo.row_struct(leaf.status[1]) - run!(leaf, deps, 1, st, meteo, constants, nothing) + end + + atm = Vector{Atmosphere}(undef, N) + for i in 1:N + atm[i]= Atmosphere(T=set.T[i], Wind=set.Wind[i], P=set.P[i], Rh=set.Rh[i], Cₐ=set.Ca[i]) + end + meteo = Weather(atm) + + return leaf, meteo end -run_plantbiophysics() -=# \ No newline at end of file +function benchmark_plantbiophysics_multitimestep_MT(leaf, meteo) + N = length(meteo) + for i in 1:N + run!(leaf[i], meteo, Constants(), nothing; executor = ThreadedEx()) + end +end + +function benchmark_plantbiophysics_multitimestep_ST(leaf, meteo) + N = length(meteo) + for i in 1:N + run!(leaf[i], meteo, Constants(), nothing; executor = SequentialEx()) + end +end \ No newline at end of file From e6a8846c3ee415f2833686cd45441bff02be8dcd Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Tue, 28 Jan 2025 15:41:04 +0100 Subject: [PATCH 035/147] Deactivate a basic multithreading check on mac runners for now --- test/test-performance.jl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/test-performance.jl b/test/test-performance.jl index 32a193411..70a9ca12b 100644 --- a/test/test-performance.jl +++ b/test/test-performance.jl @@ -43,8 +43,11 @@ models2 = ModelList(process1=ToySleepModel(), status=(a=vc,)) @test med_time_mt > nrows * 1000000 / nthr # Threads sleep/wakeup scheduling overhead causing inconsistencies ? - # In any case, sometimes MT beats ST on CI runners - @test abs(nthr * med_time_mt - med_time_seq) < 0.2 * med_time_seq + # In any case, sometimes MT beats ST on CI runners, and the mac runner seems to return puzzling false positives + # Deactivating it on mac for non + if !Sys.isapple() + @test abs(nthr * med_time_mt - med_time_seq) < 0.2 * med_time_seq + end # todo DataFrame equals @test status(models1) == status(models2) From 0804e741e21279de6946ac60fef037d10f1867f2 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Tue, 28 Jan 2025 16:20:44 +0100 Subject: [PATCH 036/147] Start Julia with 4 threads in the benchmark GH action --- .github/workflows/benchmarks_and_downstream.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmarks_and_downstream.yml b/.github/workflows/benchmarks_and_downstream.yml index 126d8a3c1..0b0db73b3 100644 --- a/.github/workflows/benchmarks_and_downstream.yml +++ b/.github/workflows/benchmarks_and_downstream.yml @@ -42,7 +42,7 @@ jobs: - name: Run benchmarks run: | cd test/downstream - julia --project --color=yes -e ' + julia --project --threads 4 --color=yes -e ' using Pkg; Pkg.instantiate(); include("test-all-benchmarks.jl")' From 5cee6a25b218f4fe34445b484e54982be2b88ebf Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Wed, 22 Jan 2025 16:35:29 +0100 Subject: [PATCH 037/147] Extra mapping with status vector test --- test/test-mapping.jl | 54 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/test/test-mapping.jl b/test/test-mapping.jl index b5921d9fe..364c8b24f 100755 --- a/test/test-mapping.jl +++ b/test/test-mapping.jl @@ -277,4 +277,58 @@ mapping_without_vectors = PlantSimEngine.replace_mapping_status_vectors_with_gen check=true, executor=SequentialEx() ) + + #replace a value with a constant vector and ensure no changes happen in the simulation + carbon_biomass_vec = Vector{Float64}(undef, nsteps) + for i in nsteps + carbon_biomass_vec[i] = 2.0 + end + mapping_with_two_vectors = Dict("Plant" => ( + MultiScaleModel( + model=ToyCAllocationModel(), + mapping=[ + # inputs + :carbon_assimilation => ["Leaf"], + :carbon_demand => ["Leaf", "Internode"], + # outputs + :carbon_allocation => ["Leaf", "Internode"] + ], + ), + MultiScaleModel( + model=ToyPlantRmModel(), + mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], + ), + ), + "Internode" => ( + ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), + ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004), + Status(TT=TT_v, carbon_biomass=1.0) + ), + "Leaf" => ( + MultiScaleModel( + model=ToyAssimModel(), + mapping=[:soil_water_content => "Soil",], + ), + ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), + ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), + Status(aPPFD=1300.0, carbon_biomass=carbon_biomass_vec, TT=10.0), # Replaced with vector here + ), + "Soil" => ( + ToySoilWaterModel(), + ), + ) + + mtg = import_mtg_example() + mapping_without_vectors_2 = PlantSimEngine.replace_mapping_status_vectors_with_generated_models(mapping_with_two_vectors, "Soil", nsteps) + graph_sim_multiscale_2 = @test_nowarn PlantSimEngine.GraphSimulation(mtg, mapping_without_vectors_2, nsteps=nsteps, check=true, outputs=out_multiscale) + + sim_multiscale_2 = run!(graph_sim_multiscale_2, + meteo_day, + PlantMeteo.Constants(), + nothing; + check=true, + executor=SequentialEx() + ) + + @test compare_outputs_graphsim(graph_sim_multiscale, graph_sim_multiscale_2) end \ No newline at end of file From 90b3a378924a67e46bcb3b35e2f024a615826c70 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Wed, 29 Jan 2025 08:52:11 +0100 Subject: [PATCH 038/147] Activate benchmark tracking for this branch --- .github/workflows/benchmarks_and_downstream.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmarks_and_downstream.yml b/.github/workflows/benchmarks_and_downstream.yml index 0b0db73b3..9ae3e49e1 100644 --- a/.github/workflows/benchmarks_and_downstream.yml +++ b/.github/workflows/benchmarks_and_downstream.yml @@ -3,7 +3,7 @@ on: push: branches: - main - - benchmarks-github-action + - Outputs-filtering2 tags: "*" workflow-dispatch: permissions: From a389b1feb14ba6ed769145bfe46232a848abbbf5 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Thu, 30 Jan 2025 14:36:48 +0100 Subject: [PATCH 039/147] Make run! return outputs filtered by the user. Struct is external to the modellist for now, and the API is hacked around a bit, placeholder code. --- src/component_models/ModelList.jl | 42 +++++++++++++++++++++-- src/mtg/save_results.jl | 57 +++++++++++++++++++++++++++++++ src/run.jl | 37 ++++++++++++++++---- test/test-ModelList.jl | 18 ++++++++++ 4 files changed, 146 insertions(+), 8 deletions(-) diff --git a/src/component_models/ModelList.jl b/src/component_models/ModelList.jl index 559148f45..ea852a560 100644 --- a/src/component_models/ModelList.jl +++ b/src/component_models/ModelList.jl @@ -177,20 +177,22 @@ julia> status(m) Note that computations will be slower using DataFrame, so if performance is an issue, use TimeStepTable instead (or a NamedTuple as shown in the example). """ -struct ModelList{M<:NamedTuple,S,V<:Tuple{Vararg{Symbol}}} +struct ModelList{M<:NamedTuple,S#=,O=#,V<:Tuple{Vararg{Symbol}}} models::M status::S + #outputs::O vars_not_propagated::V end function ModelList(models::M, status::S) where {M<:NamedTuple{names,T} where {names,T<:NTuple{N,<:AbstractModel} where {N}},S} - ModelList(models, status, ()) + ModelList(models, status, ())#outputs, ()) end # General interface: function ModelList( args...; status=nothing, + #outputs=nothing, init_fun::Function=init_fun_default, type_promotion::Union{Nothing,Dict}=nothing, variables_check::Bool=true, @@ -229,9 +231,43 @@ function ModelList( # Add the missing variables required by the models (set to default value): ts_kwargs = add_model_vars(ts_kwargs, mods, type_promotion; init_fun=init_fun, nsteps=nsteps) + #=user_outputs = outputs # todo : tuple to array + + # todo check outputs are all amongst the variables provided by the user and models + f = [] + + for i in 1:length(mods) + bb = keys(init_variables(mods[i])) + for j in 1:length(bb) + push!(f, bb[j]) + end + #f = (f..., bb...) + end + + f = unique!(f) + + # default implicit behaviour, track everything + if isnothing(user_outputs) + user_outputs = (f...,) + #all_vars = merge((keys(init_variables(object.models[i])) for i in 1:length(object.models))...) + else + unexpected_outputs = setdiff(user_outputs, f) + if !isempty(unexpected_outputs) + @error "Some output variable(s) requested was not found / were not found amongst the model variables : " + for i in 1:length(unexpected_outputs) + @error "$(unexpected_outputs[i])" + end + end + end=# + + # TODO preallocate outputs + #Dict(var => [typeof(status[var])[] for n in 1:nsteps] for var in user_outputs) + + model_list = ModelList( mods, ts_kwargs, + #user_outputs, vars_not_propagated ) variables_check && !is_initialized(model_list) @@ -239,6 +275,8 @@ function ModelList( return model_list end +outputs(m::ModelList) = m.outputs + parse_models(m) = NamedTuple([process(i) => i for i in m]) init_fun_default(x::Vector{T}) where {T} = TimeStepTable([Status(i) for i in x]) diff --git a/src/mtg/save_results.jl b/src/mtg/save_results.jl index 2c1afe8e9..65d5797d4 100644 --- a/src/mtg/save_results.jl +++ b/src/mtg/save_results.jl @@ -208,4 +208,61 @@ function save_results!(object::GraphSimulation, i) values[i] = [status[var] for status in statuses[organ]] end end +end + +function pre_allocate_outputs(m::ModelList, outs, nsteps; type_promotion=nothing, check=true) + + # NOTE : init_variables recreates a DependencyGraph, it's not great + # TODO : copy ? + out_vars_all = merge(init_variables(m; verbose=false)...) + + out_keys_requested = Symbol[] + if !isnothing(outs) + out_keys_requested = Symbol[outs...] + end + out_vars_requested = NamedTuple() + + # default implicit behaviour, track everything + if isempty(out_keys_requested) + #m.outputs = (keys(copy(status(m)))...) + out_vars_requested = out_vars_all + else + unexpected_outputs = setdiff(out_keys_requested, status_keys(status(m))) + + if !isempty(unexpected_outputs) + e = string( + "You requested as output ", + join(unexpected_outputs, ", "), + "which are not found in any model." + ) + + if check + error(e) + else + @info e + [delete!(unexpected_outputs, i) for i in unexpected_outputs] + end + end + + # get the default values from the tuple with all vars obtained from the modellist + #[out_vars_requested = (i => out_vars_all[i], out_vars_requested...) for i in out_keys_requested] + #for i in out_keys_requested + # merge(out_vars_requested, (i => out_vars_all[i],)) + #end + out_defaults_requested = (out_vars_all[i] for i in out_keys_requested) + out_vars_requested = (;zip(out_keys_requested, out_defaults_requested)...) + end + + outputs_timestep = fill(out_vars_requested, nsteps) + return TimeStepTable([Status(i) for i in outputs_timestep]) +end + +function save_results!(m::ModelList, outputs, i) + outs = outputs[i] + tst = status(m) + st_row = tst[1] + + for var in keys(outs) + outs[var] = st_row[var] + end end \ No newline at end of file diff --git a/src/run.jl b/src/run.jl index 53696add6..935957644 100644 --- a/src/run.jl +++ b/src/run.jl @@ -97,6 +97,7 @@ function run!( meteo=nothing, constants=PlantMeteo.Constants(), extra=nothing; + outputs=nothing, check=true, executor=ThreadedEx() ) @@ -107,6 +108,7 @@ function run!( meteo, constants, extra; + outputs, check, executor ) @@ -120,6 +122,7 @@ function run!( meteo::TimeStepTable{A}, constants=PlantMeteo.Constants(), extra=nothing; + outputs=nothing, check=true, executor=ThreadedEx() ) where {T<:Union{AbstractArray,AbstractDict},A} @@ -131,7 +134,8 @@ function run!( end for obj in collect(values(object)) - run!(obj, meteo, constants, extra, check=check, executor=executor) + outputs_obj = run!(obj, meteo, constants, extra, outputs=outputs, check=check, executor=executor) + # TODO end end @@ -144,9 +148,10 @@ function run!( meteo=nothing, constants=PlantMeteo.Constants(), extra=nothing; + outputs=nothing, check=true ) - run!(object, Weather[meteo], constants, extra; check) + run!(object, Weather[meteo], constants, extra; outputs, check) end # 3- one object, one meteo time-step, several status time-steps (rare case but possible) @@ -158,6 +163,7 @@ function run!( meteo=nothing, constants=PlantMeteo.Constants(), extra=nothing; + outputs=nothing, check=true, executor=ThreadedEx() ) where {T<:ModelList} @@ -171,6 +177,9 @@ function run!( ) end + nsteps = length(sim_rows) + outputs_preallocated = pre_allocate_outputs(object, outputs, nsteps) + if !timestep_parallelizable(dep_graph) if executor != SequentialEx() is_ts_parallel = which_timestep_parallelizable(dep_graph) @@ -189,7 +198,11 @@ function run!( roots = collect(dep_graph.roots) for (process, node) in roots run_node!(object, node, i, row, meteo, constants, extra) - end end + end + save_results!(object, outputs_preallocated, i) + end + + return outputs_preallocated else @floop executor for (i, row) in enumerate(sim_rows) local roots = collect(dep_graph.roots) @@ -208,6 +221,7 @@ function run!( meteo, constants=PlantMeteo.Constants(), extra=nothing; + outputs=nothing, check=true, executor=ThreadedEx() ) where {T<:ModelList} @@ -226,7 +240,7 @@ function run!( end end - if !timestep_parallelizable(dep_graph) + #if !timestep_parallelizable(dep_graph) if executor != SequentialEx() is_ts_parallel = which_timestep_parallelizable(dep_graph) mods_not_parallel = join([i.second.first for i in is_ts_parallel[findall(x -> x.second.second == false, is_ts_parallel)]], "; ") @@ -237,6 +251,9 @@ function run!( ) maxlog = 1 end + nsteps = length(meteo_rows) + outputs_preallocated = pre_allocate_outputs(object, outputs, nsteps) + # Not parallelizable over time-steps, it means some values depend on the previous value. # In this case we propagate the values of the variables from one time-step to the other, except for # the variables the user provided for all time-steps. @@ -247,8 +264,12 @@ function run!( for (process, node) in roots run_node!(object, node, i, object[i], meteo_i, constants, extra) end + save_results!(object, outputs_preallocated, i) end - else + + return outputs_preallocated + #=else + #TODO breakdown outputs and save them # Computing time-steps in parallel: @floop executor for (i, meteo_i) in enumerate(meteo_rows) local roots = collect(dep_graph.roots) @@ -256,7 +277,7 @@ function run!( run_node!(object, node, i, object[i], meteo_i, constants, extra) end end - end + end=# end # 5- several objects and one meteo time-step @@ -267,6 +288,7 @@ function run!( meteo, constants=PlantMeteo.Constants(), extra=nothing; + outputs=nothing, check=true, executor=ThreadedEx() ) where {T<:Union{AbstractArray,AbstractDict}} @@ -298,6 +320,7 @@ function run!( roots_i = collect(dep_graphs[i].roots) for (process_i, node_i) in roots_i run_node!(obj, node_i, 1, status(obj)[1], meteo, constants, extra) + # TODO save to outputs end end end @@ -372,6 +395,7 @@ function run!( meteo, constants=PlantMeteo.Constants(), extra=nothing; + outputs=nothing, check=true, executor=ThreadedEx() ) @@ -385,6 +409,7 @@ function run!( meteo, constants=PlantMeteo.Constants(), extra=nothing; + outputs=nothing, check=true, executor=ThreadedEx() ) diff --git a/test/test-ModelList.jl b/test/test-ModelList.jl index e69527314..5fefca7d7 100644 --- a/test/test-ModelList.jl +++ b/test/test-ModelList.jl @@ -205,4 +205,22 @@ end @test process3.children[1].value == Process5Model() @test isa(process3.children[1], PlantSimEngine.SoftDependencyNode) +end + +@testset "ModelList outputs preallocation" begin + meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) + vals = (var1=15.0, var2=0.3, TT_cu=cumsum(meteo_day.TT)) + leaf = ModelList( + process1=Process1Model(1.0), + process2=Process2Model(), + status=vals + ) + + outs=(:var3,) + out_out = PlantSimEngine.pre_allocate_outputs(leaf, outs, 1) + + out_out_2 = run!(leaf, meteo_day; outputs=outs, executor = SequentialEx()) + + + end \ No newline at end of file From b4daa02788e674b6dcfaf7868c6a086f5e2a82a3 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Thu, 30 Jan 2025 17:17:43 +0100 Subject: [PATCH 040/147] warning message typo fix --- src/mtg/save_results.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mtg/save_results.jl b/src/mtg/save_results.jl index 65d5797d4..e48aef44a 100644 --- a/src/mtg/save_results.jl +++ b/src/mtg/save_results.jl @@ -232,8 +232,8 @@ function pre_allocate_outputs(m::ModelList, outs, nsteps; type_promotion=nothing if !isempty(unexpected_outputs) e = string( "You requested as output ", - join(unexpected_outputs, ", "), - "which are not found in any model." + join(unexpected_outputs, " ,"), + " not found in any model." ) if check From 3c1bfd9c880e9ee140750d7e87986a70dee3dc49 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Thu, 30 Jan 2025 17:19:09 +0100 Subject: [PATCH 041/147] Basic helper functions that return a few commonly used modellists and meteos. Could easily be improved upon. Note that mixing them at random will cause many errors as the meteo sizes and status sizes won't match --- test/helper-functions.jl | 132 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/test/helper-functions.jl b/test/helper-functions.jl index ac83bff20..e645ee440 100644 --- a/test/helper-functions.jl +++ b/test/helper-functions.jl @@ -12,6 +12,17 @@ function compare_outputs_modellist_mapping(models, graphsim) return graphsim_df_outputs_only_sorted == models_df_sorted end +function compare_outputs_modellist_mapping(filtered_outputs, graphsim) + graphsim_df = outputs(graphsim, DataFrame) + + graphsim_df_outputs_only = select(graphsim_df, Not([:timestep, :organ, :node])) + models_df = DataFrame(filtered_outputs) + + models_df_sorted = models_df[:, sortperm(names(models_df))] + graphsim_df_outputs_only_sorted = graphsim_df_outputs_only[:, sortperm(names(graphsim_df_outputs_only))] + return graphsim_df_outputs_only_sorted == models_df_sorted +end + # doesn't check for mtg equality function compare_outputs_graphsim(graphsim, graphsim2) graphsim_df = outputs(graphsim, DataFrame) @@ -42,4 +53,125 @@ function check_multiscale_simulation_is_equivalent_end(models::ModelList, mtg, m ); return compare_outputs_modellist_mapping(models, graph_sim) +end + + +# Could make use of PlantMeteo's online meteo data recovery feature for more numerous examples +# or the random meteo generation used for the PBP benchmark + +#=using PlantMeteo, Dates, DataFrames +# Define the period of the simulation: +period = [Dates.Date("2021-01-01"), Dates.Date("2021-12-31")] +# Get the weather data for CIRAD's site in Montpellier, France: +meteo = get_weather(43.649777, 3.869889, period, sink = DataFrame)=# + +function get_simple_meteo_bank() + meteos= + [Atmosphere(T=20.0, Wind=1.0, P=101.3, Rh=0.65, Ri_PAR_f=300.0), + 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) + ]), + + Weather([Atmosphere(T=20.0, Wind=1.0, Rh=0.65, Ri_PAR_f=200.0), + Atmosphere(T=18.0, Wind=1.0, Rh=0.65, Ri_PAR_f=100.0), + Atmosphere(T=19.0, Wind=1.0, Rh=0.65, Ri_PAR_f=200.0), + Atmosphere(T=30.0, Wind=0.5, Rh=0.6, Ri_PAR_f=100.0), + Atmosphere(T=20.0, Wind=1.0, Rh=0.6, Ri_PAR_f=200.0), + Atmosphere(T=25.0, Wind=1.0, Rh=0.6, Ri_PAR_f=200.0), + Atmosphere(T=10.0, Wind=0.5, Rh=0.6, Ri_PAR_f=200.0)]), + + CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18), + + ] + return meteos +end + +function get_modellist_bank() + rue = 0.3 + + vals = (var1=15.0, var2=0.3, TT_cu=cumsum(meteo_day.TT)) + vals2 = (TT_cu=cumsum(meteo_day.TT),) + vals3 = (var1=15.0, var2=0.3) + + status_tuples = [vals, vals2, vals3, nothing, vals3, vals3] + + models = [ModelList( + process1=Process1Model(1.0), + process2=Process2Model(), + status=vals + ), + ModelList( + ToyLAIModel(), + Beer(0.5), + ToyRUEGrowthModel(rue), + status=vals2, + ), + + ModelList( + process1=Process1Model(1.0), + process2=Process2Model(), + process3=Process3Model(), + status=vals3 + ), + + ModelList( + process1=Process1Model(1.0), + process2=Process2Model(), + process3=Process3Model(), + process4=Process4Model(), + process5=Process5Model(), + process6=Process6Model(), + # process7=Process7Model(), + # status=(var1=15.0, var2=0.3) + ), + + ModelList( + process1=Process1Model(1.0), + process2=Process2Model(), + process3=Process3Model(), + process4=Process4Model(), + process5=Process5Model(), + process6=Process6Model(), + process7=Process7Model(), + status=(var1=15.0, var2=0.3) + ), + + ModelList( + process1=Process1Model(1.0), + process2=Process2Model(), + process3=Process3Model(), + process4=Process4Model(), + process5=Process5Model(), + status=(var1=15.0, var2=0.3) + ), + + ] + + outputs_tuples_vectors = + [ + # this one has one tuple with a duplicate, and one with a nonexistent variable + [NamedTuple(), (:var1,), (:var1, :var1), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var5), + (:var2, :var7, :var3, :var1), (:var1, :var2, :var3, :var4, :var5)], + + [NamedTuple(), (:TT_cu,), (:TT_cu,:LAI) , (:biomass,:LAI), (:TT_cu, :LAI, :PPFD, :biomass, :biomass_increment),], + + [NamedTuple(), (:var1,), (:var1, :var4), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), + (:var2, :var7, :var3, :var1), (:var1, :var2, :var3, :var4, :var5, :var6)], + + [NamedTuple(), (:var1,), (:var1, :var4), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), + (:var2, :var7, :var3, :var1), (:var1, :var2, :var3, :var4, :var5, :var6)], + + [NamedTuple(), (:var1,), (:var1, :var4), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), + (:var2, :var7, :var3, :var1), (:var1, :var2, :var3, :var4, :var5, :var6) + , (:var1, :var2, :var3, :var4, :var5, :var6, :var7, :var8, :var9)], + + [NamedTuple(), (:var1,), (:var1, :var1), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), + (:var2, :var7, :var3, :var1), (:var1, :var2, :var3, :var4, :var5, :var6) + , (:var1, :var2, :var3, :var4, :var5, :var6, :var7, :var8, :var9, :var0)], + + ] + + return models, status_tuples, outputs_tuples_vectors end \ No newline at end of file From 4da7c793a3e95511ddfcca5a0f3697a23457fce3 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Thu, 30 Jan 2025 17:20:04 +0100 Subject: [PATCH 042/147] Enhance the outputs filtering test (currently errors because some statuses and meteos don't match length properly) --- test/test-ModelList.jl | 78 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/test/test-ModelList.jl b/test/test-ModelList.jl index 5fefca7d7..bb2ca833d 100644 --- a/test/test-ModelList.jl +++ b/test/test-ModelList.jl @@ -1,5 +1,6 @@ # Tests: # Defining a list of models without status: +#= @testset "ModelList with no status" begin leaf = ModelList( process1=Process1Model(1.0), @@ -206,8 +207,57 @@ end @test process3.children[1].value == Process5Model() @test isa(process3.children[1], PlantSimEngine.SoftDependencyNode) end +=# -@testset "ModelList outputs preallocation" begin + + +# very naive function, doesn't generate full partition sets +# insert_errors : could duplicate a value, add a nonexistent one, make one the wrong type ? +#=function generate_output_tuple(vars_tuple, insert_errors, count) + + outputs_tuples_vector = [NamedTuple()] + + # number not exact, but trying every permutation sounds like a waste of time + for i in 1:max(count, length(vars_tuple)) + new_tuple = () + # TODO + new_tuple = (new_tuple..., new_var) + end + return outputs_tuples_vector +end=# + +function test_filtered_output(m::ModelList, status_tuple, requested_outputs, meteo) + + nsteps = isa(meteo, DataFrame) ? nrow(meteo) : length(meteo) + preallocated_outputs = PlantSimEngine.pre_allocate_outputs(m, requested_outputs, nsteps) + @test length(preallocated_outputs) == nsteps + if length(requested_outputs) > 0 + @test length(preallocated_outputs[1]) == length(requested_outputs) + else + # don't compare with the status because unnecessary variables in the status are discarded in the filtered outputs + out_vars_all = merge(init_variables(m; verbose=false)...) + println(out_vars_all) + @test length(preallocated_outputs[1]) == length(out_vars_all) + end + + filtered_outputs_modellist = run!(m, meteo; outputs=requested_outputs, executor = SequentialEx()) + + # compare filtered output of a modellist with the filtered output of the equivalent simulation in multiscale mode + mtg, mapping, outputs_mapping = PlantSimEngine.modellist_to_mapping(m, status_tuple; nsteps=nsteps, outputs=requested_outputs) + graphsim = PlantSimEngine.GraphSimulation(mtg, mapping, nsteps=nsteps, check=true, outputs=outputs_mapping) + + sim2 = run!(graphsim, + meteo, + PlantMeteo.Constants(), + nothing; + check=true, + executor=SequentialEx() + ) + return compare_outputs_modellist_mapping(filtered_outputs_modellist, graphsim) +end + +# TODO restrict status to a single Status +#@testset "ModelList outputs preallocation" begin meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) vals = (var1=15.0, var2=0.3, TT_cu=cumsum(meteo_day.TT)) leaf = ModelList( @@ -215,12 +265,30 @@ end process2=Process2Model(), status=vals ) - outs=(:var3,) - out_out = PlantSimEngine.pre_allocate_outputs(leaf, outs, 1) - out_out_2 = run!(leaf, meteo_day; outputs=outs, executor = SequentialEx()) + @test test_filtered_output(leaf, vals, outs, meteo_day) + + meteos = get_simple_meteo_bank() + modellists, status_tuples, outs_vectors = get_modellist_bank() + for i in 1:length(modellists) + modellist = modellists[i] + status_tuple = status_tuples[i] + outs_vector = outs_vectors[i] + all_vars = init_variables(modellist) + + #insert_errors = true + #outs_vector = generate_output_tuple(all_vars, insert_errors) + + for meteo in meteos + for out_tuple in outs_vector + println(out_tuple) + @test test_filtered_output(modellist, status_tuple, out_tuple, meteo) + end + end + end -end \ No newline at end of file + @enter test_filtered_output(modellists[2], status_tuples[2], outs_vectors[2][2], meteos[1]) +#end \ No newline at end of file From fc891941942ee798235ee6c8a5ed0b5faa0c8dc9 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Fri, 31 Jan 2025 17:58:22 +0100 Subject: [PATCH 043/147] Change modellist status to be a single status and not a timesteptable. Vector variables pass in their current value to a Status that is adjusted on the fly for each timestep. This is a breaking change that requires loads of test adjustment (and will break PBP further down the line). Current state : most tests fixed but not all. run! structure also affected. Some code simplified, overall. --- src/checks/dimensions.jl | 28 +++++--- src/component_models/ModelList.jl | 63 ++++++----------- src/component_models/Status.jl | 69 +++++++++---------- src/mtg/save_results.jl | 6 +- src/run.jl | 108 ++++++++++++++---------------- src/traits/table_traits.jl | 3 +- test/helper-functions.jl | 58 +++++++++++----- test/test-ModelList.jl | 68 ++++++------------- test/test-Status.jl | 44 ++++++------ test/test-dimensions.jl | 16 ++--- test/test-fitting.jl | 2 +- test/test-mapping.jl | 6 +- test/test-simulation.jl | 55 +++++++-------- test/test-toy_models.jl | 40 +++++------ 14 files changed, 267 insertions(+), 299 deletions(-) diff --git a/src/checks/dimensions.jl b/src/checks/dimensions.jl index ee6f6373f..b6da3d148 100644 --- a/src/checks/dimensions.jl +++ b/src/checks/dimensions.jl @@ -40,7 +40,7 @@ PlantSimEngine.check_dimensions(models, w) ERROR: DimensionMismatch: Component status should have the same number of time-steps (2) than weather data (3). ``` """ -check_dimensions(component, weather) = check_dimensions(DataFormat(component), DataFormat(weather), component, weather) +check_dimensions(component, weather) = check_dimensions(DataFormat(weather), component, weather) # Here we add methods for applying to a component, an array or a dict of: function check_dimensions(component::T, w) where {T<:ModelList} @@ -62,19 +62,31 @@ function check_dimensions(component::T, weather) where {T<:AbstractDict{N,<:Mode end -function check_dimensions(::TableAlike, ::TableAlike, st, weather) - length(st) > 1 && length(st) != length(Tables.rows(weather)) && - throw(DimensionMismatch("Component status should have the same number of time-steps ($(length(st))) than weather data ($(length(weather))).")) - return nothing -end +# TODO multi timestep handling # A Status (one time-step) is always authorized with a Weather (it is recycled). # The status is updated at each time-step, but no intermediate saving though! -function check_dimensions(::SingletonAlike, ::TableAlike, st, weather) +function check_dimensions(::TableAlike, st::Status, weather) + weather_len = get_nsteps(weather) + + for (var, value) in zip(keys(st), st) + if length(value) > 1 + if length(value) != weather_len + throw(DimensionMismatch("Component status has a vector variable : $(var) of length $(length(value)) but the weather data expects $(weather_len) timesteps.")) + end + end + end + return nothing end -function check_dimensions(s, ::SingletonAlike, st, weather) +function check_dimensions(::SingletonAlike, st::Status, weather) + for (var, value) in zip(keys(st), st) + if length(value) > 1 + throw(DimensionMismatch("Component status has a vector variable : $(var) implying multiple timesteps but weather data only provides a single timestep.")) + end + end + return nothing end diff --git a/src/component_models/ModelList.jl b/src/component_models/ModelList.jl index ea852a560..b227f49ce 100644 --- a/src/component_models/ModelList.jl +++ b/src/component_models/ModelList.jl @@ -3,7 +3,6 @@ ModelList(models::M, status::S) ModelList(; status=nothing, - init_fun::Function=init_fun_default, type_promotion=nothing, variables_check=true, kwargs... @@ -23,8 +22,6 @@ type promotion, time steps handling. implements `getproperty`. - `status`: a structure containing the initializations for the variables of the models. Usually a NamedTuple when given as a kwarg, or any structure that implements the Tables interface from `Tables.jl` (*e.g.* DataFrame, see details). -- `nsteps=nothing`: the number of time steps to pre-allocated. If `nothing`, the number of time steps is deduced from the status (or 1 if no status is given). -- `init_fun`: a function that initializes the status based on a vector of NamedTuples (see details). - `type_promotion`: optional type conversion for the variables with default values. `nothing` by default, *i.e.* no conversion. Note that conversion is not applied to the variables input by the user as `kwargs` (need to do it manually). @@ -34,13 +31,6 @@ Should be provided as a Dict with current type as keys and new type as values. # Details -The argument `init_fun` is set by default to `init_fun_default` which initializes the status with a `TimeStepTable` -of `Status` structures. - -If you change `init_fun` by another function, make sure the type you are using (*i.e.* in place of `TimeStepTable`) -implements the `Tables.jl` interface (*e.g.* DataFrame does). And if you still use `TimeStepTable` but only change -`Status`, make sure the type you give is indexable using the dot synthax (*e.g.* `x.var`). - If you need to input a custom Type for the status and make your users able to only partially initialize the `status` field in the input, you'll have to implement a method for `add_model_vars!`, a function that adds the models variables to the type in case it is not fully initialized. The default method is compatible @@ -179,12 +169,12 @@ TimeStepTable instead (or a NamedTuple as shown in the example). """ struct ModelList{M<:NamedTuple,S#=,O=#,V<:Tuple{Vararg{Symbol}}} models::M - status::S + status::Status{S} #outputs::O vars_not_propagated::V end -function ModelList(models::M, status::S) where {M<:NamedTuple{names,T} where {names,T<:NTuple{N,<:AbstractModel} where {N}},S} +function ModelList(models::M, status::Status) where {M<:NamedTuple{names,T} where {names,T<:NTuple{N,<:AbstractModel} where {N}}} ModelList(models, status, ())#outputs, ()) end @@ -193,7 +183,6 @@ function ModelList( args...; status=nothing, #outputs=nothing, - init_fun::Function=init_fun_default, type_promotion::Union{Nothing,Dict}=nothing, variables_check::Bool=true, nsteps=nothing, @@ -220,16 +209,16 @@ function ModelList( mods = merge(args, kwargs) # Make a vector of NamedTuples from the input (please implement yours if you need it) - ts_kwargs = homogeneous_ts_kwargs(status, nsteps) + ts_kwargs = homogeneous_ts_kwargs(status) # Variables for which a value was given for each time-step by the user: - vars_not_propagated = get_vars_not_propagated(status) + vector_vars = get_vars_not_propagated(status) # Note: that the length was checked in homogeneous_ts_kwargs, so we don't need to check it again here. # Note 2: we need to know these variables because they will not be propagated between time-steps, but set at # the given value instead. # Add the missing variables required by the models (set to default value): - ts_kwargs = add_model_vars(ts_kwargs, mods, type_promotion; init_fun=init_fun, nsteps=nsteps) + ts_kwargs = add_model_vars(ts_kwargs, mods, type_promotion) #=user_outputs = outputs # todo : tuple to array @@ -268,7 +257,7 @@ function ModelList( mods, ts_kwargs, #user_outputs, - vars_not_propagated + vector_vars ) variables_check && !is_initialized(model_list) @@ -294,7 +283,7 @@ any Tables.jl-compatible `x` and for NamedTuples. Careful, the function makes a copy of the input `x` if it does not list all needed variables. """ -function add_model_vars(x, models, type_promotion; init_fun=init_fun_default, nsteps=nothing) +function add_model_vars(x, models, type_promotion) ref_vars = merge(init_variables(models; verbose=false)...) # If no variable is required, we return the input: length(ref_vars) == 0 && return x @@ -309,29 +298,17 @@ function add_model_vars(x, models, type_promotion; init_fun=init_fun_default, ns ref_vars = convert_vars(ref_vars, type_promotion) # If the user gave an empty status, we initialize all variables to their default values: - if x === nothing || (!Tables.istable(x) && length(x) == 0) - if nsteps === nothing - return init_fun(ref_vars) - else - return init_fun(fill(ref_vars, nsteps)) - end + if x === nothing + return Status(ref_vars) end - if Tables.istable(x) - # Making a vars for each ith value in the user vars: - x_full = [merge(ref_vars, NamedTuple(Tables.rows(x)[1]))] - for r in Tables.rows(x)[2:end] - push!(x_full, merge(ref_vars, NamedTuple(r))) - end - else - x_full = merge(ref_vars, NamedTuple(x)) - end + x_full = merge(ref_vars, NamedTuple(x)) - return init_fun(x_full) + return Status(x_full) end function status_keys(st) - Tables.istable(st) && return Tables.columnnames(st) + #Tables.istable(st) && return Tables.columnnames(st) return keys(st) end @@ -342,7 +319,7 @@ function add_model_vars(x::Nothing, models, type_promotion) ref_vars = merge(init_variables(models; verbose=false)...) length(ref_vars) == 0 && return x # Convert model variables types to the one required by the user: - return convert_vars(ref_vars, type_promotion) + return Status(convert_vars(ref_vars, type_promotion)) end """ @@ -350,7 +327,7 @@ end By default, the function returns its argument. """ -homogeneous_ts_kwargs(kwargs, nsteps) = kwargs +homogeneous_ts_kwargs(kwargs) = kwargs """ kwargs_to_timestep(kwargs::NamedTuple{N,T}) where {N,T} @@ -365,28 +342,28 @@ It is used to be able to *e.g.* give constant values for all time-steps for one PlantSimEngine.homogeneous_ts_kwargs((Tₗ=[25.0, 26.0], aPPFD=1000.0)) ``` """ -function homogeneous_ts_kwargs(kwargs::NamedTuple{N,T}, nsteps) where {N,T} +function homogeneous_ts_kwargs(kwargs::NamedTuple{N,T}) where {N,T} length(kwargs) == 0 && return kwargs vars_vals = collect(Any, values(kwargs)) - length_vars = [isa(i, RefVector) ? 1 : length(i) for i in vars_vals] + #length_vars = [isa(i, RefVector) ? 1 : length(i) for i in vars_vals] #Note: length is 1 for RefVector because it is a vector of references to other scales, # not a vector of values # One of the variable is given as an array, meaning this is actually several # time-steps. In this case we make an array of vars. - max_length_st = nsteps !== nothing ? nsteps : maximum(length_vars) + max_length_st = 1#nsteps !== nothing ? nsteps : maximum(length_vars) - for i in eachindex(vars_vals) + #=for i in eachindex(vars_vals) # If the ith vars has length one, repeat its value to match the max time-steps: if length_vars[i] == 1 vars_vals[i] = repeat([vars_vals[i]], max_length_st) else length_vars[i] != max_length_st && @error "$(keys(kwargs)[i]) should be length $max_length_st or 1" end - end + end=# # Making a vars for each ith value in the user vars: - vars_array = NamedTuple[NamedTuple{keys(kwargs)}(j[i] for j in vars_vals) for i in 1:max_length_st] + vars_array = NamedTuple{keys(kwargs)}(j for j in vars_vals) return vars_array end diff --git a/src/component_models/Status.jl b/src/component_models/Status.jl index eed425a44..0a1c7f906 100644 --- a/src/component_models/Status.jl +++ b/src/component_models/Status.jl @@ -133,46 +133,37 @@ function Base.:(==)(s1::Status, s2::Status) end -""" - propagate_values!(status1::Dict, status2::Dict, vars_not_propagated::Set) - -Propagates the values of all variables in `status1` to `status2`, except for vars in `vars_not_propagated`. - -# Arguments - -- `status1::Dict`: A dictionary containing the current values of variables. -- `status2::Dict`: A dictionary to which the values of variables will be propagated. -- `vars_not_propagated::Set`: A set of variables whose values should not be propagated. - -# Examples - -```jldoctest st1 -julia> status1 = Status(var1 = 15.0, var2 = 0.3); -``` - -```jldoctest st1 -julia> status2 = Status(var1 = 16.0, var2 = -Inf); -``` - -```jldoctest st1 -julia> vars_not_propagated = (:var1,); - -```jldoctest st1 -julia> PlantSimEngine.propagate_values!(status1, status2, vars_not_propagated); -``` +# Returns a status with all vector variables replaced with their first value (ie a Status ready for simulation) +# also returns a tuple of symbols corresponding to the vector variables +function flatten_status(s::Status) + status_values_flattened = NamedTuple() + vector_variables = NamedTuple() + + for (var, value) in zip(keys(s), s) + if length(value) > 1 + vector_variables = (vector_variables..., var) + status_values_flattened = (status_values_flattened..., value[1]) + else + status_values_flattened = (status_values_flattened..., value) + end + end -```jldoctest st1 -julia> status2.var2 == status1.var2 -true -``` + return Status(;zip(keys(s), status_values_flattened)...), vector_variables +end -```jldoctest st1 -julia> status2.var1 == status1.var1 -false -``` -""" -function propagate_values!(status1, status2, vars_not_propagated) - for var in setdiff(keys(status1), vars_not_propagated) - status2[var] = status1[var] +# Update to the next timestep the variables that were passed in as vectors by the user +function update_vector_variables(s::Status, sf::Status, vector_variables, i) + for vec in vector_variables + sf[vec] = s[vec][i] end end + +function get_status_vector_max_length(s::Status) + max_len = 1 + for (var, value) in zip(keys(s), s) + if length(value) > 1 + max_len = length(value) + end + end + return max_len +end \ No newline at end of file diff --git a/src/mtg/save_results.jl b/src/mtg/save_results.jl index e48aef44a..b93ec0aa2 100644 --- a/src/mtg/save_results.jl +++ b/src/mtg/save_results.jl @@ -257,12 +257,10 @@ function pre_allocate_outputs(m::ModelList, outs, nsteps; type_promotion=nothing return TimeStepTable([Status(i) for i in outputs_timestep]) end -function save_results!(m::ModelList, outputs, i) +function save_results!(status_flattened::Status, outputs, i) outs = outputs[i] - tst = status(m) - st_row = tst[1] for var in keys(outs) - outs[var] = st_row[var] + outs[var] = status_flattened[var] end end \ No newline at end of file diff --git a/src/run.jl b/src/run.jl index 935957644..508317fdd 100644 --- a/src/run.jl +++ b/src/run.jl @@ -86,6 +86,20 @@ julia> (models[:var4],models[:var6]) """ run! +function adjust_weather_timesteps_to_status_length(st::Status, meteo) + status_timesteps_len = get_status_vector_max_length(st) + meteo_adjusted = meteo + + if isnothing(meteo) + meteo_adjusted = Weather(repeat([Atmosphere(NamedTuple())], status_timesteps_len)) + elseif get_nsteps(meteo) == 1 + meteo_adjusted = Weather(repeat([meteo], status_timesteps_len)) + end + + return meteo_adjusted +end + + # Managing one or several objects, one or several time-steps: # This is the default function called by the user, which uses traits @@ -103,7 +117,6 @@ function run!( ) run!( DataFormat(object), - DataFormat(meteo), object, meteo, constants, @@ -116,7 +129,6 @@ end # 1- several objects and several time-steps function run!( - ::TableAlike, ::TableAlike, object::T, meteo::TimeStepTable{A}, @@ -133,31 +145,16 @@ function run!( ) maxlog = 1 end + outputs_vector = [] for obj in collect(values(object)) - outputs_obj = run!(obj, meteo, constants, extra, outputs=outputs, check=check, executor=executor) - # TODO + push!(outputs_vector, run!(obj, meteo, constants, extra, outputs=outputs, check=check, executor=executor)) end -end - - -# 2- one object, one time-step -function run!( - ::SingletonAlike, - ::SingletonAlike, - object, - meteo=nothing, - constants=PlantMeteo.Constants(), - extra=nothing; - outputs=nothing, - check=true -) - run!(object, Weather[meteo], constants, extra; outputs, check) + return outputs_vector end # 3- one object, one meteo time-step, several status time-steps (rare case but possible) # Also occurs when meteo is nothing -function run!( - ::TableAlike, +#=function run!( ::SingletonAlike, object::T, meteo=nothing, @@ -167,7 +164,7 @@ function run!( check=true, executor=ThreadedEx() ) where {T<:ModelList} - sim_rows = Tables.rows(status(object)) +sim_rows = Tables.rows(status(object)) dep_graph = dep(object, length(sim_rows)) if check && length(dep_graph.not_found) > 0 @@ -179,8 +176,9 @@ function run!( nsteps = length(sim_rows) outputs_preallocated = pre_allocate_outputs(object, outputs, nsteps) + status_flattened, vector_variables = flatten_status(object.status) - if !timestep_parallelizable(dep_graph) + #if !timestep_parallelizable(dep_graph) if executor != SequentialEx() is_ts_parallel = which_timestep_parallelizable(dep_graph) mods_not_parallel = join([i.second.first for i in is_ts_parallel[findall(x -> x.second.second == false, is_ts_parallel)]], "; ") @@ -197,9 +195,10 @@ function run!( i > 1 && propagate_values!(sim_rows[i-1], row, object.vars_not_propagated) roots = collect(dep_graph.roots) for (process, node) in roots - run_node!(object, node, i, row, meteo, constants, extra) + run_node!(object, node, i, row, nothing, constants, extra) end - save_results!(object, outputs_preallocated, i) + save_results!(status_flattened, outputs_preallocated, i) + i+1 <= nsteps && update_vector_variables(object.status, status_flattened, vector_variables, i + 1) end return outputs_preallocated @@ -211,26 +210,28 @@ function run!( end end end -end +end=# # 4- one object, several meteo time-step, several status time-steps function run!( - ::TableAlike, - ::TableAlike, + ::SingletonAlike, object::T, - meteo, + meteo=nothing, constants=PlantMeteo.Constants(), extra=nothing; outputs=nothing, check=true, executor=ThreadedEx() ) where {T<:ModelList} - meteo_rows = Tables.rows(meteo) + + meteo_adjusted = adjust_weather_timesteps_to_status_length(object.status, meteo) + + meteo_rows = Tables.rows(meteo_adjusted) dep_graph = dep(object, length(meteo_rows)) if check # Check if the meteo data and the status have the same length (or length 1) - check_dimensions(object, meteo) + check_dimensions(object, meteo_adjusted) if length(dep_graph.not_found) > 0 error( @@ -253,6 +254,7 @@ function run!( nsteps = length(meteo_rows) outputs_preallocated = pre_allocate_outputs(object, outputs, nsteps) + status_flattened, vector_variables = flatten_status(object.status) # Not parallelizable over time-steps, it means some values depend on the previous value. # In this case we propagate the values of the variables from one time-step to the other, except for @@ -260,11 +262,11 @@ function run!( roots = collect(dep_graph.roots) for (i, meteo_i) in enumerate(meteo_rows) - i > 1 && propagate_values!(object[i-1], object[i], object.vars_not_propagated) for (process, node) in roots - run_node!(object, node, i, object[i], meteo_i, constants, extra) + run_node!(object, node, i, status_flattened, meteo_i, constants, extra) end - save_results!(object, outputs_preallocated, i) + save_results!(status_flattened, outputs_preallocated, i) + i+1 <= nsteps && update_vector_variables(object.status, status_flattened, vector_variables, i + 1) end return outputs_preallocated @@ -283,7 +285,6 @@ end # 5- several objects and one meteo time-step function run!( ::TableAlike, - ::SingletonAlike, object::T, meteo, constants=PlantMeteo.Constants(), @@ -294,7 +295,7 @@ function run!( ) where {T<:Union{AbstractArray,AbstractDict}} dep_graphs = [dep(obj) for obj in collect(values(object))] - obj_parallelizable = all([object_parallelizable(graph) for graph in dep_graphs]) + #obj_parallelizable = all([object_parallelizable(graph) for graph in dep_graphs]) # Check if the simulation can be parallelized over objects: if executor != SequentialEx() @@ -302,9 +303,12 @@ function run!( "Parallelisation over objects was removed, (but may be reintroduced in the future). Parallelisation will only occur over timesteps." ) maxlog = 1 end + + outputs_vector = [] + # Each object: for (i, obj) in enumerate(collect(values(object))) - + if check # Check if the meteo data and the status have the same length (or length 1) check_dimensions(obj, meteo) @@ -316,13 +320,9 @@ function run!( ) end end - - roots_i = collect(dep_graphs[i].roots) - for (process_i, node_i) in roots_i - run_node!(obj, node_i, 1, status(obj)[1], meteo, constants, extra) - # TODO save to outputs - end + push!(outputs_vector, run!(obj, meteo, constants, extra, outputs=outputs, check=check, executor=executor)) end + return outputs_vector end @@ -375,10 +375,15 @@ function run!( ) isnothing(nsteps) && (nsteps = get_nsteps(meteo)) + meteo_adjusted = meteo + if nsteps == 1 + meteo_adjusted = Weather([meteo]) + end + sim = GraphSimulation(object, mapping, nsteps=nsteps, check=check, outputs=outputs) run!( sim, - meteo, + meteo_adjusted, constants, extra; check=check, @@ -390,21 +395,6 @@ end function run!( ::TreeAlike, - ::SingletonAlike, - object::GraphSimulation, - meteo, - constants=PlantMeteo.Constants(), - extra=nothing; - outputs=nothing, - check=true, - executor=ThreadedEx() -) - run!(object, Weather[meteo], constants, extra, check, executor) -end - -function run!( - ::TreeAlike, - ::TableAlike, object::GraphSimulation, meteo, constants=PlantMeteo.Constants(), diff --git a/src/traits/table_traits.jl b/src/traits/table_traits.jl index 39c1c98e6..f9194c44d 100644 --- a/src/traits/table_traits.jl +++ b/src/traits/table_traits.jl @@ -56,8 +56,7 @@ DataFormat(::Type{<:Dict}) = TableAlike() DataFormat(::Type{<:NamedTuple}) = SingletonAlike() DataFormat(::Type{<:Status}) = SingletonAlike() -DataFormat(::Type{<:ModelList{Mo,S,V} where {Mo,S<:Status,V}}) = SingletonAlike() -DataFormat(::Type{<:ModelList{Mo,S,V}}) where {Mo,S,V} = TableAlike() +DataFormat(::Type{<:ModelList{Mo,S,V} where {Mo,S,V}}) = SingletonAlike() DataFormat(::Type{<:GraphSimulation}) = TreeAlike() DataFormat(::Type{<:PlantMeteo.AbstractAtmosphere}) = SingletonAlike() diff --git a/test/helper-functions.jl b/test/helper-functions.jl index e645ee440..58a91e28a 100644 --- a/test/helper-functions.jl +++ b/test/helper-functions.jl @@ -1,17 +1,5 @@ # Simple helper functions that can be used in various tests here and there - -function compare_outputs_modellist_mapping(models, graphsim) - graphsim_df = outputs(graphsim, DataFrame) - - graphsim_df_outputs_only = select(graphsim_df, Not([:timestep, :organ, :node])) - models_df = DataFrame(status(models)) - - models_df_sorted = models_df[:, sortperm(names(models_df))] - graphsim_df_outputs_only_sorted = graphsim_df_outputs_only[:, sortperm(names(graphsim_df_outputs_only))] - return graphsim_df_outputs_only_sorted == models_df_sorted -end - function compare_outputs_modellist_mapping(filtered_outputs, graphsim) graphsim_df = outputs(graphsim, DataFrame) @@ -41,7 +29,7 @@ function check_multiscale_simulation_is_equivalent_begin(models::ModelList, stat return mtg, mapping, out end -function check_multiscale_simulation_is_equivalent_end(models::ModelList, mtg, mapping, out, meteo) +function check_multiscale_simulation_is_equivalent_end(modellist_outputs, mtg, mapping, out, meteo) graph_sim = PlantSimEngine.GraphSimulation(mtg, mapping, nsteps=length(meteo), check=true, outputs=out) sim = run!(graph_sim, @@ -52,7 +40,7 @@ function check_multiscale_simulation_is_equivalent_end(models::ModelList, mtg, m executor=SequentialEx() ); - return compare_outputs_modellist_mapping(models, graph_sim) + return compare_outputs_modellist_mapping(modellist_outputs, graph_sim) end @@ -91,7 +79,7 @@ end function get_modellist_bank() rue = 0.3 - vals = (var1=15.0, var2=0.3, TT_cu=cumsum(meteo_day.TT)) + vals = (var1=15.0, var2=0.3)#, TT_cu=cumsum(meteo_day.TT)) vals2 = (TT_cu=cumsum(meteo_day.TT),) vals3 = (var1=15.0, var2=0.3) @@ -152,7 +140,7 @@ function get_modellist_bank() outputs_tuples_vectors = [ # this one has one tuple with a duplicate, and one with a nonexistent variable - [NamedTuple(), (:var1,), (:var1, :var1), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var5), + [(:var1,), (:var1, :var1), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var5), (:var2, :var7, :var3, :var1), (:var1, :var2, :var3, :var4, :var5)], [NamedTuple(), (:TT_cu,), (:TT_cu,:LAI) , (:biomass,:LAI), (:TT_cu, :LAI, :PPFD, :biomass, :biomass_increment),], @@ -174,4 +162,42 @@ function get_modellist_bank() ] return models, status_tuples, outputs_tuples_vectors +end + +# Split into two parts to ensure eval() syncs and that automatic generation becomes visible for later simulation +# See world-age problems and comments around modellist_to_mapping if you don't know/remember what that's about +function test_filtered_output_begin(m::ModelList, status_tuple, requested_outputs, meteo) + + meteo_adjusted = PlantSimEngine.adjust_weather_timesteps_to_status_length(m.status, meteo) + nsteps = PlantSimEngine.get_nsteps(meteo_adjusted) + preallocated_outputs = PlantSimEngine.pre_allocate_outputs(m, requested_outputs, nsteps) + @test length(preallocated_outputs) == nsteps + if length(requested_outputs) > 0 + @test length(preallocated_outputs[1]) == length(requested_outputs) + else + # don't compare with the status because unnecessary variables in the status are discarded in the filtered outputs + out_vars_all = merge(init_variables(m; verbose=false)...) + println(out_vars_all) + @test length(preallocated_outputs[1]) == length(out_vars_all) + end + + filtered_outputs_modellist = run!(m, meteo; outputs=requested_outputs, executor = SequentialEx()) + + # compare filtered output of a modellist with the filtered output of the equivalent simulation in multiscale mode + mtg, mapping, outputs_mapping = PlantSimEngine.modellist_to_mapping(m, status_tuple; nsteps=nsteps, outputs=requested_outputs) + + return mtg, mapping, outputs_mapping, nsteps, filtered_outputs_modellist +end + +function test_filtered_output(mtg, mapping, nsteps, outputs_mapping, meteo, filtered_outputs_modellist) + graphsim = PlantSimEngine.GraphSimulation(mtg, mapping, nsteps=nsteps, check=true, outputs=outputs_mapping) + + sim2 = run!(graphsim, + meteo, + PlantMeteo.Constants(), + nothing; + check=true, + executor=SequentialEx() + ) + return compare_outputs_modellist_mapping(filtered_outputs_modellist, graphsim) end \ No newline at end of file diff --git a/test/test-ModelList.jl b/test/test-ModelList.jl index bb2ca833d..eb55ed820 100644 --- a/test/test-ModelList.jl +++ b/test/test-ModelList.jl @@ -1,6 +1,6 @@ # Tests: # Defining a list of models without status: -#= + @testset "ModelList with no status" begin leaf = ModelList( process1=Process1Model(1.0), @@ -12,7 +12,7 @@ @test all(getproperty(leaf.status, i)[1] == getproperty(st, i) for i in keys(st)) @test !is_initialized(leaf) @test to_initialize(leaf) == (process1=(:var1, :var2), process2=(:var1,)) - @test length(status(leaf)) == 1 + @test length(status(leaf)) == 5 # Requiring 3 steps for initialization: leaf = ModelList( @@ -21,8 +21,8 @@ nsteps=3 ) - @test length(status(leaf)) == 3 - @test status(leaf, :var1) == [-Inf, -Inf, -Inf] + @test length(status(leaf)) == 5 + @test status(leaf, :var1) == -Inf end; @@ -84,8 +84,8 @@ end; nsteps=3 ) - @test length(status(leaf)) == 3 - @test status(leaf, :var1) == [15.0, 15.0, 15.0] + @test length(status(leaf)) == 5 + @test status(leaf, :var1) == 15.0 end; @testset "ModelList with fully initialized status" begin @@ -140,13 +140,13 @@ end; # Copy the model list: ml2 = copy(models) - @test DataFrame(status(ml2)) == DataFrame(status(models)) + @test DataFrame(TimeStepTable([status(ml2)])) == DataFrame(TimeStepTable([status(models)])) # Copy the model list with new status: - tst = TimeStepTable([Status(var1=20.0, var2=0.5)]) - ml3 = copy(models, tst) + st = Status(var1=20.0, var2=0.5) + ml3 = copy(models, st) - @test status(ml3) == tst + @test status(ml3) == st @test ml3.models == models.models @@ -207,7 +207,7 @@ end @test process3.children[1].value == Process5Model() @test isa(process3.children[1], PlantSimEngine.SoftDependencyNode) end -=# + @@ -226,49 +226,21 @@ end return outputs_tuples_vector end=# -function test_filtered_output(m::ModelList, status_tuple, requested_outputs, meteo) - - nsteps = isa(meteo, DataFrame) ? nrow(meteo) : length(meteo) - preallocated_outputs = PlantSimEngine.pre_allocate_outputs(m, requested_outputs, nsteps) - @test length(preallocated_outputs) == nsteps - if length(requested_outputs) > 0 - @test length(preallocated_outputs[1]) == length(requested_outputs) - else - # don't compare with the status because unnecessary variables in the status are discarded in the filtered outputs - out_vars_all = merge(init_variables(m; verbose=false)...) - println(out_vars_all) - @test length(preallocated_outputs[1]) == length(out_vars_all) - end - - filtered_outputs_modellist = run!(m, meteo; outputs=requested_outputs, executor = SequentialEx()) - - # compare filtered output of a modellist with the filtered output of the equivalent simulation in multiscale mode - mtg, mapping, outputs_mapping = PlantSimEngine.modellist_to_mapping(m, status_tuple; nsteps=nsteps, outputs=requested_outputs) - graphsim = PlantSimEngine.GraphSimulation(mtg, mapping, nsteps=nsteps, check=true, outputs=outputs_mapping) - - sim2 = run!(graphsim, - meteo, - PlantMeteo.Constants(), - nothing; - check=true, - executor=SequentialEx() - ) - return compare_outputs_modellist_mapping(filtered_outputs_modellist, graphsim) -end # TODO restrict status to a single Status -#@testset "ModelList outputs preallocation" begin +@testset "ModelList outputs preallocation" begin meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) vals = (var1=15.0, var2=0.3, TT_cu=cumsum(meteo_day.TT)) - leaf = ModelList( + leaf = ModelList( process1=Process1Model(1.0), process2=Process2Model(), status=vals ) outs=(:var3,) - @test test_filtered_output(leaf, vals, outs, meteo_day) - + mtg, mapping, outputs_mapping, nsteps, filtered_outputs_modellist = test_filtered_output_begin(leaf, vals, outs, meteo_day) + @test test_filtered_output(mtg, mapping, nsteps, outputs_mapping, meteo_day, filtered_outputs_modellist) + meteos = get_simple_meteo_bank() modellists, status_tuples, outs_vectors = get_modellist_bank() @@ -285,10 +257,12 @@ end for meteo in meteos for out_tuple in outs_vector println(out_tuple) - @test test_filtered_output(modellist, status_tuple, out_tuple, meteo) + mtg, mapping, outputs_mapping, nsteps, filtered_outputs_modellist = test_filtered_output_begin(modellist, status_tuple, out_tuple, meteo) + @test test_filtered_output(mtg, mapping, nsteps, outputs_mapping, meteo, filtered_outputs_modellist) end end end - @enter test_filtered_output(modellists[2], status_tuples[2], outs_vectors[2][2], meteos[1]) -#end \ No newline at end of file + #mtg, mapping, outputs_mapping, nsteps, filtered_outputs_modellist = test_filtered_output_begin(modellists[1], status_tuples[1], outs_vectors[1][1], meteos[1]) + #@test test_filtered_output(mtg, mapping, nsteps, outputs_mapping, meteo_day, filtered_outputs_modellist) +end \ No newline at end of file diff --git a/test/test-Status.jl b/test/test-Status.jl index 96739f8f2..a121d4493 100644 --- a/test/test-Status.jl +++ b/test/test-Status.jl @@ -39,36 +39,34 @@ end status=(var1=[15.0, 16.0], var2=0.3) ) - @test typeof(status(models)) == TimeStepTable{ - Status{ + @test typeof(status(models)) == Status{ (:var5, :var4, :var6, :var1, :var3, :var2), - NTuple{6,Base.RefValue{Float64}} - } - } + Tuple{ + Base.RefValue{Float64}, Base.RefValue{Float64}, Base.RefValue{Float64}, + Base.RefValue{Vector{Float64}}, Base.RefValue{Float64}, Base.RefValue{Float64}} + } + + @test status(models) == models.status @test status(models)[1] == status(models, 1) - @test typeof(status(models, 1)) == PlantMeteo.TimeStepRow{ - Status{ - (:var5, :var4, :var6, :var1, :var3, :var2), - NTuple{6,Base.RefValue{Float64}} - } - } - - @test status(models, 1).var1 == 15.0 - @test status(models, 1).var2 == 0.3 - @test status(models).var1 == [15.0, 16.0] - @test status(models).var2 == [0.3, 0.3] + @test typeof(status(models, 1)) == Float64 + @test typeof(status(models, 4)) == Vector{Float64} + + @test status(models, :var1)[1] == 15.0 + @test status(models, 6) == 0.3 + @test status(models, :var1) == [15.0, 16.0] + @test status(models, :var2) == 0.3 - @test status(models, :var4) == [-Inf, -Inf] - @test status(models, 1).var3 == -Inf - @test status(models, 1).var4 == -Inf - @test status(models, 1).var5 == -Inf - @test status(models, 1).var6 == -Inf + @test status(models, :var4) == -Inf + @test status(models, :var3) == -Inf + @test status(models, :var5) == -Inf + @test status(models, :var6) == -Inf + # TODO this behaviour is now changed, ramifications hard to gauge # Testing setindex: - models[:var6] = [5.5, 5.8] - @test status(models, :var6) == [5.5, 5.8] + #@test models[:var6] = [5.5, 5.8] + #@test status(models, :var6) == [5.5, 5.8] # Testing a vector of ModelList: @test status([models, models]) == [models.status, models.status] diff --git a/test/test-dimensions.jl b/test/test-dimensions.jl index 05ac6cb5f..9e3a4df1f 100644 --- a/test/test-dimensions.jl +++ b/test/test-dimensions.jl @@ -1,4 +1,4 @@ -@testset "Chech status and weather correspond" begin +#@testset "Chech status and weather correspond" begin st = Status(Rₛ=13.747, sky_fraction=1.0, d=0.03, aPPFD=1500) tst1 = TimeStepTable([st]) tst2 = TimeStepTable([st, st]) @@ -13,22 +13,22 @@ @test PlantSimEngine.check_dimensions(st, atm) === nothing # TimeStepTable and Atmosphere are always authorized - @test PlantSimEngine.check_dimensions(tst1, atm) === nothing - @test PlantSimEngine.check_dimensions(tst2, atm) === nothing + # @test PlantSimEngine.check_dimensions(tst1, atm) === nothing + # @test PlantSimEngine.check_dimensions(tst2, atm) === nothing # Status and Weather are always authorized @test PlantSimEngine.check_dimensions(st, w1) === nothing @test PlantSimEngine.check_dimensions(st, w2) === nothing # TimeStepTable and Weather must be checked for equal length - @test PlantSimEngine.check_dimensions(tst1, w1) === nothing - @test PlantSimEngine.check_dimensions(tst2, w2) === nothing + # @test PlantSimEngine.check_dimensions(tst1, w1) === nothing + # @test PlantSimEngine.check_dimensions(tst2, w2) === nothing # This still works because one time step is recycled: - @test PlantSimEngine.check_dimensions(tst1, w2) === nothing + # @test PlantSimEngine.check_dimensions(tst1, w2) === nothing - @test_throws DimensionMismatch PlantSimEngine.check_dimensions(tst2, w1) - @test_throws DimensionMismatch PlantSimEngine.check_dimensions(tst3, w2) + # @test_throws DimensionMismatch PlantSimEngine.check_dimensions(tst2, w1) + # @test_throws DimensionMismatch PlantSimEngine.check_dimensions(tst3, w2) # ModelList and Weather must be checked for equal length m1 = ModelList( diff --git a/test/test-fitting.jl b/test/test-fitting.jl index d5898b493..79e9c5919 100644 --- a/test/test-fitting.jl +++ b/test/test-fitting.jl @@ -4,7 +4,7 @@ k = 0.6 meteo = Atmosphere(T=20.0, Wind=1.0, P=101.3, Rh=0.65, Ri_PAR_f=300.0) m = ModelList(Beer(k), status=(LAI=2.0,)) - run!(m, meteo) + outputs = run!(m, meteo) df = DataFrame(aPPFD=m[:aPPFD][1], LAI=m.status.LAI[1], Ri_PAR_f=meteo.Ri_PAR_f[1]) diff --git a/test/test-mapping.jl b/test/test-mapping.jl index 364c8b24f..19234333d 100755 --- a/test/test-mapping.jl +++ b/test/test-mapping.jl @@ -175,7 +175,7 @@ PlantSimEngine.ObjectDependencyTrait(::Type{<:ToyTestDegreeDaysCumulModel}) = Pl status=st, ) - run!(models, + modellist_outputs = run!(models, meteo_day ; check=true, @@ -193,7 +193,7 @@ PlantSimEngine.ObjectDependencyTrait(::Type{<:ToyTestDegreeDaysCumulModel}) = Pl executor=SequentialEx() ) - @test compare_outputs_modellist_mapping(models, graphsim) + @test compare_outputs_modellist_mapping(modellist_outputs, graphsim) # fully automated model generation st2 = (TT_cu=Vector(cumsum(meteo_day.TT)),) @@ -211,7 +211,7 @@ PlantSimEngine.ObjectDependencyTrait(::Type{<:ToyTestDegreeDaysCumulModel}) = Pl check=true, executor=SequentialEx() ) - @test compare_outputs_modellist_mapping(models, graphsim2) + @test compare_outputs_modellist_mapping(modellist_outputs, graphsim2) @test compare_outputs_graphsim(graphsim, graphsim2) end diff --git a/test/test-simulation.jl b/test/test-simulation.jl index 2ca13f13b..ed2f91dc0 100644 --- a/test/test-simulation.jl +++ b/test/test-simulation.jl @@ -28,10 +28,10 @@ end; Process1Model(1.0), status=(var1=15.0, var2=0.3) ) - run!(models) + outputs = run!(models) - vars = keys(status(models)) - @test [models[i][1] for i in vars] == [15.0, 0.3, 5.5] + vars = keys(outputs) + @test [outputs[i][1] for i in vars] == [15.0, 0.3, 5.5] end; @@ -47,12 +47,12 @@ end; meteo = Atmosphere(T=20.0, Wind=1.0, Rh=0.65) - run!(models, meteo) - vars = keys(status(models)) - @test [models[i][1] for i in vars] == [34.95, 22.0, 56.95, 15.0, 5.5, 0.3] + modellist_outputs = run!(models, meteo) + vars = keys(modellist_outputs) + @test [modellist_outputs[i][1] for i in vars] == [34.95, 22.0, 56.95, 15.0, 5.5, 0.3] mtg, mapping, out = check_multiscale_simulation_is_equivalent_begin(models, status_nt, Weather([meteo])) - @test check_multiscale_simulation_is_equivalent_end(models, mtg, mapping, out, Weather([meteo])) + @test check_multiscale_simulation_is_equivalent_end(modellist_outputs, mtg, mapping, out, Weather([meteo])) end; @testset "Simulation: 1 time-step, 1 Atmosphere, 2 objects" begin @@ -73,15 +73,15 @@ end; meteo = Atmosphere(T=20.0, Wind=1.0, Rh=0.65) @testset "simulation with an array of objects" begin - run!([models, models2], meteo) - @test [models[i][1] for i in keys(status(models))] == [34.95, 22.0, 56.95, 15.0, 5.5, 0.3] - @test [models2[i][1] for i in keys(status(models2))] == [36.95, 26.0, 62.95, 15.0, 6.5, 0.3] + outputs_vector = run!([models, models2], meteo) + @test [outputs_vector[1][i][1] for i in keys(outputs_vector[1])] == [34.95, 22.0, 56.95, 15.0, 5.5, 0.3] + @test [outputs_vector[2][1] for i in keys(outputs_vector[2])] == [36.95, 26.0, 62.95, 15.0, 6.5, 0.3] end @testset "simulation with a dict of objects" begin - run!(Dict("mod1" => models, "mod2" => models2), meteo) - @test [models[i][1] for i in keys(status(models))] == [34.95, 22.0, 56.95, 15.0, 5.5, 0.3] - @test [models2[i][1] for i in keys(status(models2))] == [36.95, 26.0, 62.95, 15.0, 6.5, 0.3] + outputs_vector = run!(Dict("mod1" => models, "mod2" => models2), meteo) + @test [outputs_vector[1][i][1] for i in keys(outputs_vector[1])] == [34.95, 22.0, 56.95, 15.0, 5.5, 0.3] + @test [outputs_vector[2][i][1] for i in keys(outputs_vector[2])] == [36.95, 26.0, 62.95, 15.0, 6.5, 0.3] end end; @@ -95,9 +95,9 @@ end; meteo = Atmosphere(T=20.0, Wind=1.0, Rh=0.65) - run!(models, meteo) - vars = keys(status(models)) - @test [models[i] for i in vars] == [ + outputs = run!(models, meteo) + vars = keys(outputs) + @test [outputs[i] for i in vars] == [ [34.95, 35.550000000000004], [22.0, 23.2], [56.95, 58.75], @@ -125,9 +125,9 @@ end; ] ) - run!(models, meteo) - vars = keys(status(models)) - @test [models[i] for i in vars] == [ + modellist_outputs = run!(models, meteo) + vars = keys(modellist_outputs) + @test [modellist_outputs[i] for i in vars] == [ [34.95, 40.0], [22.0, 23.2], [56.95, 63.2], @@ -137,11 +137,11 @@ end; ] mtg, mapping, out = check_multiscale_simulation_is_equivalent_begin(models, status_nt, meteo) - @test check_multiscale_simulation_is_equivalent_end(models, mtg, mapping, out, meteo) + @test check_multiscale_simulation_is_equivalent_end(modellist_outputs, mtg, mapping, out, meteo) end; -@testset "Simulation: 2 time-steps, 2 Atmospheres, 2 objects" begin +#@testset "Simulation: 2 time-steps, 2 Atmospheres, 2 objects" begin models = ModelList( process1=Process1Model(1.0), process2=Process2Model(), @@ -164,21 +164,22 @@ end; ) @testset "simulation with an array of objects" begin - run!([models, models2], meteo) - @test [models[i] for i in keys(status(models))] == [ + outputs_vector = run!([models, models2], meteo) + @test [outputs_vector[1][i] for i in keys(outputs_vector[1])] == [ [34.95, 40.0], [22.0, 23.2], [56.95, 63.2], [15.0, 16.0], [5.5, 5.8], [0.3, 0.3] ] - @test [models2[i] for i in keys(status(models2))] == [ + @test [outputs_vector[2][i] for i in keys(outputs_vector[2])] == [ [36.95, 42.0], [26.0, 27.2], [62.95, 69.2], [15.0, 16.0], [6.5, 6.8], [0.3, 0.3] ] end + #TODO @testset "simulation with a dict of objects" begin - run!(Dict("mod1" => models, "mod2" => models2), meteo) - @test [models[i] for i in keys(status(models))] == [ + outputs_vector = run!(Dict("mod1" => models, "mod2" => models2), meteo) + @test [outputs_vector[1][i] for i in keys(outputs_vector[1])] == [ [34.95, 40.0], [22.0, 23.2], [56.95, 63.2], [15.0, 16.0], [5.5, 5.8], [0.3, 0.3] ] - @test [models2[i] for i in keys(status(models2))] == [ + @test [outputs_vector[2][i] for i in keys(outputs_vector[2])] == [ [36.95, 42.0], [26.0, 27.2], [62.95, 69.2], [15.0, 16.0], [6.5, 6.8], [0.3, 0.3] ] end diff --git a/test/test-toy_models.jl b/test/test-toy_models.jl index 3912e1280..7a4fd1354 100644 --- a/test/test-toy_models.jl +++ b/test/test-toy_models.jl @@ -1,5 +1,7 @@ meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) +# Note (smack) : The first test's behaviour is weird to me, because there is an [Info :] that correctly indicates +# :LAI is not initialised, yet @test_nowarn doesn't capture it. I'm not sure what the intended test was, between 'Info' and 'Warn' @testset "ToyLAIModel" begin @test_nowarn ModelList(ToyLAIModel()) @test_nowarn ModelList(ToyLAIModel(), status=(TT_cu=10,)) @@ -13,11 +15,11 @@ meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), status=(TT_cu=cumsum(meteo_day.TT),), ) - @test_nowarn run!(m) + outputs = @test_nowarn run!(m) @test m[:TT_cu] == cumsum(meteo_day.TT) - @test m[:LAI][begin] ≈ 0.00554987593080316 - @test m[:LAI][end] ≈ 0.0 + @test outputs[:LAI][begin] ≈ 0.00554987593080316 + @test outputs[:LAI][end] ≈ 0.0 end @testset "ToyLAIModel+Beer" begin @@ -27,10 +29,10 @@ end status=(TT_cu=cumsum(meteo_day.TT),), ) - run!(models, meteo_day) + outputs = run!(models, meteo_day) - @test mean(models.status[:aPPFD]) ≈ 9.511021781482347 - @test mean(models.status[:LAI]) ≈ 1.098492557536525 + @test mean(outputs[:aPPFD]) ≈ 9.511021781482347 + @test mean(outputs[:LAI]) ≈ 1.098492557536525 end @@ -45,8 +47,8 @@ end status=(aPPFD=30.0,), ) - run!(model, executor=SequentialEx()) - @test model.status[:biomass] ≈ rue * model.status[:aPPFD] + outputs = run!(model, executor=SequentialEx()) + @test outputs[:biomass][1] ≈ rue * model.status[:aPPFD] # Several time steps: model = ModelList( @@ -54,8 +56,8 @@ end status=(aPPFD=[10.0, 30.0, 25.0],), ) - run!(model, executor=SequentialEx()) - @test model.status[:biomass] ≈ cumsum(rue * model.status[:aPPFD]) + outputs = run!(model, executor=SequentialEx()) + @test outputs[:biomass] ≈ cumsum(rue * model.status[:aPPFD]) end @testset "ToyAssimGrowthModel" begin @@ -73,8 +75,8 @@ end @test to_initialize(model) == NamedTuple() - run!(model) - @test model.status[:biomass] ≈ [4.5] + outputs = run!(model) + @test outputs[:biomass] ≈ [4.5] # Several time steps: model = ModelList( @@ -82,9 +84,9 @@ end status=(aPPFD=[10.0, 30.0, 25.0],), ) - run!(model) - @test model.status[:biomass] ≈ cumsum(model.status[:biomass_increment]) - @test model.status[:biomass_increment] ≈ [0.8333333333333334, 4.5, 3.5833333333333335] + outputs = run!(model) + @test outputs[:biomass] ≈ cumsum(outputs[:biomass_increment]) + @test outputs[:biomass_increment] ≈ [0.8333333333333334, 4.5, 3.5833333333333335] end @testset "ToyLAIModel+Beer+ToyRUEGrowthModel" begin @@ -100,9 +102,9 @@ end @test_logs (:warn, r"A parallel executor was provided") run!(models, meteo_day) # If we provide a serial executor, it works without a warning: - @test_nowarn run!(models, meteo_day, executor=SequentialEx()) + outputs = @test_nowarn run!(models, meteo_day, executor=SequentialEx()) - @test mean(models.status[:aPPFD]) ≈ 9.511021781482347 - @test mean(models.status[:LAI]) ≈ 1.098492557536525 - @test models.status[:biomass][end] ≈ 1041.4687939085675 rtol = 1e-4 + @test mean(outputs[:aPPFD]) ≈ 9.511021781482347 + @test mean(outputs[:LAI]) ≈ 1.098492557536525 + @test outputs[:biomass][end] ≈ 1041.4687939085675 rtol = 1e-4 end \ No newline at end of file From 7963a310c1174d8c789db4a6346a88962a30d99c Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Fri, 31 Jan 2025 17:58:40 +0100 Subject: [PATCH 044/147] More fixes --- test/test-dimensions.jl | 2 +- test/test-fitting.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test-dimensions.jl b/test/test-dimensions.jl index 9e3a4df1f..ab2741c52 100644 --- a/test/test-dimensions.jl +++ b/test/test-dimensions.jl @@ -1,4 +1,4 @@ -#@testset "Chech status and weather correspond" begin +@testset "Chech status and weather correspond" begin st = Status(Rₛ=13.747, sky_fraction=1.0, d=0.03, aPPFD=1500) tst1 = TimeStepTable([st]) tst2 = TimeStepTable([st, st]) diff --git a/test/test-fitting.jl b/test/test-fitting.jl index 79e9c5919..466b33aac 100644 --- a/test/test-fitting.jl +++ b/test/test-fitting.jl @@ -6,7 +6,7 @@ m = ModelList(Beer(k), status=(LAI=2.0,)) outputs = run!(m, meteo) - df = DataFrame(aPPFD=m[:aPPFD][1], LAI=m.status.LAI[1], Ri_PAR_f=meteo.Ri_PAR_f[1]) + df = DataFrame(aPPFD=outputs[:aPPFD][1], LAI=m.status.LAI[1], Ri_PAR_f=meteo.Ri_PAR_f[1]) k_fit = fit(Beer, df).k @test k_fit == k From 76f3eaf7a0ac7fb2da92438346efbbdfd9ad474f Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 3 Feb 2025 15:34:39 +0100 Subject: [PATCH 045/147] Fix multi-object run! function and tests (+ other tests). Not sure the current way is ideal over many modellists, but it's simple, non-constraining and works for the basic test cases we have. --- src/run.jl | 36 +++++++++++++++++++++++++++--------- test/test-performance.jl | 4 ++-- test/test-simulation.jl | 12 ++++++------ 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/run.jl b/src/run.jl index 508317fdd..2e8af3d6a 100644 --- a/src/run.jl +++ b/src/run.jl @@ -145,11 +145,19 @@ function run!( ) maxlog = 1 end - outputs_vector = [] - for obj in collect(values(object)) - push!(outputs_vector, run!(obj, meteo, constants, extra, outputs=outputs, check=check, executor=executor)) + outputs_collection = isa(object, AbstractArray) ? [] : isnothing(outputs) ? Dict() : Dict{TimeStepTable{Status{typeof(outputs)}}} + + # Each object: + for obj in object + + if isa(object, AbstractArray) + push!(outputs_collection, run!(obj, meteo, constants, extra, outputs=outputs, check=check, executor=executor)) + else + outputs_collection[obj.first] = run!(obj.second, meteo, constants, extra, outputs=outputs, check=check, executor=executor) + end + end - return outputs_vector + return outputs_collection end # 3- one object, one meteo time-step, several status time-steps (rare case but possible) @@ -292,7 +300,7 @@ function run!( outputs=nothing, check=true, executor=ThreadedEx() -) where {T<:Union{AbstractArray,AbstractDict}} +) where {T<:Union{AbstractArray, AbstractDict}} dep_graphs = [dep(obj) for obj in collect(values(object))] #obj_parallelizable = all([object_parallelizable(graph) for graph in dep_graphs]) @@ -304,8 +312,6 @@ function run!( ) maxlog = 1 end - outputs_vector = [] - # Each object: for (i, obj) in enumerate(collect(values(object))) @@ -320,9 +326,21 @@ function run!( ) end end - push!(outputs_vector, run!(obj, meteo, constants, extra, outputs=outputs, check=check, executor=executor)) end - return outputs_vector + + outputs_collection = isa(object, AbstractArray) ? [] : isnothing(outputs) ? Dict() : Dict{TimeStepTable{Status{typeof(outputs)}}} + + # Each object: + for obj in object + + if isa(object, AbstractArray) + push!(outputs_collection, run!(obj, meteo, constants, extra, outputs=outputs, check=check, executor=executor)) + else + outputs_collection[obj.first] = run!(obj.second, meteo, constants, extra, outputs=outputs, check=check, executor=executor) + end + + end + return outputs_collection end diff --git a/test/test-performance.jl b/test/test-performance.jl index 70a9ca12b..6e9500a99 100644 --- a/test/test-performance.jl +++ b/test/test-performance.jl @@ -49,6 +49,6 @@ models2 = ModelList(process1=ToySleepModel(), status=(a=vc,)) @test abs(nthr * med_time_mt - med_time_seq) < 0.2 * med_time_seq end - # todo DataFrame equals - @test status(models1) == status(models2) + # unsure how to recover outputs in benchmarked expressions to compare them, rerun the functions as a workaround for now + @test run!(models1, meteo_day; executor = SequentialEx()) == run!(models2, meteo_day; executor = ThreadedEx()) end \ No newline at end of file diff --git a/test/test-simulation.jl b/test/test-simulation.jl index ed2f91dc0..a21e4f639 100644 --- a/test/test-simulation.jl +++ b/test/test-simulation.jl @@ -75,13 +75,13 @@ end; @testset "simulation with an array of objects" begin outputs_vector = run!([models, models2], meteo) @test [outputs_vector[1][i][1] for i in keys(outputs_vector[1])] == [34.95, 22.0, 56.95, 15.0, 5.5, 0.3] - @test [outputs_vector[2][1] for i in keys(outputs_vector[2])] == [36.95, 26.0, 62.95, 15.0, 6.5, 0.3] + @test [outputs_vector[2][i][1] for i in keys(outputs_vector[2])] == [36.95, 26.0, 62.95, 15.0, 6.5, 0.3] end @testset "simulation with a dict of objects" begin outputs_vector = run!(Dict("mod1" => models, "mod2" => models2), meteo) - @test [outputs_vector[1][i][1] for i in keys(outputs_vector[1])] == [34.95, 22.0, 56.95, 15.0, 5.5, 0.3] - @test [outputs_vector[2][i][1] for i in keys(outputs_vector[2])] == [36.95, 26.0, 62.95, 15.0, 6.5, 0.3] + @test [outputs_vector["mod1"][1][i] for i in keys(outputs_vector["mod1"])] == [34.95, 22.0, 56.95, 15.0, 5.5, 0.3] + @test [outputs_vector["mod2"][1][i] for i in keys(outputs_vector["mod2"])] == [36.95, 26.0, 62.95, 15.0, 6.5, 0.3] end end; @@ -141,7 +141,7 @@ end; end; -#@testset "Simulation: 2 time-steps, 2 Atmospheres, 2 objects" begin +@testset "Simulation: 2 time-steps, 2 Atmospheres, 2 objects" begin models = ModelList( process1=Process1Model(1.0), process2=Process2Model(), @@ -176,10 +176,10 @@ end; #TODO @testset "simulation with a dict of objects" begin outputs_vector = run!(Dict("mod1" => models, "mod2" => models2), meteo) - @test [outputs_vector[1][i] for i in keys(outputs_vector[1])] == [ + @test [[outputs_vector["mod1"][1][i], outputs_vector["mod1"][2][i]] for i in keys(outputs_vector["mod1"])] == [ [34.95, 40.0], [22.0, 23.2], [56.95, 63.2], [15.0, 16.0], [5.5, 5.8], [0.3, 0.3] ] - @test [outputs_vector[2][i] for i in keys(outputs_vector[2])] == [ + @test [[outputs_vector["mod2"][1][i], outputs_vector["mod2"][2][i]] for i in keys(outputs_vector["mod2"])] == [ [36.95, 42.0], [26.0, 27.2], [62.95, 69.2], [15.0, 16.0], [6.5, 6.8], [0.3, 0.3] ] end From 8bbf9daba3d0083afdec659607f710a20b219b8e Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 3 Feb 2025 15:35:15 +0100 Subject: [PATCH 046/147] Fix some remaining ModelList status issues that break some tests --- src/component_models/ModelList.jl | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/component_models/ModelList.jl b/src/component_models/ModelList.jl index b227f49ce..f336ce86d 100644 --- a/src/component_models/ModelList.jl +++ b/src/component_models/ModelList.jl @@ -169,12 +169,12 @@ TimeStepTable instead (or a NamedTuple as shown in the example). """ struct ModelList{M<:NamedTuple,S#=,O=#,V<:Tuple{Vararg{Symbol}}} models::M - status::Status{S} + status::S #outputs::O vars_not_propagated::V end -function ModelList(models::M, status::Status) where {M<:NamedTuple{names,T} where {names,T<:NTuple{N,<:AbstractModel} where {N}}} +function ModelList(models::M, status::Status#=, outputs::O=#) where {#=O,=# M<:NamedTuple{names,T} where {names,T<:NTuple{N,<:AbstractModel} where {N}}} ModelList(models, status, ())#outputs, ()) end @@ -247,16 +247,18 @@ function ModelList( @error "$(unexpected_outputs[i])" end end - end=# - - # TODO preallocate outputs - #Dict(var => [typeof(status[var])[] for n in 1:nsteps] for var in user_outputs) + end + # Can't preallocate outputs without knowing which are filtered + # And creating a TSTable for all of them doesn't help, as a TST of a subset + # of the outputs is not of the same type and conversion fails + status_flattened, vec_vars = flatten_status(deepcopy(ts_kwargs)) + user_outputs = TimeStepTable([status_flattened])=# model_list = ModelList( mods, ts_kwargs, - #user_outputs, + #init_fun_default(user_outputs), vector_vars ) variables_check && !is_initialized(model_list) @@ -286,11 +288,12 @@ Careful, the function makes a copy of the input `x` if it does not list all need function add_model_vars(x, models, type_promotion) ref_vars = merge(init_variables(models; verbose=false)...) # If no variable is required, we return the input: - length(ref_vars) == 0 && return x + length(ref_vars) == 0 && return isa(x, Status) ? x : Status(x) # If the user gave a status, we check if all the variables are already initialized: vars_in_x = status_keys(x) - all([k in vars_in_x for k in keys(ref_vars)]) && return x # If so, we return the input + status_x = + all([k in vars_in_x for k in keys(ref_vars)]) && return isa(x, Status) ? x : Status(x) # If so, we return the input # Else, we add the variables by making a new object (carefull, this is a copy so it takes more time): From 01d7dafc1d5c22c650e253c49266fe6c3660a96c Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 3 Feb 2025 17:14:46 +0100 Subject: [PATCH 047/147] Small changes/fixes/hacks. All tests pass except for documentation. There are some API issues remaining to be discussed, and some tests that probably need some reworking. PBP and XPalm test also not yet fixed, so downstream CI will break. --- .../model_generation_from_status_vectors.jl | 16 +++++++-- src/run.jl | 2 +- test/downstream/test-all-benchmarks.jl | 3 -- test/helper-functions.jl | 36 ++++++++++--------- test/test-ModelList.jl | 8 ++--- 5 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/mtg/mapping/model_generation_from_status_vectors.jl b/src/mtg/mapping/model_generation_from_status_vectors.jl index 18303b131..05e8e4db1 100644 --- a/src/mtg/mapping/model_generation_from_status_vectors.jl +++ b/src/mtg/mapping/model_generation_from_status_vectors.jl @@ -191,7 +191,19 @@ function modellist_to_mapping(modellist_original::ModelList, modellist_status; n models = modellist.models - mapping_incomplete = Dict( + mapping_incomplete = isnothing(modellist_status) ? + ( + Dict( + default_scale => ( + models..., + MultiScaleModel( + model=HelperCurrentTimestepModel(), + mapping=[PreviousTimeStep(:next_timestep),], + ), + Status((current_timestep=1,next_timestep=1,)) + ), + )) : ( + Dict( default_scale => ( models..., MultiScaleModel( @@ -201,7 +213,7 @@ function modellist_to_mapping(modellist_original::ModelList, modellist_status; n Status((modellist_status..., current_timestep=1,next_timestep=1,)) ), ) - + ) timestep_scale = "Default" organ = "Default" diff --git a/src/run.jl b/src/run.jl index 2e8af3d6a..f57f42cf0 100644 --- a/src/run.jl +++ b/src/run.jl @@ -92,7 +92,7 @@ function adjust_weather_timesteps_to_status_length(st::Status, meteo) if isnothing(meteo) meteo_adjusted = Weather(repeat([Atmosphere(NamedTuple())], status_timesteps_len)) - elseif get_nsteps(meteo) == 1 + elseif get_nsteps(meteo) == 1 && isa(meteo, Atmosphere) meteo_adjusted = Weather(repeat([meteo], status_timesteps_len)) end diff --git a/test/downstream/test-all-benchmarks.jl b/test/downstream/test-all-benchmarks.jl index f9635775a..a6bc22c97 100644 --- a/test/downstream/test-all-benchmarks.jl +++ b/test/downstream/test-all-benchmarks.jl @@ -9,9 +9,6 @@ using DataFrames, CSV using MultiScaleTreeGraph using PlantMeteo, Statistics -# Include the example dummy processes: -using PlantSimEngine.Examples - using BenchmarkTools using Dates diff --git a/test/helper-functions.jl b/test/helper-functions.jl index 58a91e28a..990ac0f9a 100644 --- a/test/helper-functions.jl +++ b/test/helper-functions.jl @@ -56,7 +56,7 @@ meteo = get_weather(43.649777, 3.869889, period, sink = DataFrame)=# function get_simple_meteo_bank() meteos= [Atmosphere(T=20.0, Wind=1.0, P=101.3, Rh=0.65, Ri_PAR_f=300.0), - Weather( + #=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) @@ -68,7 +68,7 @@ function get_simple_meteo_bank() Atmosphere(T=30.0, Wind=0.5, Rh=0.6, Ri_PAR_f=100.0), Atmosphere(T=20.0, Wind=1.0, Rh=0.6, Ri_PAR_f=200.0), Atmosphere(T=25.0, Wind=1.0, Rh=0.6, Ri_PAR_f=200.0), - Atmosphere(T=10.0, Wind=0.5, Rh=0.6, Ri_PAR_f=200.0)]), + Atmosphere(T=10.0, Wind=0.5, Rh=0.6, Ri_PAR_f=200.0)]),=# CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18), @@ -82,8 +82,11 @@ function get_modellist_bank() vals = (var1=15.0, var2=0.3)#, TT_cu=cumsum(meteo_day.TT)) vals2 = (TT_cu=cumsum(meteo_day.TT),) vals3 = (var1=15.0, var2=0.3) + vals4 = (var9 =1.0,var0=1.0) + vals5 = (var0=1.0,) + vals6 = (var0=1.0,) - status_tuples = [vals, vals2, vals3, nothing, vals3, vals3] + status_tuples = [vals, vals2, vals3, vals4, vals5, vals6] models = [ModelList( process1=Process1Model(1.0), @@ -112,7 +115,7 @@ function get_modellist_bank() process5=Process5Model(), process6=Process6Model(), # process7=Process7Model(), - # status=(var1=15.0, var2=0.3) + status=vals4 ), ModelList( @@ -123,7 +126,7 @@ function get_modellist_bank() process5=Process5Model(), process6=Process6Model(), process7=Process7Model(), - status=(var1=15.0, var2=0.3) + status=vals5 ), ModelList( @@ -132,7 +135,7 @@ function get_modellist_bank() process3=Process3Model(), process4=Process4Model(), process5=Process5Model(), - status=(var1=15.0, var2=0.3) + status=vals6 ), ] @@ -140,24 +143,24 @@ function get_modellist_bank() outputs_tuples_vectors = [ # this one has one tuple with a duplicate, and one with a nonexistent variable - [(:var1,), (:var1, :var1), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var5), - (:var2, :var7, :var3, :var1), (:var1, :var2, :var3, :var4, :var5)], + [(:var1,), #=(:var1, :var1),=# (:var1, :var2), (:var1, :var3), (:var1, :var4, :var5), + #=(:var2, :var7, :var3, :var1),=# (:var1, :var2, :var3, :var4, :var5)], - [NamedTuple(), (:TT_cu,), (:TT_cu,:LAI) , (:biomass,:LAI), (:TT_cu, :LAI, :PPFD, :biomass, :biomass_increment),], + [#=NamedTuple(),=# (:TT_cu,), (:TT_cu,:LAI) , (:biomass,:LAI), (:TT_cu, :LAI, :aPPFD, :biomass, :biomass_increment),], - [NamedTuple(), (:var1,), (:var1, :var4), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), - (:var2, :var7, :var3, :var1), (:var1, :var2, :var3, :var4, :var5, :var6)], + [#=NamedTuple(),=# (:var1,), (:var1, :var4), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), + #=(:var2, :var7, :var3, :var1),=# (:var1, :var2, :var3, :var4, :var5, :var6)], - [NamedTuple(), (:var1,), (:var1, :var4), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), + [#=NamedTuple(),=# (:var1,), (:var1, :var4), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), (:var2, :var7, :var3, :var1), (:var1, :var2, :var3, :var4, :var5, :var6)], - [NamedTuple(), (:var1,), (:var1, :var4), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), + [#=NamedTuple(),=# (:var1,), (:var1, :var4), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), (:var2, :var7, :var3, :var1), (:var1, :var2, :var3, :var4, :var5, :var6) , (:var1, :var2, :var3, :var4, :var5, :var6, :var7, :var8, :var9)], - [NamedTuple(), (:var1,), (:var1, :var1), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), + [#=NamedTuple(),=# (:var1,), #=(:var1, :var1),=# (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), (:var2, :var7, :var3, :var1), (:var1, :var2, :var3, :var4, :var5, :var6) - , (:var1, :var2, :var3, :var4, :var5, :var6, :var7, :var8, :var9, :var0)], + , (:var1, :var2, :var3, :var4, :var5, :var6, :var7, #=:var8, :var9,=# :var0)], ] @@ -168,8 +171,7 @@ end # See world-age problems and comments around modellist_to_mapping if you don't know/remember what that's about function test_filtered_output_begin(m::ModelList, status_tuple, requested_outputs, meteo) - meteo_adjusted = PlantSimEngine.adjust_weather_timesteps_to_status_length(m.status, meteo) - nsteps = PlantSimEngine.get_nsteps(meteo_adjusted) + nsteps = PlantSimEngine.get_nsteps(meteo) preallocated_outputs = PlantSimEngine.pre_allocate_outputs(m, requested_outputs, nsteps) @test length(preallocated_outputs) == nsteps if length(requested_outputs) > 0 diff --git a/test/test-ModelList.jl b/test/test-ModelList.jl index eb55ed820..f1a4dc1ce 100644 --- a/test/test-ModelList.jl +++ b/test/test-ModelList.jl @@ -227,7 +227,7 @@ end end=# -# TODO restrict status to a single Status + @testset "ModelList outputs preallocation" begin meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) vals = (var1=15.0, var2=0.3, TT_cu=cumsum(meteo_day.TT)) @@ -256,9 +256,9 @@ end=# for meteo in meteos for out_tuple in outs_vector - println(out_tuple) - mtg, mapping, outputs_mapping, nsteps, filtered_outputs_modellist = test_filtered_output_begin(modellist, status_tuple, out_tuple, meteo) - @test test_filtered_output(mtg, mapping, nsteps, outputs_mapping, meteo, filtered_outputs_modellist) + meteo_adjusted = PlantSimEngine.adjust_weather_timesteps_to_status_length(modellist.status, meteo) + mtg, mapping, outputs_mapping, nsteps, filtered_outputs_modellist = test_filtered_output_begin(modellist, status_tuple, out_tuple, meteo_adjusted) + @test test_filtered_output(mtg, mapping, nsteps, outputs_mapping, meteo_adjusted, filtered_outputs_modellist) end end end From 5f16eb9a97fe6d7c5344ec95e37b5dfc408baea3 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 3 Feb 2025 17:34:52 +0100 Subject: [PATCH 048/147] Fix to the modellist outputs filtering test --- test/helper-functions.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/helper-functions.jl b/test/helper-functions.jl index 990ac0f9a..96ff9df80 100644 --- a/test/helper-functions.jl +++ b/test/helper-functions.jl @@ -77,6 +77,8 @@ function get_simple_meteo_bank() end function get_modellist_bank() + meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) + rue = 0.3 vals = (var1=15.0, var2=0.3)#, TT_cu=cumsum(meteo_day.TT)) From 412b542880c9ed4c5d9ecb3b9a1e88b5bace7bfc Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Thu, 6 Feb 2025 11:44:42 +0100 Subject: [PATCH 049/147] Remove commented out code and outdated comments, remove vars_not_propagated (now unnecessary) --- src/component_models/ModelList.jl | 94 ++----------------------------- src/dataframe.jl | 16 +----- src/run.jl | 70 ++--------------------- src/traits/table_traits.jl | 2 +- 4 files changed, 14 insertions(+), 168 deletions(-) diff --git a/src/component_models/ModelList.jl b/src/component_models/ModelList.jl index f336ce86d..5769cb4ca 100644 --- a/src/component_models/ModelList.jl +++ b/src/component_models/ModelList.jl @@ -167,22 +167,19 @@ julia> status(m) Note that computations will be slower using DataFrame, so if performance is an issue, use TimeStepTable instead (or a NamedTuple as shown in the example). """ -struct ModelList{M<:NamedTuple,S#=,O=#,V<:Tuple{Vararg{Symbol}}} +struct ModelList{M<:NamedTuple,S} models::M status::S - #outputs::O - vars_not_propagated::V end -function ModelList(models::M, status::Status#=, outputs::O=#) where {#=O,=# M<:NamedTuple{names,T} where {names,T<:NTuple{N,<:AbstractModel} where {N}}} - ModelList(models, status, ())#outputs, ()) -end +#=function ModelList(models::M, status::Status) where {M<:NamedTuple{names,T} where {names,T<:NTuple{N,<:AbstractModel} where {N}}} + ModelList(models, status) +end=# # General interface: function ModelList( args...; status=nothing, - #outputs=nothing, type_promotion::Union{Nothing,Dict}=nothing, variables_check::Bool=true, nsteps=nothing, @@ -210,56 +207,11 @@ function ModelList( # Make a vector of NamedTuples from the input (please implement yours if you need it) ts_kwargs = homogeneous_ts_kwargs(status) - - # Variables for which a value was given for each time-step by the user: - vector_vars = get_vars_not_propagated(status) - # Note: that the length was checked in homogeneous_ts_kwargs, so we don't need to check it again here. - # Note 2: we need to know these variables because they will not be propagated between time-steps, but set at - # the given value instead. - - # Add the missing variables required by the models (set to default value): ts_kwargs = add_model_vars(ts_kwargs, mods, type_promotion) - #=user_outputs = outputs # todo : tuple to array - - # todo check outputs are all amongst the variables provided by the user and models - f = [] - - for i in 1:length(mods) - bb = keys(init_variables(mods[i])) - for j in 1:length(bb) - push!(f, bb[j]) - end - #f = (f..., bb...) - end - - f = unique!(f) - - # default implicit behaviour, track everything - if isnothing(user_outputs) - user_outputs = (f...,) - #all_vars = merge((keys(init_variables(object.models[i])) for i in 1:length(object.models))...) - else - unexpected_outputs = setdiff(user_outputs, f) - if !isempty(unexpected_outputs) - @error "Some output variable(s) requested was not found / were not found amongst the model variables : " - for i in 1:length(unexpected_outputs) - @error "$(unexpected_outputs[i])" - end - end - end - - # Can't preallocate outputs without knowing which are filtered - # And creating a TSTable for all of them doesn't help, as a TST of a subset - # of the outputs is not of the same type and conversion fails - status_flattened, vec_vars = flatten_status(deepcopy(ts_kwargs)) - user_outputs = TimeStepTable([status_flattened])=# - model_list = ModelList( mods, ts_kwargs, - #init_fun_default(user_outputs), - vector_vars ) variables_check && !is_initialized(model_list) @@ -270,12 +222,8 @@ outputs(m::ModelList) = m.outputs parse_models(m) = NamedTuple([process(i) => i for i in m]) -init_fun_default(x::Vector{T}) where {T} = TimeStepTable([Status(i) for i in x]) -init_fun_default(x::N) where {N<:NamedTuple} = TimeStepTable([Status(x)]) -init_fun_default(x) = x - """ - add_model_vars(x, models, type_promotion; init_fun=init_fun_default) + add_model_vars(x, models, type_promotion) Check which variables in `x` are not initialized considering a set of `models` and the variables needed for their simulation. If some variables are uninitialized, initialize them to their default values. @@ -311,7 +259,6 @@ function add_model_vars(x, models, type_promotion) end function status_keys(st) - #Tables.istable(st) && return Tables.columnnames(st) return keys(st) end @@ -348,39 +295,12 @@ PlantSimEngine.homogeneous_ts_kwargs((Tₗ=[25.0, 26.0], aPPFD=1000.0)) function homogeneous_ts_kwargs(kwargs::NamedTuple{N,T}) where {N,T} length(kwargs) == 0 && return kwargs vars_vals = collect(Any, values(kwargs)) - #length_vars = [isa(i, RefVector) ? 1 : length(i) for i in vars_vals] - #Note: length is 1 for RefVector because it is a vector of references to other scales, - # not a vector of values - - # One of the variable is given as an array, meaning this is actually several - # time-steps. In this case we make an array of vars. - max_length_st = 1#nsteps !== nothing ? nsteps : maximum(length_vars) - - #=for i in eachindex(vars_vals) - # If the ith vars has length one, repeat its value to match the max time-steps: - if length_vars[i] == 1 - vars_vals[i] = repeat([vars_vals[i]], max_length_st) - else - length_vars[i] != max_length_st && @error "$(keys(kwargs)[i]) should be length $max_length_st or 1" - end - end=# - - # Making a vars for each ith value in the user vars: + vars_array = NamedTuple{keys(kwargs)}(j for j in vars_vals) return vars_array end - -""" - get_vars_not_propagated(status) - -Returns all variables that are given for several time-steps in the status. -""" -get_vars_not_propagated(status) = (findall(x -> length(x) > 1, status)...,) -get_vars_not_propagated(df::DataFrames.DataFrame) = (propertynames(df)...,) -get_vars_not_propagated(::Nothing) = () - """ Base.copy(l::ModelList) Base.copy(l::ModelList, status) @@ -414,7 +334,6 @@ function Base.copy(m::T) where {T<:ModelList} ModelList( m.models, deepcopy(m.status), - m.vars_not_propagated ) end @@ -422,7 +341,6 @@ function Base.copy(m::T, status) where {T<:ModelList} ModelList( m.models, status, - m.vars_not_propagated ) end diff --git a/src/dataframe.jl b/src/dataframe.jl index 0904fda8f..a47e67e77 100644 --- a/src/dataframe.jl +++ b/src/dataframe.jl @@ -60,23 +60,11 @@ function DataFrames.DataFrame(components::T) where {T<:AbstractDict{N,<:ModelLis reduce(vcat, df) end -# NB: could use dispatch on concrete types but would enforce specific implementation for each - - -""" - DataFrame(components::ModelList{T,<:TimeStepTable}) - -Implementation of `DataFrame` for a `ModelList` model with several time steps. -""" -function DataFrames.DataFrame(components::ModelList{T,S,V}) where {T,S<:TimeStepTable,V} - DataFrames.DataFrame([(NamedTuple(j)..., timestep=i) for (i, j) in enumerate(status(components))]) -end - """ - DataFrame(components::ModelList{T,S,V}) where {T,S<:Status,V} + DataFrame(components::ModelList{T,S}) where {T,S<:Status} Implementation of `DataFrame` for a `ModelList` model with one time step. """ -function DataFrames.DataFrame(components::ModelList{T,S,V}) where {T,S<:Status,V} +function DataFrames.DataFrame(components::ModelList{T,S}) where {T,S<:Status} DataFrames.DataFrame([NamedTuple(status(components)[1])]) end diff --git a/src/run.jl b/src/run.jl index f57f42cf0..52f8752b9 100644 --- a/src/run.jl +++ b/src/run.jl @@ -102,10 +102,9 @@ end # Managing one or several objects, one or several time-steps: -# This is the default function called by the user, which uses traits +# User entry point, which uses traits # to dispatch to the correct method. The traits are defined in table_traits.jl -# and define either TableAlike or SingletonAlike objects. -# Please use these traits to define your own objects. +# and define either TableAlike, TreeAlike or SingletonAlike objects. function run!( object, meteo=nothing, @@ -127,7 +126,7 @@ function run!( ) end -# 1- several objects and several time-steps +# 1- several ModelList objects and several time-steps function run!( ::TableAlike, object::T, @@ -160,67 +159,8 @@ function run!( return outputs_collection end -# 3- one object, one meteo time-step, several status time-steps (rare case but possible) -# Also occurs when meteo is nothing -#=function run!( - ::SingletonAlike, - object::T, - meteo=nothing, - constants=PlantMeteo.Constants(), - extra=nothing; - outputs=nothing, - check=true, - executor=ThreadedEx() -) where {T<:ModelList} -sim_rows = Tables.rows(status(object)) - dep_graph = dep(object, length(sim_rows)) - - if check && length(dep_graph.not_found) > 0 - error( - "The following processes are missing to run the ModelList: ", - dep_graph.not_found - ) - end - - nsteps = length(sim_rows) - outputs_preallocated = pre_allocate_outputs(object, outputs, nsteps) - status_flattened, vector_variables = flatten_status(object.status) - - #if !timestep_parallelizable(dep_graph) - if executor != SequentialEx() - is_ts_parallel = which_timestep_parallelizable(dep_graph) - mods_not_parallel = join([i.second.first for i in is_ts_parallel[findall(x -> x.second.second == false, is_ts_parallel)]], "; ") - - check && @warn string( - "A parallel executor was provided (`executor=$(executor)`) but some models cannot be run in parallel: $mods_not_parallel. ", - "The simulation will be run sequentially. Use `executor=SequentialEx()` to remove this warning." - ) maxlog = 1 - end - # Not parallelizable over time-steps, it means some values depend on the previous value. - # In this case we propagate the values of the variables from one time-step to the other, except for - # the variables the user provided for all time-steps. - for (i, row) in enumerate(sim_rows) - i > 1 && propagate_values!(sim_rows[i-1], row, object.vars_not_propagated) - roots = collect(dep_graph.roots) - for (process, node) in roots - run_node!(object, node, i, row, nothing, constants, extra) - end - save_results!(status_flattened, outputs_preallocated, i) - i+1 <= nsteps && update_vector_variables(object.status, status_flattened, vector_variables, i + 1) - end - - return outputs_preallocated - else - @floop executor for (i, row) in enumerate(sim_rows) - local roots = collect(dep_graph.roots) - for (process, node) in roots - run_node!(object, node, i, row, meteo, constants, extra) - end - end - end -end=# - -# 4- one object, several meteo time-step, several status time-steps +# 2 - one object, one or multiple meteo time-step(s), with vectors provided in the status +# Meaning a single meteo timestep might be expanded to fit the status vector size function run!( ::SingletonAlike, object::T, diff --git a/src/traits/table_traits.jl b/src/traits/table_traits.jl index f9194c44d..2e6373273 100644 --- a/src/traits/table_traits.jl +++ b/src/traits/table_traits.jl @@ -56,7 +56,7 @@ DataFormat(::Type{<:Dict}) = TableAlike() DataFormat(::Type{<:NamedTuple}) = SingletonAlike() DataFormat(::Type{<:Status}) = SingletonAlike() -DataFormat(::Type{<:ModelList{Mo,S,V} where {Mo,S,V}}) = SingletonAlike() +DataFormat(::Type{<:ModelList{Mo,S} where {Mo,S}}) = SingletonAlike() DataFormat(::Type{<:GraphSimulation}) = TreeAlike() DataFormat(::Type{<:PlantMeteo.AbstractAtmosphere}) = SingletonAlike() From 85e75264261eb3211965dc4fceb88e1cd2cc1a1c Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Thu, 6 Feb 2025 16:47:19 +0100 Subject: [PATCH 050/147] Fix run! behaviour in modellist mode for dataframerow-type meteos. Multiscale meteo adjustments still necessary --- src/component_models/Status.jl | 1 + src/run.jl | 63 ++++++++++++++++++++-------------- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/component_models/Status.jl b/src/component_models/Status.jl index 0a1c7f906..c09f36403 100644 --- a/src/component_models/Status.jl +++ b/src/component_models/Status.jl @@ -158,6 +158,7 @@ function update_vector_variables(s::Status, sf::Status, vector_variables, i) end end +# TODO do a bit more and return error if there is a length discrepancy that isn't accounted for by timestep differences function get_status_vector_max_length(s::Status) max_len = 1 for (var, value) in zip(keys(s), s) diff --git a/src/run.jl b/src/run.jl index 52f8752b9..7af42bf7b 100644 --- a/src/run.jl +++ b/src/run.jl @@ -89,13 +89,18 @@ run! function adjust_weather_timesteps_to_status_length(st::Status, meteo) status_timesteps_len = get_status_vector_max_length(st) meteo_adjusted = meteo - + if isnothing(meteo) meteo_adjusted = Weather(repeat([Atmosphere(NamedTuple())], status_timesteps_len)) - elseif get_nsteps(meteo) == 1 && isa(meteo, Atmosphere) - meteo_adjusted = Weather(repeat([meteo], status_timesteps_len)) + elseif get_nsteps(meteo) == 1 + if isa(meteo, Atmosphere) + meteo_adjusted = Weather(repeat([meteo], status_timesteps_len)) + end end + if DataFormat(meteo_adjusted) == TableAlike() + return Tables.rows(meteo_adjusted) + end return meteo_adjusted end @@ -173,9 +178,9 @@ function run!( ) where {T<:ModelList} meteo_adjusted = adjust_weather_timesteps_to_status_length(object.status, meteo) - - meteo_rows = Tables.rows(meteo_adjusted) - dep_graph = dep(object, length(meteo_rows)) + nsteps = get_nsteps(meteo_adjusted) + + dep_graph = dep(object, nsteps) if check # Check if the meteo data and the status have the same length (or length 1) @@ -191,33 +196,40 @@ function run!( #if !timestep_parallelizable(dep_graph) if executor != SequentialEx() - is_ts_parallel = which_timestep_parallelizable(dep_graph) - mods_not_parallel = join([i.second.first for i in is_ts_parallel[findall(x -> x.second.second == false, is_ts_parallel)]], "; ") + is_ts_parallel = which_timestep_parallelizable(dep_graph) + mods_not_parallel = join([i.second.first for i in is_ts_parallel[findall(x -> x.second.second == false, is_ts_parallel)]], "; ") - check && @warn string( - "A parallel executor was provided (`executor=$(executor)`) but some models cannot be run in parallel: $mods_not_parallel. ", - "The simulation will be run sequentially. Use `executor=SequentialEx()` to remove this warning." - ) maxlog = 1 - end + check && @warn string( + "A parallel executor was provided (`executor=$(executor)`) but some models cannot be run in parallel: $mods_not_parallel. ", + "The simulation will be run sequentially. Use `executor=SequentialEx()` to remove this warning." + ) maxlog = 1 + end + + outputs_preallocated = pre_allocate_outputs(object, outputs, nsteps) + status_flattened, vector_variables = flatten_status(object.status) - nsteps = length(meteo_rows) - outputs_preallocated = pre_allocate_outputs(object, outputs, nsteps) - status_flattened, vector_variables = flatten_status(object.status) - - # Not parallelizable over time-steps, it means some values depend on the previous value. - # In this case we propagate the values of the variables from one time-step to the other, except for - # the variables the user provided for all time-steps. - roots = collect(dep_graph.roots) + # Not parallelizable over time-steps, it means some values depend on the previous value. + # In this case we propagate the values of the variables from one time-step to the other, except for + # the variables the user provided for all time-steps. + roots = collect(dep_graph.roots) + + if nsteps == 1 + for (process, node) in roots + run_node!(object, node, 1, status_flattened, meteo_adjusted, constants, extra) + end + save_results!(status_flattened, outputs_preallocated, 1) + else - for (i, meteo_i) in enumerate(meteo_rows) + for (i, meteo_i) in enumerate(meteo_adjusted) for (process, node) in roots run_node!(object, node, i, status_flattened, meteo_i, constants, extra) end save_results!(status_flattened, outputs_preallocated, i) - i+1 <= nsteps && update_vector_variables(object.status, status_flattened, vector_variables, i + 1) + i + 1 <= nsteps && update_vector_variables(object.status, status_flattened, vector_variables, i + 1) end - - return outputs_preallocated + end + + return outputs_preallocated #=else #TODO breakdown outputs and save them # Computing time-steps in parallel: @@ -272,7 +284,6 @@ function run!( # Each object: for obj in object - if isa(object, AbstractArray) push!(outputs_collection, run!(obj, meteo, constants, extra, outputs=outputs, check=check, executor=executor)) else From 30f49846a359f34185b314e8fbf99bda640c6ceb Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 10 Feb 2025 11:25:33 +0100 Subject: [PATCH 051/147] Add tests for #105, #111, #86 --- test/test-corner-cases.jl | 61 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/test/test-corner-cases.jl b/test/test-corner-cases.jl index b77b02c94..3e30bb118 100644 --- a/test/test-corner-cases.jl +++ b/test/test-corner-cases.jl @@ -495,4 +495,63 @@ end @test model_1.hard_dependency[1].parent == model_1 @test model_1.hard_dependency[2].parent == model_1 - end \ No newline at end of file + end + + + +########################## +## No outputs when simulating a mapping with one meteo timestep #105 +########################## + +@testset "Issue 105 : no outputs when simulating a mapping with one meteo timestep" begin + + using PlantSimEngine, PlantMeteo, DataFrames + using PlantSimEngine.Examples + mtg = import_mtg_example() + m = Dict( + "Leaf" => ( + Process1Model(1.0), + Status(var1=10.0, var2=1.0,) + ) + ) + vars = Dict{String,Any}("Leaf" => (:var1,)) + out = run!(mtg, m, Atmosphere(T=20.0, Wind=1.0, Rh=0.65), outputs=vars, executor=SequentialEx()) + df = outputs(out, DataFrame) + @test DataFrames.nrow(df) == 2 +end + +########################## +## Multiscale : outputs not saved when dependency graph only has one depth level #111 +########################## + +# Probably very similar to #105 +@testset "Issue 111 : Multiscale : outputs not saved when dependency graph only has one depth level" begin + + using Pkg + Pkg.develop("PlantSimEngine") + using PlantSimEngine + using PlantSimEngine.Examples + using MultiScaleTreeGraph + + status2 = (var1=15.0, var2=0.3) + + meteo = Weather([ + Atmosphere(T=25.0, Wind=1.0, Rh=0.6, Ri_PAR_f=200.0), + Atmosphere(T=10.0, Wind=0.5, Rh=0.6, Ri_PAR_f=200.0)]) + + outs = Dict("Default" => (:var1,)) + mtg = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Default", 0, 0),) + + mapping = Dict( + "Default" => ( + Process1Model(1.0), + Status(var1=15.0, var2=0.3,), + ), + ) + + sim = run!(mtg, mapping, meteo; outputs=outs) + using DataFrames + df = outputs(sim, DataFrame) + @test DataFrames.nrow(df) == PlantSimEngine.get_nsteps(meteo) + +end \ No newline at end of file From a0e47cf38e2950a7af579a122e2358829bc0ec9c Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 10 Feb 2025 15:49:37 +0100 Subject: [PATCH 052/147] oops, forgot #86 --- test/test-corner-cases.jl | 46 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/test/test-corner-cases.jl b/test/test-corner-cases.jl index 3e30bb118..a86205e13 100644 --- a/test/test-corner-cases.jl +++ b/test/test-corner-cases.jl @@ -554,4 +554,48 @@ end df = outputs(sim, DataFrame) @test DataFrames.nrow(df) == PlantSimEngine.get_nsteps(meteo) -end \ No newline at end of file +end + + +############################################ +### #86 : BoundsError with a single model and several Weather timesteps +############################################ + +using PlantSimEngine +PlantSimEngine.@process "toy" verbose = false + +""" +Inputs : a, b, c +Outputs : d, e +""" + +struct ToyToyModel{T} <: AbstractToyModel + internal_constant::T +end + +function PlantSimEngine.inputs_(::ToyToyModel) + (a = -Inf, b = -Inf, c = -Inf) +end + +# note : here, d is set with = further down, but e is set with +=, ie inf + thingy, is this a bug on my end ? +function PlantSimEngine.outputs_(::ToyToyModel) + (d = -Inf, e = -Inf) +end + +function PlantSimEngine.run!(m::ToyToyModel, models, status, meteo, constants=nothing, extra_args=nothing) + status.d = m.internal_constant * status.a + status.e += m.internal_constant +end + + +meteo = Weather([ + Atmosphere(T=20.0, Wind=1.0, Rh=0.65, Ri_PAR_f=200.0), + Atmosphere(T=18.0, Wind=1.0, Rh=0.65, Ri_PAR_f=100.0), +]) + +model = ModelList( + ToyToyModel(1), + status = ( a = 1, b = 0, c = 0), + #nsteps = length(meteo) +) +sim = run!(model, meteo) \ No newline at end of file From c9e8d33741fd625d13413678cac0932594492a14 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 10 Feb 2025 15:50:31 +0100 Subject: [PATCH 053/147] Add a mapping bank for more tests --- test/helper-functions.jl | 123 +++++++++++++++++++++++++++++++++++---- test/test-ModelList.jl | 26 ++++++++- 2 files changed, 138 insertions(+), 11 deletions(-) diff --git a/test/helper-functions.jl b/test/helper-functions.jl index 96ff9df80..88f9bef26 100644 --- a/test/helper-functions.jl +++ b/test/helper-functions.jl @@ -55,8 +55,8 @@ meteo = get_weather(43.649777, 3.869889, period, sink = DataFrame)=# function get_simple_meteo_bank() meteos= - [Atmosphere(T=20.0, Wind=1.0, P=101.3, Rh=0.65, Ri_PAR_f=300.0), - #=Weather( + [#=nothing,=# Atmosphere(T=20.0, Wind=1.0, P=101.3, Rh=0.65, Ri_PAR_f=300.0), + 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) @@ -68,7 +68,7 @@ function get_simple_meteo_bank() Atmosphere(T=30.0, Wind=0.5, Rh=0.6, Ri_PAR_f=100.0), Atmosphere(T=20.0, Wind=1.0, Rh=0.6, Ri_PAR_f=200.0), Atmosphere(T=25.0, Wind=1.0, Rh=0.6, Ri_PAR_f=200.0), - Atmosphere(T=10.0, Wind=0.5, Rh=0.6, Ri_PAR_f=200.0)]),=# + Atmosphere(T=10.0, Wind=0.5, Rh=0.6, Ri_PAR_f=200.0)]), CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18), @@ -145,30 +145,133 @@ function get_modellist_bank() outputs_tuples_vectors = [ # this one has one tuple with a duplicate, and one with a nonexistent variable - [(:var1,), #=(:var1, :var1),=# (:var1, :var2), (:var1, :var3), (:var1, :var4, :var5), + [NamedTuple(), (:var1,), #=(:var1, :var1),=# (:var1, :var2), (:var1, :var3), (:var1, :var4, :var5), #=(:var2, :var7, :var3, :var1),=# (:var1, :var2, :var3, :var4, :var5)], - [#=NamedTuple(),=# (:TT_cu,), (:TT_cu,:LAI) , (:biomass,:LAI), (:TT_cu, :LAI, :aPPFD, :biomass, :biomass_increment),], + [NamedTuple(), (:TT_cu,), (:TT_cu,:LAI) , (:biomass,:LAI), (:TT_cu, :LAI, :aPPFD, :biomass, :biomass_increment),], - [#=NamedTuple(),=# (:var1,), (:var1, :var4), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), + [NamedTuple(), (:var1,), (:var1, :var4), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), #=(:var2, :var7, :var3, :var1),=# (:var1, :var2, :var3, :var4, :var5, :var6)], - [#=NamedTuple(),=# (:var1,), (:var1, :var4), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), + [NamedTuple(), (:var1,), (:var1, :var4), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), (:var2, :var7, :var3, :var1), (:var1, :var2, :var3, :var4, :var5, :var6)], - [#=NamedTuple(),=# (:var1,), (:var1, :var4), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), + [NamedTuple(), (:var1,), (:var1, :var4), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), (:var2, :var7, :var3, :var1), (:var1, :var2, :var3, :var4, :var5, :var6) , (:var1, :var2, :var3, :var4, :var5, :var6, :var7, :var8, :var9)], - [#=NamedTuple(),=# (:var1,), #=(:var1, :var1),=# (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), + [NamedTuple(), (:var1,), #=(:var1, :var1),=# (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), (:var2, :var7, :var3, :var1), (:var1, :var2, :var3, :var4, :var5, :var6) - , (:var1, :var2, :var3, :var4, :var5, :var6, :var7, #=:var8, :var9,=# :var0)], + , #=(:var1, :var2, :var3, :var4, :var5, :var6, :var7, :var8, :var9, :var0)=#], ] return models, status_tuples, outputs_tuples_vectors end +# Could add some mtg variation too +function get_simple_mapping_bank() + mappings = [ + Dict( + "Scene" => ToyDegreeDaysCumulModel(), + "Plant" => ( + MultiScaleModel( + model=ToyLAIModel(), + mapping=[:TT_cu => "Scene",],), + Beer(0.6), + MultiScaleModel( + model=ToyCAllocationModel(), + mapping=[ + :carbon_assimilation => ["Leaf"], + :carbon_demand => ["Leaf", "Internode"], + :carbon_allocation => ["Leaf", "Internode"]],), + MultiScaleModel( + model=ToyPlantRmModel(), + mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],],),), + "Internode" => ( + MultiScaleModel( + model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), + mapping=[:TT => "Scene",],), + MultiScaleModel( + model=ToyInternodeEmergence(TT_emergence=20.0), + mapping=[:TT_cu => "Scene"],), + ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004), + Status(carbon_biomass=1.0)), + "Leaf" => ( + MultiScaleModel( + model=ToyAssimModel(), + mapping=[:soil_water_content => "Soil", :aPPFD => "Plant"],), + MultiScaleModel( + model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), + mapping=[:TT => "Scene",],), + ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), + Status(carbon_biomass=1.0)), + "Soil" => (ToySoilWaterModel(),),), +########## + Dict( + "Default" => ( + Process1Model(1.0), + Status(var1=15.0, var2=0.3,),),), +########## + Dict( + "Plant" => ( + MultiScaleModel( + model=ToyCAllocationModel(), + mapping=[ + # inputs + :carbon_assimilation => ["Leaf"], + :carbon_demand => ["Leaf", "Internode"], + # outputs + :carbon_allocation => ["Leaf", "Internode"]],), + MultiScaleModel( + model=ToyPlantRmModel(), + mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],],),), + "Internode" => ( + ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), + ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004), + Status(TT=10.0, carbon_biomass=1.0)), + "Leaf" => ( + MultiScaleModel( + model=ToyAssimModel(), + mapping=[:soil_water_content => "Soil",], + # Notice we provide "Soil", not ["Soil"], so a single value is expected here + ), + ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), + Status(aPPFD=1300.0, TT=10.0, carbon_biomass=1.0), + ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025),), + "Soil" => (ToySoilWaterModel(),),), +################## + ] + +out_vars_vectors = [ + [nothing, + NamedTuple(), + Dict(), + Dict("Leaf" => NamedTuple()), + Dict("Leaf" => (:carbon_allocation,),), #incorrect, wrong scale + Dict("Leaf" => (:carbon_demand,),), #incorrect, wrong scale + Dict( + "Leaf" => (:carbon_assimilation, :carbon_demand, :soil_water_content, :carbon_allocation), + "Internode" => (:carbon_allocation, :TT_cu_emergence), + "Plant" => (:carbon_allocation,), + "Soil" => (:soil_water_content,),),], + ############# + [nothing, NamedTuple(), Dict("Default" => (:var1,))], + ############# + [ + nothing, + NamedTuple(), + Dict( + "Flowers" => (:carbon_assimilation, :carbon_demand), # There are no flowers in this MTG + "Leaf" => (:carbon_assimilation, :carbon_demand, :non_existing_variable), # :non_existing_variable is not computed by any model + "Soil" => (:soil_water_content,), + ),], +] + + return mappings, out_vars_vectors +end + + # Split into two parts to ensure eval() syncs and that automatic generation becomes visible for later simulation # See world-age problems and comments around modellist_to_mapping if you don't know/remember what that's about function test_filtered_output_begin(m::ModelList, status_tuple, requested_outputs, meteo) diff --git a/test/test-ModelList.jl b/test/test-ModelList.jl index f1a4dc1ce..fe7d3631e 100644 --- a/test/test-ModelList.jl +++ b/test/test-ModelList.jl @@ -241,9 +241,33 @@ end=# mtg, mapping, outputs_mapping, nsteps, filtered_outputs_modellist = test_filtered_output_begin(leaf, vals, outs, meteo_day) @test test_filtered_output(mtg, mapping, nsteps, outputs_mapping, meteo_day, filtered_outputs_modellist) - meteos = get_simple_meteo_bank() + meteos = + [Atmosphere(T=20.0, Wind=1.0, P=101.3, Rh=0.65, Ri_PAR_f=300.0), + CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18), + ] modellists, status_tuples, outs_vectors = get_modellist_bank() + # remove some of the currently unhandled cases + outs_vectors = + [ + # this one has one tuple with a duplicate, and one with a nonexistent variable + [(:var1,), #=(:var1, :var1),=# (:var1, :var2), (:var1, :var3), (:var1, :var4, :var5), + #=(:var2, :var7, :var3, :var1),=# (:var1, :var2, :var3, :var4, :var5)], + [#=NamedTuple(),=# (:TT_cu,), (:TT_cu,:LAI) , (:biomass,:LAI), (:TT_cu, :LAI, :aPPFD, :biomass, :biomass_increment),], + [#=NamedTuple(),=# (:var1,), (:var1, :var4), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), + #=(:var2, :var7, :var3, :var1),=# (:var1, :var2, :var3, :var4, :var5, :var6)], + [#=NamedTuple(),=# (:var1,), (:var1, :var4), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), + (:var2, :var7, :var3, :var1), (:var1, :var2, :var3, :var4, :var5, :var6)], + [#=NamedTuple(),=# (:var1,), (:var1, :var4), (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), + (:var2, :var7, :var3, :var1), (:var1, :var2, :var3, :var4, :var5, :var6) + , (:var1, :var2, :var3, :var4, :var5, :var6, :var7, :var8, :var9)], + [#=NamedTuple(),=# (:var1,), #=(:var1, :var1),=# (:var1, :var2), (:var1, :var3), (:var1, :var4, :var6, :var5), + (:var2, :var7, :var3, :var1), (:var1, :var2, :var3, :var4, :var5, :var6) + , (:var1, :var2, :var3, :var4, :var5, :var6, :var7, #=:var8, :var9,=# :var0)], + ] + + + for i in 1:length(modellists) modellist = modellists[i] From 21d859f668c1ae28fc33efb8d048b8076caa1db7 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 10 Feb 2025 18:00:25 +0100 Subject: [PATCH 054/147] Removed remaining parallelisation check in multiscale. Made run! return the outputs only in multiscale, and changed outputs(GraphSimulation, ...) to outputs(Dict{String, O}, ...). Renamed the outputs kwarg to tracked_outputs (haven't done so for GraphSimulation constructor yet). Changed default behaviour of tracked_outputs (if nothing -> get every var ; if NamedTuple() -> only node in multiscale). Fixed some meteo issues. Fixed tests broken by those changes. Added a test that tries combinations of meteo+modellist/mapping+outputs that are valid or return DimensionMismatch. --- src/mtg/GraphSimulation.jl | 31 ++- src/mtg/save_results.jl | 31 ++- src/run.jl | 88 ++++----- test/helper-functions.jl | 42 ++-- test/test-corner-cases.jl | 256 +++++++++++++------------ test/test-mtg-dynamic.jl | 7 +- test/test-mtg-multiscale-cyclic-dep.jl | 15 +- test/test-mtg-multiscale.jl | 189 ++++++++++-------- test/test-simulation.jl | 80 +++++++- 9 files changed, 452 insertions(+), 287 deletions(-) diff --git a/src/mtg/GraphSimulation.jl b/src/mtg/GraphSimulation.jl index 4e49bbd92..7ce16ca08 100644 --- a/src/mtg/GraphSimulation.jl +++ b/src/mtg/GraphSimulation.jl @@ -88,10 +88,9 @@ sim = run!(mtg, mapping, meteo, outputs = Dict( outputs(sim, DataFrames) ``` """ -function outputs(sim::GraphSimulation, sink; refvectors=false, no_value=nothing) +function outputs(outs::Dict{String,O} where O, sink; refvectors=false, no_value=nothing) @assert Tables.istable(sink) "The sink argument must be compatible with the Tables.jl interface (`Tables.istable(sink)` must return `true`, *e.g.* `DataFrame`)" - outs = outputs(sim) variables_names_types = Iterators.flatten(collect(i.first => eltype(i.second[1]) for i in filter(x -> x.first != :node, vars)) for (organs, vars) in outs) |> collect variables_names_types_dict = Dict{Symbol,Any}() @@ -113,6 +112,20 @@ function outputs(sim::GraphSimulation, sink; refvectors=false, no_value=nothing) variables_names_types = (timestep=Int, organ=String, node=Int, NamedTuple(variables_names_types_dict)...) var_names_all = keys(variables_names_types) t = NamedTuple{var_names_all,Tuple{values(variables_names_types)...}}[] + #=size_hint = 0 + for (organ, vars) in outs # organ = "Leaf"; vars = outs[organ] + var_names = setdiff(collect(keys(vars)), [:node]) + if length(var_names) == 0 + continue + end + steps_iterable = axes(vars[var_names[1]], 1) + for timestep in steps_iterable # timestep = 1 + node_iterable = axes(vars[var_names[1]][timestep], 1) + size_hint+=length(node_iterable) + end + end + + sizehint!(t, size_hint)=# for (organ, vars) in outs # organ = "Leaf"; vars = outs[organ] var_names = setdiff(collect(keys(vars)), [:node]) @@ -142,10 +155,16 @@ function outputs(sim::GraphSimulation, sink; refvectors=false, no_value=nothing) return sink(t) end -function outputs(sim::GraphSimulation, key::Symbol) - Tables.columns(outputs(sim, Vector{NamedTuple}))[key] +function outputs(outs::Dict{String, O} where O, key::Symbol) + Tables.columns(outputs(outs, Vector{NamedTuple}))[key] +end + +function outputs(outs::Dict{String, O} where O, i::T) where {T<:Integer} + Tables.columns(outputs(outs, Vector{NamedTuple}))[i] end -function outputs(sim::GraphSimulation, i::T) where {T<:Integer} - Tables.columns(outputs(sim, Vector{NamedTuple}))[i] +# ModelLists now return outputs as a TimeStepTable{Status}, conversion is straightforward +function outputs(out::TimeStepTable{T} where T, sink) + @assert Tables.istable(sink) "The sink argument must be compatible with the Tables.jl interface (`Tables.istable(sink)` must return `true`, *e.g.* `DataFrame`)" + return sink(out) end \ No newline at end of file diff --git a/src/mtg/save_results.jl b/src/mtg/save_results.jl index b93ec0aa2..a912cabdf 100644 --- a/src/mtg/save_results.jl +++ b/src/mtg/save_results.jl @@ -111,6 +111,18 @@ julia> collect(keys(preallocated_vars["Leaf"])) """ function pre_allocate_outputs(statuses, statuses_template, reverse_multiscale_mapping, vars_need_init, outs, nsteps; type_promotion=nothing, check=true) outs_ = Dict{String,Vector{Symbol}}() + + # default behaviour : track everything + if isnothing(outs) + for organ in keys(statuses) + outs_[organ] = keys(statuses_template[organ]) + end + # No outputs requested by user : just return the timestep and node + elseif length(outs) == 0 + for i in keys(statuses) + outs_[i] = [] + end + end for i in keys(outs) # i = "Plant" @assert isa(outs[i], Tuple{Vararg{Symbol}}) """Outputs for scale $i should be a tuple of symbols, *e.g.* `"$i" => (:a, :b)`, found `"$i" => $(outs[i])` instead.""" outs_[i] = [outs[i]...] @@ -201,6 +213,11 @@ from the `status(object)` in the `outputs(object)`. """ function save_results!(object::GraphSimulation, i) outs = outputs(object) + + if length(outs) == 0 + return + end + statuses = status(object) for (organ, vars) in outs @@ -218,13 +235,15 @@ function pre_allocate_outputs(m::ModelList, outs, nsteps; type_promotion=nothing out_keys_requested = Symbol[] if !isnothing(outs) + if length(outs) == 0 # no outputs desired, for some reason + return NamedTuple() + end out_keys_requested = Symbol[outs...] end out_vars_requested = NamedTuple() # default implicit behaviour, track everything if isempty(out_keys_requested) - #m.outputs = (keys(copy(status(m)))...) out_vars_requested = out_vars_all else unexpected_outputs = setdiff(out_keys_requested, status_keys(status(m))) @@ -243,12 +262,7 @@ function pre_allocate_outputs(m::ModelList, outs, nsteps; type_promotion=nothing [delete!(unexpected_outputs, i) for i in unexpected_outputs] end end - - # get the default values from the tuple with all vars obtained from the modellist - #[out_vars_requested = (i => out_vars_all[i], out_vars_requested...) for i in out_keys_requested] - #for i in out_keys_requested - # merge(out_vars_requested, (i => out_vars_all[i],)) - #end + out_defaults_requested = (out_vars_all[i] for i in out_keys_requested) out_vars_requested = (;zip(out_keys_requested, out_defaults_requested)...) end @@ -258,6 +272,9 @@ function pre_allocate_outputs(m::ModelList, outs, nsteps; type_promotion=nothing end function save_results!(status_flattened::Status, outputs, i) + if length(outputs) == 0 + return + end outs = outputs[i] for var in keys(outs) diff --git a/src/run.jl b/src/run.jl index 7af42bf7b..5600f4d16 100644 --- a/src/run.jl +++ b/src/run.jl @@ -87,9 +87,15 @@ julia> (models[:var4],models[:var6]) run! function adjust_weather_timesteps_to_status_length(st::Status, meteo) + # This isn't ideal in terms of codeflow, but check_dimensions will kick in later + # And determine whether there is a status vector length discrepancy status_timesteps_len = get_status_vector_max_length(st) meteo_adjusted = meteo + if DataFormat(meteo_adjusted) == TableAlike() + return Tables.rows(meteo_adjusted) + end + if isnothing(meteo) meteo_adjusted = Weather(repeat([Atmosphere(NamedTuple())], status_timesteps_len)) elseif get_nsteps(meteo) == 1 @@ -98,24 +104,19 @@ function adjust_weather_timesteps_to_status_length(st::Status, meteo) end end - if DataFormat(meteo_adjusted) == TableAlike() - return Tables.rows(meteo_adjusted) - end return meteo_adjusted end -# Managing one or several objects, one or several time-steps: - -# User entry point, which uses traits -# to dispatch to the correct method. The traits are defined in table_traits.jl +# User entry point, which uses traits to dispatch to the correct method. +# The traits are defined in table_traits.jl # and define either TableAlike, TreeAlike or SingletonAlike objects. function run!( object, meteo=nothing, constants=PlantMeteo.Constants(), extra=nothing; - outputs=nothing, + tracked_outputs=nothing, check=true, executor=ThreadedEx() ) @@ -125,12 +126,16 @@ function run!( meteo, constants, extra; - outputs, + tracked_outputs, check, executor ) end +########################################################################################## +## ModelList (single-scale) simulations +########################################################################################## + # 1- several ModelList objects and several time-steps function run!( ::TableAlike, @@ -138,7 +143,7 @@ function run!( meteo::TimeStepTable{A}, constants=PlantMeteo.Constants(), extra=nothing; - outputs=nothing, + tracked_outputs=nothing, check=true, executor=ThreadedEx() ) where {T<:Union{AbstractArray,AbstractDict},A} @@ -149,30 +154,30 @@ function run!( ) maxlog = 1 end - outputs_collection = isa(object, AbstractArray) ? [] : isnothing(outputs) ? Dict() : Dict{TimeStepTable{Status{typeof(outputs)}}} + outputs_collection = isa(object, AbstractArray) ? [] : isnothing(tracked_outputs) ? Dict() : Dict{TimeStepTable{Status{typeof(tracked_outputs)}}} # Each object: for obj in object if isa(object, AbstractArray) - push!(outputs_collection, run!(obj, meteo, constants, extra, outputs=outputs, check=check, executor=executor)) + push!(outputs_collection, run!(obj, meteo, constants, extra, tracked_outputs=tracked_outputs, check=check, executor=executor)) else - outputs_collection[obj.first] = run!(obj.second, meteo, constants, extra, outputs=outputs, check=check, executor=executor) + outputs_collection[obj.first] = run!(obj.second, meteo, constants, extra, tracked_outputs=tracked_outputs, check=check, executor=executor) end end return outputs_collection end -# 2 - one object, one or multiple meteo time-step(s), with vectors provided in the status -# Meaning a single meteo timestep might be expanded to fit the status vector size +# 2 - One object, one or multiple meteo time-step(s), with vectors provided in the status +# (meaning a single meteo timestep might be expanded to fit the status vector size) function run!( ::SingletonAlike, object::T, meteo=nothing, constants=PlantMeteo.Constants(), extra=nothing; - outputs=nothing, + tracked_outputs=nothing, check=true, executor=ThreadedEx() ) where {T<:ModelList} @@ -205,7 +210,7 @@ function run!( ) maxlog = 1 end - outputs_preallocated = pre_allocate_outputs(object, outputs, nsteps) + outputs_preallocated = pre_allocate_outputs(object, tracked_outputs, nsteps) status_flattened, vector_variables = flatten_status(object.status) # Not parallelizable over time-steps, it means some values depend on the previous value. @@ -213,12 +218,12 @@ function run!( # the variables the user provided for all time-steps. roots = collect(dep_graph.roots) - if nsteps == 1 + #=if nsteps == 1 for (process, node) in roots run_node!(object, node, 1, status_flattened, meteo_adjusted, constants, extra) end save_results!(status_flattened, outputs_preallocated, 1) - else + else=# for (i, meteo_i) in enumerate(meteo_adjusted) for (process, node) in roots @@ -227,7 +232,7 @@ function run!( save_results!(status_flattened, outputs_preallocated, i) i + 1 <= nsteps && update_vector_variables(object.status, status_flattened, vector_variables, i + 1) end - end + #end return outputs_preallocated #=else @@ -242,14 +247,14 @@ function run!( end=# end -# 5- several objects and one meteo time-step +# 3- several objects and one meteo time-step function run!( ::TableAlike, object::T, meteo, constants=PlantMeteo.Constants(), extra=nothing; - outputs=nothing, + tracked_outputs=nothing, check=true, executor=ThreadedEx() ) where {T<:Union{AbstractArray, AbstractDict}} @@ -280,14 +285,14 @@ function run!( end end - outputs_collection = isa(object, AbstractArray) ? [] : isnothing(outputs) ? Dict() : Dict{TimeStepTable{Status{typeof(outputs)}}} + outputs_collection = isa(object, AbstractArray) ? [] : isnothing(tracked_outputs) ? Dict() : Dict{TimeStepTable{Status{typeof(tracked_outputs)}}} # Each object: for obj in object if isa(object, AbstractArray) - push!(outputs_collection, run!(obj, meteo, constants, extra, outputs=outputs, check=check, executor=executor)) + push!(outputs_collection, run!(obj, meteo, constants, extra, tracked_outputs=tracked_outputs, check=check, executor=executor)) else - outputs_collection[obj.first] = run!(obj.second, meteo, constants, extra, outputs=outputs, check=check, executor=executor) + outputs_collection[obj.first] = run!(obj.second, meteo, constants, extra, tracked_outputs=tracked_outputs, check=check, executor=executor) end end @@ -296,7 +301,8 @@ end -# for each dependency node in the graph (always one time-step, one object), actual workhorse: +# Not exposed to the user : +# for each dependency node in the graph (always one time-step, one object), actual workhorse function run_node!( object::T, node::SoftDependencyNode, @@ -327,10 +333,13 @@ function run_node!( end -# Compatibility with MTG: +########################################################################################## +### Multiscale simulations +########################################################################################## +# Another user entry point # If we pass an MTG and a mapping, then we use them to compute a GraphSimulation object -# that we use with the first method in this file. +# that we then use with the generic run! entry point. function run!( object::MultiScaleTreeGraph.Node, mapping::Dict{String,T} where {T}, @@ -338,7 +347,7 @@ function run!( constants=PlantMeteo.Constants(), extra=nothing; nsteps=nothing, - outputs=nothing, + tracked_outputs=nothing, check=true, executor=ThreadedEx() ) @@ -349,7 +358,7 @@ function run!( meteo_adjusted = Weather([meteo]) end - sim = GraphSimulation(object, mapping, nsteps=nsteps, check=check, outputs=outputs) + sim = GraphSimulation(object, mapping, nsteps=nsteps, check=check, outputs=tracked_outputs) run!( sim, meteo_adjusted, @@ -359,7 +368,7 @@ function run!( executor=executor ) - return sim + return outputs(sim) end function run!( @@ -368,7 +377,7 @@ function run!( meteo, constants=PlantMeteo.Constants(), extra=nothing; - outputs=nothing, + tracked_outputs=nothing, check=true, executor=ThreadedEx() ) @@ -382,16 +391,17 @@ function run!( for (i, meteo_i) in enumerate(Tables.rows(meteo)) roots = collect(dep_graph.roots) for (process_key, dependency_node) in roots - # Note: parallelization over objects is handled by the run! method below run_node_multiscale!(object, dependency_node, i, models, meteo_i, constants, object, check, executor) end # At the end of the time-step, we save the results of the simulation in the object: save_results!(object, i) end + + return outputs(object) end -# For a tree-alike object: +# Function that runs on dependency graph nodes, actual workhorse : function run_node_multiscale!( object::T, node::SoftDependencyNode, @@ -414,16 +424,6 @@ function run_node_multiscale!( node_statuses = status(object)[node.scale] # Get the status of the nodes at the current scale models_at_scale = models[node.scale] - # Check if the simulation can be parallelized over objects: - #TODO: move this check up in the call stack so we check only once per time-step - if !last(object_parallelizable(node)) && executor != SequentialEx() - check && @warn string( - "A parallel executor was provided (`executor=$(executor)`) but the model $(node.value) (or its hard dependencies) cannot be run in parallel over objects.", - " The simulation will be run sequentially. Use `executor=SequentialEx()` to remove this warning." - ) maxlog = 1 - executor = SequentialEx() - end - for st in node_statuses # for each node status at the current scale (potentially in parallel over nodes) # Actual call to the model: run!(node.value, models_at_scale, st, meteo, constants, extra) diff --git a/test/helper-functions.jl b/test/helper-functions.jl index 88f9bef26..8a0a48f7f 100644 --- a/test/helper-functions.jl +++ b/test/helper-functions.jl @@ -1,24 +1,24 @@ # Simple helper functions that can be used in various tests here and there function compare_outputs_modellist_mapping(filtered_outputs, graphsim) - graphsim_df = outputs(graphsim, DataFrame) + outputs_df = outputs(graphsim.outputs, DataFrame) - graphsim_df_outputs_only = select(graphsim_df, Not([:timestep, :organ, :node])) + outputs_df_outputs_only = select(outputs_df, Not([:timestep, :organ, :node])) models_df = DataFrame(filtered_outputs) models_df_sorted = models_df[:, sortperm(names(models_df))] - graphsim_df_outputs_only_sorted = graphsim_df_outputs_only[:, sortperm(names(graphsim_df_outputs_only))] - return graphsim_df_outputs_only_sorted == models_df_sorted + outputs_df_outputs_only_sorted = outputs_df_outputs_only[:, sortperm(names(outputs_df_outputs_only))] + return outputs_df_outputs_only_sorted == models_df_sorted end # doesn't check for mtg equality function compare_outputs_graphsim(graphsim, graphsim2) - graphsim_df = outputs(graphsim, DataFrame) - graphsim_df_sorted = graphsim_df[:, sortperm(names(graphsim_df))] + outputs_df = outputs(graphsim.outputs, DataFrame) + outputs_df_sorted = outputs_df[:, sortperm(names(outputs_df))] - graphsim2_df = outputs(graphsim2, DataFrame) - graphsim2_df_sorted = graphsim2_df[:, sortperm(names(graphsim2_df))] - return graphsim_df_sorted == graphsim2_df_sorted + outputs2_df = outputs(graphsim2.outputs, DataFrame) + outputs2_df_sorted = outputs2_df[:, sortperm(names(outputs2_df))] + return outputs_df_sorted == outputs2_df_sorted end # Breaking this function into two to ensure eval() state synchronisation happens (see comments around the modellist_to_mapping definition) @@ -247,28 +247,34 @@ out_vars_vectors = [ [nothing, NamedTuple(), Dict(), - Dict("Leaf" => NamedTuple()), - Dict("Leaf" => (:carbon_allocation,),), #incorrect, wrong scale - Dict("Leaf" => (:carbon_demand,),), #incorrect, wrong scale + #Dict("Leaf" => NamedTuple()), # incorrect + Dict("Leaf" => (:carbon_allocation,),), + Dict("Leaf" => (:carbon_demand,),), Dict( "Leaf" => (:carbon_assimilation, :carbon_demand, :soil_water_content, :carbon_allocation), "Internode" => (:carbon_allocation, :TT_cu_emergence), "Plant" => (:carbon_allocation,), "Soil" => (:soil_water_content,),),], ############# - [nothing, NamedTuple(), Dict("Default" => (:var1,))], + [nothing, + NamedTuple(), + Dict("Default" => (:var1,)) + ], ############# [ nothing, NamedTuple(), Dict( - "Flowers" => (:carbon_assimilation, :carbon_demand), # There are no flowers in this MTG - "Leaf" => (:carbon_assimilation, :carbon_demand, :non_existing_variable), # :non_existing_variable is not computed by any model + "Leaf" => (:carbon_assimilation, :carbon_demand), "Soil" => (:soil_water_content,), ),], ] - - return mappings, out_vars_vectors + mtgs = [ + import_mtg_example(), + MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Default", 0, 0 ),), + import_mtg_example() + ] + return mtgs, mappings, out_vars_vectors end @@ -288,7 +294,7 @@ function test_filtered_output_begin(m::ModelList, status_tuple, requested_output @test length(preallocated_outputs[1]) == length(out_vars_all) end - filtered_outputs_modellist = run!(m, meteo; outputs=requested_outputs, executor = SequentialEx()) + filtered_outputs_modellist = run!(m, meteo; tracked_outputs=requested_outputs, executor = SequentialEx()) # compare filtered output of a modellist with the filtered output of the equivalent simulation in multiscale mode mtg, mapping, outputs_mapping = PlantSimEngine.modellist_to_mapping(m, status_tuple; nsteps=nsteps, outputs=requested_outputs) diff --git a/test/test-corner-cases.jl b/test/test-corner-cases.jl index a86205e13..e5b833e3f 100644 --- a/test/test-corner-cases.jl +++ b/test/test-corner-cases.jl @@ -10,13 +10,13 @@ # relates to #77 and #99 -PlantSimEngine.@process "Msg3Lvl_amont" verbose = false -PlantSimEngine.@process "Msg3Lvl_amont2" verbose = false -PlantSimEngine.@process "Msg3Lvl_echelle1" verbose = false -PlantSimEngine.@process "Msg3Lvl_echelle2" verbose = false -PlantSimEngine.@process "Msg3Lvl_echelle3" verbose = false -PlantSimEngine.@process "Msg3Lvl_aval" verbose = false -PlantSimEngine.@process "Msg3Lvl_aval2" verbose = false +PlantSimEngine.@process "Msg3Lvl_amont" verbose = false +PlantSimEngine.@process "Msg3Lvl_amont2" verbose = false +PlantSimEngine.@process "Msg3Lvl_echelle1" verbose = false +PlantSimEngine.@process "Msg3Lvl_echelle2" verbose = false +PlantSimEngine.@process "Msg3Lvl_echelle3" verbose = false +PlantSimEngine.@process "Msg3Lvl_aval" verbose = false +PlantSimEngine.@process "Msg3Lvl_aval2" verbose = false # Roots : amont and amont2 # amont2 points to aval @@ -32,11 +32,11 @@ struct Msg3LvlScaleAmontModel <: AbstractMsg3Lvl_AmontModel end function PlantSimEngine.inputs_(::Msg3LvlScaleAmontModel) - (a = -Inf,) + (a=-Inf,) end function PlantSimEngine.outputs_(::Msg3LvlScaleAmontModel) - (b = -Inf, c = -Inf) + (b=-Inf, c=-Inf) end function PlantSimEngine.run!(::Msg3LvlScaleAmontModel, models, status, meteo, constants=nothing, extra_args=nothing) @@ -50,11 +50,11 @@ struct Msg3LvlScaleAmont2Model <: AbstractMsg3Lvl_Amont2Model end function PlantSimEngine.inputs_(::Msg3LvlScaleAmont2Model) - (a2 = -Inf,) + (a2=-Inf,) end function PlantSimEngine.outputs_(::Msg3LvlScaleAmont2Model) - (b2 = -Inf,) + (b2=-Inf,) end function PlantSimEngine.run!(::Msg3LvlScaleAmont2Model, models, status, meteo, constants=nothing, extra_args=nothing) @@ -68,11 +68,11 @@ end function PlantSimEngine.inputs_(::Msg3LvlScaleEchelle3Model) #(b = -Inf, - (c = -Inf,) + (c=-Inf,) end function PlantSimEngine.outputs_(::Msg3LvlScaleEchelle3Model) - (e3 = -Inf, f3 = -Inf) + (e3=-Inf, f3=-Inf) end function PlantSimEngine.run!(::Msg3LvlScaleEchelle3Model, models, status, meteo, constants=nothing, extra_args=nothing) @@ -87,11 +87,11 @@ end function PlantSimEngine.inputs_(::Msg3LvlScaleEchelle2Model) - (c = -Inf, e3 = -Inf, f3 = -Inf) + (c=-Inf, e3=-Inf, f3=-Inf) end function PlantSimEngine.outputs_(::Msg3LvlScaleEchelle2Model) - (e2 = -Inf, f2 = -Inf) + (e2=-Inf, f2=-Inf) end PlantSimEngine.dep(::Msg3LvlScaleEchelle2Model) = (Msg3Lvl_echelle3=AbstractMsg3Lvl_Echelle3Model => ("E3",),) @@ -109,16 +109,16 @@ struct Msg3LvlScaleEchelle1Model <: AbstractMsg3Lvl_Echelle1Model end function PlantSimEngine.inputs_(::Msg3LvlScaleEchelle1Model) - (b = -Inf, e2 = -Inf, f2 = -Inf) + (b=-Inf, e2=-Inf, f2=-Inf) end function PlantSimEngine.outputs_(::Msg3LvlScaleEchelle1Model) - (e1 = -Inf, f1 = -Inf)#, e3 = -Inf) + (e1=-Inf, f1=-Inf)#, e3 = -Inf) end PlantSimEngine.dep(::Msg3LvlScaleEchelle1Model) = (Msg3Lvl_echelle2=AbstractMsg3Lvl_Echelle2Model => ("E2",),) function PlantSimEngine.run!(::Msg3LvlScaleEchelle1Model, models, status, meteo, constants=nothing, extra_args=nothing) - + status_E2 = extra_args.statuses["E2"][1] run!(extra_args.models["E2"].Msg3Lvl_echelle2, models, status_E2, meteo, constants, extra_args) status.e1 = status.e2 @@ -133,11 +133,11 @@ struct Msg3LvlScaleAval2Model <: AbstractMsg3Lvl_Aval2Model end function PlantSimEngine.inputs_(::Msg3LvlScaleAval2Model) - (i2 = -Inf,) + (i2=-Inf,) end - + function PlantSimEngine.outputs_(::Msg3LvlScaleAval2Model) - (g2 = -Inf,) + (g2=-Inf,) end function PlantSimEngine.run!(::Msg3LvlScaleAval2Model, models, status, meteo, constants=nothing, extra_args=nothing) @@ -150,17 +150,17 @@ struct Msg3LvlScaleAvalModel <: AbstractMsg3Lvl_AvalModel end function PlantSimEngine.inputs_(::Msg3LvlScaleAvalModel) - (e1 = -Inf, f1 = -Inf, b2 = - Inf, g2 = -Inf, e3 = -Inf) + (e1=-Inf, f1=-Inf, b2=-Inf, g2=-Inf, e3=-Inf) end - + function PlantSimEngine.outputs_(::Msg3LvlScaleAvalModel) - (g = -Inf,) + (g=-Inf,) end PlantSimEngine.dep(::Msg3LvlScaleAvalModel) = (Msg3Lvl_aval2=AbstractMsg3Lvl_Aval2Model => ("E2",),) function PlantSimEngine.run!(::Msg3LvlScaleAvalModel, models, status, meteo, constants=nothing, extra_args=nothing) - + status_E2 = extra_args.statuses["E2"][1] run!(extra_args.models["E2"].Msg3Lvl_aval2, models, status_E2, meteo, constants, extra_args) status.g = status.f1 + status.b2 + status_E2.g2 @@ -177,12 +177,12 @@ end MultiScaleModel( model=Msg3LvlScaleAvalModel(), mapping=[:e3 => "E3" => :e3, :b2 => "E2" => :b2, :g2 => "E2" => :g2], - ), + ), MultiScaleModel( model=Msg3LvlScaleEchelle1Model(), mapping=[:e2 => "E2" => :e2, :f2 => "E2" => :f2,], ), Status(a=1.0,)# y = 1.0, z = 1.0) - ), + ), "E2" => ( Msg3LvlScaleAmont2Model(), Msg3LvlScaleAval2Model(), @@ -191,7 +191,7 @@ end mapping=[:c => "E1" => :c, :e3 => "E3" => :e3, :f3 => "E3" => :f3,], ), Status(a2=1.0, i2=1.0,) - ), + ), "E3" => ( MultiScaleModel( model=Msg3LvlScaleEchelle3Model(), @@ -218,12 +218,14 @@ end Node(mtg3Lvl, MultiScaleTreeGraph.NodeMTG("/", "E2", 0, 1)) Node(mtg3Lvl, MultiScaleTreeGraph.NodeMTG("/", "E3", 0, 2)) - sim3Lvl = @test_nowarn PlantSimEngine.run!(mtg3Lvl, mapping3Lvl, meteo3Lvl, outputs=outs3Lvl, executor=SequentialEx()) + #sim3Lvl = @test_nowarn PlantSimEngine.run!(mtg3Lvl, mapping3Lvl, meteo3Lvl, tracked_outputs=outs3Lvl, executor=SequentialEx()) + nsteps = PlantSimEngine.get_nsteps(meteo3Lvl) + sim3Lvl = PlantSimEngine.GraphSimulation(mtg3Lvl, mapping3Lvl, nsteps=nsteps, check=false, outputs=outs3Lvl) + out = run!(sim3Lvl, meteo3Lvl) @test length(sim3Lvl.dependency_graph.roots) == 2 model_amont1 = last(collect(sim3Lvl.dependency_graph.roots)[2]) - model_ech1 = model_amont1.children[1] @test model_ech1.hard_dependency[1].children[1].parent.parent == model_ech1 @@ -236,10 +238,10 @@ end ## Hard dep at another scale, soft dep on the nested model (both at same scale) ####################################################################################################################### -PlantSimEngine.@process "hard_dep_same_scale_echelle1" verbose = false -PlantSimEngine.@process "hard_dep_same_scale_echelle1bis" verbose = false -PlantSimEngine.@process "hard_dep_same_scale_echelle3" verbose = false -PlantSimEngine.@process "hard_dep_same_scale_aval" verbose = false +PlantSimEngine.@process "hard_dep_same_scale_echelle1" verbose = false +PlantSimEngine.@process "hard_dep_same_scale_echelle1bis" verbose = false +PlantSimEngine.@process "hard_dep_same_scale_echelle3" verbose = false +PlantSimEngine.@process "hard_dep_same_scale_aval" verbose = false ################# @@ -248,11 +250,11 @@ end function PlantSimEngine.inputs_(::HardDepSameScaleEchelle3Model) #(b = -Inf, - (d = -Inf,) + (d=-Inf,) end function PlantSimEngine.outputs_(::HardDepSameScaleEchelle3Model) - (e3 = -Inf, f3 = -Inf) + (e3=-Inf, f3=-Inf) end function PlantSimEngine.run!(::HardDepSameScaleEchelle3Model, models, status, meteo, constants=nothing, extra_args=nothing) @@ -266,11 +268,11 @@ struct HardDepSameScaleEchelle1Model <: AbstractHard_Dep_Same_Scale_Echelle1Mode end function PlantSimEngine.inputs_(::HardDepSameScaleEchelle1Model) - (a = -Inf, e2 = -Inf)# e3 = -Inf, f3 = -Inf) + (a=-Inf, e2=-Inf)# e3 = -Inf, f3 = -Inf) end function PlantSimEngine.outputs_(::HardDepSameScaleEchelle1Model) - (e1 = -Inf, f1 = -Inf) + (e1=-Inf, f1=-Inf) end #PlantSimEngine.dep(::HardDepSameScaleEchelle1Model) = (hard_dep_same_scale_echelle3=AbstractHard_Dep_Same_Scale_Echelle3Model => ("E3",),) @@ -290,11 +292,11 @@ struct HardDepSameScaleEchelle1bisModel <: AbstractHard_Dep_Same_Scale_Echelle1B end function PlantSimEngine.inputs_(::HardDepSameScaleEchelle1bisModel) - (e3 = -Inf,) + (e3=-Inf,) end function PlantSimEngine.outputs_(::HardDepSameScaleEchelle1bisModel) - (e2 = -Inf, f2 = -Inf) + (e2=-Inf, f2=-Inf) end PlantSimEngine.dep(::HardDepSameScaleEchelle1bisModel) = (hard_dep_same_scale_echelle3=AbstractHard_Dep_Same_Scale_Echelle3Model => ("E3",),) @@ -314,11 +316,11 @@ struct HardDepSameScaleAvalModel <: AbstractHard_Dep_Same_Scale_AvalModel end function PlantSimEngine.inputs_(::HardDepSameScaleAvalModel) - (e3 = -Inf,) # f1 or f2 ? + (e3=-Inf,) # f1 or f2 ? end - + function PlantSimEngine.outputs_(::HardDepSameScaleAvalModel) - (g = -Inf,) + (g=-Inf,) end function PlantSimEngine.run!(::HardDepSameScaleAvalModel, models, status, meteo, constants=nothing, extra_args=nothing) @@ -336,7 +338,7 @@ end model=HardDepSameScaleEchelle1bisModel(), mapping=[:e3 => "E3" => :e3], ), - Status(a=1.0),), + Status(a=1.0),), "E3" => ( HardDepSameScaleEchelle3Model(), HardDepSameScaleAvalModel(), Status(d=1.0,), @@ -355,10 +357,13 @@ end mtg = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "E1", 0, 0),) Node(mtg, MultiScaleTreeGraph.NodeMTG("/", "E3", 0, 1)) - sim = @test_nowarn PlantSimEngine.run!(mtg, mapping, meteo, outputs=outs, executor = SequentialEx()) + #sim = @test_nowarn PlantSimEngine.run!(mtg, mapping, meteo, tracked_outputs=outs, executor=SequentialEx()) + nsteps = PlantSimEngine.get_nsteps(meteo) + sim = PlantSimEngine.GraphSimulation(mtg, mapping, nsteps=nsteps, check=false, outputs=outs) + out = run!(sim, meteo) model_1 = last(collect(sim.dependency_graph.roots)[1]) - + # Downscale soft dependency aval should point to the root node 1bis, instead of the 'real parent' 3, which is an inner hard dependency to 1bis # so 1 and aval both point to 1bis @test length(model_1.children) == 2 @@ -371,7 +376,7 @@ end ## 2 different scales that make use of the *same* model ####################################################################################################################### -PlantSimEngine.@process "single_model_multiple_scales" verbose = false +PlantSimEngine.@process "single_model_multiple_scales" verbose = false struct SingleModelScale1 <: AbstractSingle_Model_Multiple_ScalesModel end @@ -383,31 +388,31 @@ struct SingleModelScale3 <: AbstractSingle_Model_Multiple_ScalesModel end function PlantSimEngine.inputs_(::SingleModelScale1) - (in = -Inf, in1 = -Inf) + (in=-Inf, in1=-Inf) end function PlantSimEngine.outputs_(::SingleModelScale1) - (out = -Inf, out1 = -Inf) + (out=-Inf, out1=-Inf) end function PlantSimEngine.inputs_(::SingleModelScale2) - (in = -Inf, in2 = -Inf) + (in=-Inf, in2=-Inf) end function PlantSimEngine.outputs_(::SingleModelScale2) - (out = -Inf, out2 = -Inf) + (out=-Inf, out2=-Inf) end function PlantSimEngine.inputs_(::SingleModelScale2bis) - (in = -Inf, in2bis = -Inf) + (in=-Inf, in2bis=-Inf) end function PlantSimEngine.outputs_(::SingleModelScale2bis) - (out = -Inf, out2bis = -Inf) + (out=-Inf, out2bis=-Inf) end function PlantSimEngine.inputs_(::SingleModelScale3) - (in = -Inf, in3 = -Inf, out2 = -Inf, out1 = -Inf) + (in=-Inf, in3=-Inf, out2=-Inf, out1=-Inf) end function PlantSimEngine.outputs_(::SingleModelScale3) - (out = -Inf, out3 = -Inf) + (out=-Inf, out3=-Inf) end PlantSimEngine.dep(::SingleModelScale1) = (single_model_multiple_scales=AbstractSingle_Model_Multiple_ScalesModel => ("E2bis", "E2"),) @@ -418,7 +423,7 @@ function PlantSimEngine.run!(::SingleModelScale1, models, status, meteo, constan status_E2b = sim_object.statuses["E2bis"][1] run!(sim_object.models["E2"].single_model_multiple_scales, models, status_E2, meteo, constants) run!(sim_object.models["E2bis"].single_model_multiple_scales, models, status_E2b, meteo, constants) - status.out = status_E2.out+ status_E2b.out + status.in + status.out = status_E2.out + status_E2b.out + status.in status.out1 = status_E2.out2 + status_E2b.out2bis + status.out1 end @@ -433,7 +438,7 @@ function PlantSimEngine.run!(::SingleModelScale2bis, models, status, meteo, cons end function PlantSimEngine.run!(::SingleModelScale3, models, status, meteo, constants=nothing, sim_object=nothing) - status.out = status.in + status.in3 + status.out2; + status.out = status.in + status.in3 + status.out2 status.out3 = status.in3 + status.out1 end @@ -443,59 +448,60 @@ end @testset "Process/model reuse at different scales" begin mapping = Dict( - "E1" => ( - SingleModelScale1(), - Status(in = 1.0, in1 = 1.0), - ), - "E2" => ( - SingleModelScale2(), - Status(in = 1.0, in2 = 1.0), - ), - "E2bis" => ( - SingleModelScale2bis(), - Status(in = 1.0, in2bis = 1.0), - ), - "E3" => ( - MultiScaleModel( - model = SingleModelScale3(), - mapping = [:out1 => "E1" => :out1, :out2 => "E2" => :out2, ], - ), - Status(in= 1.0, in3 = 1.0,), - ), + "E1" => ( + SingleModelScale1(), + Status(in=1.0, in1=1.0), + ), + "E2" => ( + SingleModelScale2(), + Status(in=1.0, in2=1.0), + ), + "E2bis" => ( + SingleModelScale2bis(), + Status(in=1.0, in2bis=1.0), + ), + "E3" => ( + MultiScaleModel( + model=SingleModelScale3(), + mapping=[:out1 => "E1" => :out1, :out2 => "E2" => :out2,], + ), + Status(in=1.0, in3=1.0,), + ), + ) + + outs = Dict( + "E1" => (:out, :out1), + "E2" => (:out, :out2), + "E2bis" => (:out,), # comment this line out, and remove nodes relating to E2 and E2bis to expose the issue in #103 + "E3" => (:out3,) ) - - outs = Dict( - "E1" => (:out, :out1), - "E2" => (:out, :out2), - "E2bis" => (:out,), # comment this line out, and remove nodes relating to E2 and E2bis to expose the issue in #103 - "E3" => (:out3,) - ) - - meteo = Weather([ - Atmosphere(T=25.0, Wind=1.0, Rh=0.6, Ri_PAR_f=200.0), - Atmosphere(T=10.0, Wind=0.5, Rh=0.6, Ri_PAR_f=200.0) - - ]) - - mtg = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "E1", 0, 0),) - Node(mtg, MultiScaleTreeGraph.NodeMTG("/", "E3", 0, 1)) - Node(mtg, MultiScaleTreeGraph.NodeMTG("/", "E2", 0, 2)) - Node(mtg, MultiScaleTreeGraph.NodeMTG("/", "E2bis", 0, 3)) - - sim = @test_nowarn PlantSimEngine.run!(mtg, mapping, meteo, outputs = outs, executor = SequentialEx()) - - roots = sim.dependency_graph.roots - @test length(sim.dependency_graph.roots) == 1 - - model_1 = last(collect(roots)[1]) - - @test length(model_1.children) == 1 - @test length(model_1.hard_dependency) == 2 - @test model_1.children[1].parent[1] == model_1 - @test model_1.hard_dependency[1].parent == model_1 - @test model_1.hard_dependency[2].parent == model_1 - - end + + meteo = Weather([ + Atmosphere(T=25.0, Wind=1.0, Rh=0.6, Ri_PAR_f=200.0), + Atmosphere(T=10.0, Wind=0.5, Rh=0.6, Ri_PAR_f=200.0)]) + + mtg = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "E1", 0, 0),) + Node(mtg, MultiScaleTreeGraph.NodeMTG("/", "E3", 0, 1)) + Node(mtg, MultiScaleTreeGraph.NodeMTG("/", "E2", 0, 2)) + Node(mtg, MultiScaleTreeGraph.NodeMTG("/", "E2bis", 0, 3)) + + #sim = @test_nowarn PlantSimEngine.run!(mtg, mapping, meteo, tracked_outputs=outs, executor = SequentialEx()) + nsteps = PlantSimEngine.get_nsteps(meteo) + sim = PlantSimEngine.GraphSimulation(mtg, mapping, nsteps=nsteps, check=false, outputs=outs) + out = run!(sim, meteo) + + roots = sim.dependency_graph.roots + @test length(sim.dependency_graph.roots) == 1 + + model_1 = last(collect(roots)[1]) + + @test length(model_1.children) == 1 + @test length(model_1.hard_dependency) == 2 + @test model_1.children[1].parent[1] == model_1 + @test model_1.hard_dependency[1].parent == model_1 + @test model_1.hard_dependency[2].parent == model_1 + +end @@ -515,7 +521,7 @@ end ) ) vars = Dict{String,Any}("Leaf" => (:var1,)) - out = run!(mtg, m, Atmosphere(T=20.0, Wind=1.0, Rh=0.65), outputs=vars, executor=SequentialEx()) + out = run!(mtg, m, Atmosphere(T=20.0, Wind=1.0, Rh=0.65), tracked_outputs=vars, executor=SequentialEx()) df = outputs(out, DataFrame) @test DataFrames.nrow(df) == 2 end @@ -549,7 +555,7 @@ end ), ) - sim = run!(mtg, mapping, meteo; outputs=outs) + sim = run!(mtg, mapping, meteo; tracked_outputs=outs) using DataFrames df = outputs(sim, DataFrame) @test DataFrames.nrow(df) == PlantSimEngine.get_nsteps(meteo) @@ -574,28 +580,30 @@ struct ToyToyModel{T} <: AbstractToyModel end function PlantSimEngine.inputs_(::ToyToyModel) - (a = -Inf, b = -Inf, c = -Inf) + (a=-Inf, b=-Inf, c=-Inf) end # note : here, d is set with = further down, but e is set with +=, ie inf + thingy, is this a bug on my end ? function PlantSimEngine.outputs_(::ToyToyModel) - (d = -Inf, e = -Inf) + (d=-Inf, e=-Inf) end function PlantSimEngine.run!(m::ToyToyModel, models, status, meteo, constants=nothing, extra_args=nothing) - status.d = m.internal_constant * status.a + status.d = m.internal_constant * status.a status.e += m.internal_constant end - -meteo = Weather([ +@testset "Issue #86 : BoundsError with a single model and several Weather timesteps" begin + meteo = Weather([ Atmosphere(T=20.0, Wind=1.0, Rh=0.65, Ri_PAR_f=200.0), Atmosphere(T=18.0, Wind=1.0, Rh=0.65, Ri_PAR_f=100.0), -]) - -model = ModelList( - ToyToyModel(1), - status = ( a = 1, b = 0, c = 0), - #nsteps = length(meteo) -) -sim = run!(model, meteo) \ No newline at end of file + ]) + + model = ModelList( + ToyToyModel(1), + status=(a=1, b=0, c=0), + #nsteps = length(meteo) + ) + sim = run!(model, meteo) + @test DataFrames.nrow(sim) == PlantSimEngine.get_nsteps(meteo) +end \ No newline at end of file diff --git a/test/test-mtg-dynamic.jl b/test/test-mtg-dynamic.jl index bd044b89d..41f38e0d6 100644 --- a/test/test-mtg-dynamic.jl +++ b/test/test-mtg-dynamic.jl @@ -69,10 +69,13 @@ out_vars = Dict( "Soil" => (:soil_water_content,), ) -out = run!(mtg, mapping, meteo, outputs=out_vars, executor=SequentialEx()) +nsteps = PlantSimEngine.get_nsteps(meteo) +sim = PlantSimEngine.GraphSimulation(mtg, mapping, nsteps=nsteps, check=true, outputs=out_vars) +out = run!(sim,meteo) +#out = run!(mtg, mapping, meteo, tracked_outputs=out_vars, executor=SequentialEx()) @testset "MTG with dynamic growth" begin - st = out.statuses + st = sim.statuses @test length(mtg) == 9 @test length(st["Scene"]) == length(st["Soil"]) == length(st["Plant"]) == 1 @test length(st["Internode"]) == length(st["Leaf"]) == 3 diff --git a/test/test-mtg-multiscale-cyclic-dep.jl b/test/test-mtg-multiscale-cyclic-dep.jl index 64c4ecaa7..c6bbe9dac 100644 --- a/test/test-mtg-multiscale-cyclic-dep.jl +++ b/test/test-mtg-multiscale-cyclic-dep.jl @@ -104,8 +104,12 @@ end @test length(cycle_vec) == 7 @test to_initialize(mapping_nocyclic) == Dict() - out = @test_nowarn run!(mtg, mapping_nocyclic, meteo, outputs=out_vars, executor=SequentialEx()) - st = status(out) + + #out = @test_nowarn run!(mtg, mapping_nocyclic, meteo, tracked_outputs=out_vars, executor=SequentialEx()) + nsteps = PlantSimEngine.get_nsteps(meteo) + sim = PlantSimEngine.GraphSimulation(mtg, mapping, nsteps=nsteps, check=true, outputs=out_vars) + out = @test_nowarn run!(sim,meteo) + st = status(sim) st["Leaf"][1].carbon_biomass = 2.0 @test st["Leaf"][2].carbon_biomass != 2.0 @@ -212,7 +216,12 @@ end d = @test_nowarn dep(mapping) @test to_initialize(mapping) == Dict() - out = @test_nowarn run!(mtg, mapping, meteo, outputs=out_vars, executor=SequentialEx()) + #out = @test_nowarn run!(mtg, mapping, meteo, tracked_outputs=out_vars, executor=SequentialEx()) + + nsteps = PlantSimEngine.get_nsteps(meteo) + sim = PlantSimEngine.GraphSimulation(mtg, mapping, nsteps=nsteps, check=true, outputs=out_vars) + out = run!(sim,meteo) + # To update the reference: ref_path = joinpath(pkgdir(PlantSimEngine), "test/references/ref_output_simulation.csv") # CSV.write(ref_path, sort(outputs(out, DataFrame, no_value=missing), [:timestep, :node]), transform=(col, val) -> something(val, missing)) diff --git a/test/test-mtg-multiscale.jl b/test/test-mtg-multiscale.jl index cd3dd7914..1854b1da2 100644 --- a/test/test-mtg-multiscale.jl +++ b/test/test-mtg-multiscale.jl @@ -373,23 +373,32 @@ end end # Testing with a simple mapping (just the soil model, no multiscale mapping): @testset "run! on MTG: simple mapping" begin - out = @test_nowarn run!(mtg, Dict("Soil" => (ToySoilWaterModel(),)), meteo) - @test out.statuses["Soil"][1].node == soil - @test out.models == Dict("Soil" => (soil_water=ToySoilWaterModel(out.models["Soil"].soil_water.values),)) - @test out.models["Soil"].soil_water.values == [0.5] - @test length(out.dependency_graph.roots) == 1 - @test collect(keys(out.dependency_graph.roots))[1] == Pair("Soil", :soil_water) - @test out.graph == mtg + #out = @test_nowarn run!(mtg, Dict("Soil" => (ToySoilWaterModel(),)), meteo) + nsteps = PlantSimEngine.get_nsteps(meteo) + sim = PlantSimEngine.GraphSimulation(mtg, Dict("Soil" => (ToySoilWaterModel(),)), nsteps=nsteps, check=true, outputs=nothing) + out = run!(sim,meteo) + + @test sim.statuses["Soil"][1].node == soil + @test sim.models == Dict("Soil" => (soil_water=ToySoilWaterModel(sim.models["Soil"].soil_water.values),)) + @test sim.models["Soil"].soil_water.values == [0.5] + @test length(sim.dependency_graph.roots) == 1 + @test collect(keys(sim.dependency_graph.roots))[1] == Pair("Soil", :soil_water) + @test sim.graph == mtg leaf_mapping = Dict("Leaf" => (ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), Status(TT=10.0))) - out = run!(mtg, leaf_mapping, meteo) - @test collect(keys(out.statuses)) == ["Leaf"] - @test length(out.statuses["Leaf"]) == 2 - @test out.statuses["Leaf"][1].TT == 10.0 # As initialized in the mapping - @test out.statuses["Leaf"][1].carbon_demand == 0.5 - - @test out.statuses["Leaf"][1].node == leaf1 - @test out.statuses["Leaf"][2].node == leaf2 + + #out = run!(mtg, leaf_mapping, meteo) + nsteps = PlantSimEngine.get_nsteps(meteo) + sim = PlantSimEngine.GraphSimulation(mtg, leaf_mapping, nsteps=nsteps, check=true, outputs=nothing) + out = run!(sim,meteo) + + @test collect(keys(sim.statuses)) == ["Leaf"] + @test length(sim.statuses["Leaf"]) == 2 + @test sim.statuses["Leaf"][1].TT == 10.0 # As initialized in the mapping + @test sim.statuses["Leaf"][1].carbon_demand == 0.5 + + @test sim.statuses["Leaf"][1].node == leaf1 + @test sim.statuses["Leaf"][2].node == leaf2 end # A mapping with all different types of mapping (single, multi-scale, model as is, or tuple of): @@ -422,91 +431,101 @@ end ) # The mapping above should throw an error because TT is not initialized for the Internode: if VERSION < v"1.8" # We test differently depending on the julia version because the format of the error message changed - @test_throws ErrorException run!(mtg, mapping_all, meteo) + + nsteps = PlantSimEngine.get_nsteps(meteo) + sim = PlantSimEngine.GraphSimulation(mtg, mapping_all, nsteps=nsteps, check=true, outputs=nothing) + @test_throws ErrorException out = run!(sim,meteo) else @test_throws "Variable `Rm` is not computed by any model, not initialised by the user in the status, and not found in the MTG at scale Plant (checked for MTG node 3)." run!(mtg, mapping_all, meteo) end # It should work if we don't check the mapping though: - out = @test_nowarn run!(mtg, mapping_all, meteo, check=false) + #out = @test_nowarn run!(mtg, mapping_all, meteo, check=false) + nsteps = PlantSimEngine.get_nsteps(meteo) + sim = PlantSimEngine.GraphSimulation(mtg, mapping_all, nsteps=nsteps, check=false, outputs=nothing) + out = run!(sim,meteo) # Note that the outputs are garbage because the TT is not initialized. - @test out.models == Dict{String,NamedTuple}( - "Soil" => (soil_water=ToySoilWaterModel(out.models["Soil"].soil_water.values),), + @test sim.models == Dict{String,NamedTuple}( + "Soil" => (soil_water=ToySoilWaterModel(sim.models["Soil"].soil_water.values),), "Internode" => (carbon_demand=ToyCDemandModel{Float64}(10.0, 200.0),), "Plant" => (carbon_allocation=ToyCAllocationModel(),), "Leaf" => (carbon_assimilation=ToyAssimModel{Float64}(0.2), carbon_demand=ToyCDemandModel{Float64}(10.0, 200.0)) ) - @test out.models["Soil"].soil_water.values == [0.5] - @test length(out.dependency_graph.roots) == 3 # 3 because the plant is not a root (its model has dependencies) - @test out.statuses["Internode"][1].TT === -Inf - @test out.statuses["Internode"][1].carbon_demand === -Inf + @test sim.models["Soil"].soil_water.values == [0.5] + @test length(sim.dependency_graph.roots) == 3 # 3 because the plant is not a root (its model has dependencies) + @test sim.statuses["Internode"][1].TT === -Inf + @test sim.statuses["Internode"][1].carbon_demand === -Inf - st_leaf1 = out.statuses["Leaf"][1] + st_leaf1 = sim.statuses["Leaf"][1] @test st_leaf1.TT == 10.0 @test st_leaf1.carbon_demand == 0.5 # This one depends on the soil, which is random, so we test using the computation directly: - @test st_leaf1.carbon_assimilation == st_leaf1.aPPFD * out.models["Leaf"].carbon_assimilation.LUE * st_leaf1.soil_water_content + @test st_leaf1.carbon_assimilation == st_leaf1.aPPFD * sim.models["Leaf"].carbon_assimilation.LUE * st_leaf1.soil_water_content @test st_leaf1.carbon_allocation == 0.0 end @testset "run! on MTG with complete mapping (with init)" begin - out = @test_nowarn run!(mtg_init, mapping_1, meteo, executor=SequentialEx()) - - @test typeof(out.statuses) == Dict{String,Vector{Status}} - @test length(out.statuses["Plant"]) == 1 - @test length(out.statuses["Leaf"]) == 2 - @test length(out.statuses["Internode"]) == 2 - @test length(out.statuses["Soil"]) == 1 - @test out.statuses["Soil"][1].node == get_node(mtg_init, 2) - @test out.statuses["Soil"][1].soil_water_content !== -Inf + + #out = @test_nowarn run!(mtg_init, mapping_1, meteo, executor=SequentialEx()) + nsteps = PlantSimEngine.get_nsteps(meteo) + sim = PlantSimEngine.GraphSimulation(mtg_init, mapping_1, nsteps=nsteps, check=false, outputs=nothing) + out = run!(sim,meteo) + + @test typeof(sim.statuses) == Dict{String,Vector{Status}} + @test length(sim.statuses["Plant"]) == 1 + @test length(sim.statuses["Leaf"]) == 2 + @test length(sim.statuses["Internode"]) == 2 + @test length(sim.statuses["Soil"]) == 1 + @test sim.statuses["Soil"][1].node == get_node(mtg_init, 2) + @test sim.statuses["Soil"][1].soil_water_content !== -Inf # Testing that we get the link between the node and its status: - @test out.statuses["Soil"][1] == get_node(mtg_init, 2).plantsimengine_status + @test sim.statuses["Soil"][1] == get_node(mtg_init, 2).plantsimengine_status # Testing if the value in the status of the leaves is the same as the one in the status of the soil: - @test out.statuses["Soil"][1].soil_water_content === out.statuses["Leaf"][1].soil_water_content - @test out.statuses["Soil"][1].soil_water_content === out.statuses["Leaf"][2].soil_water_content + @test sim.statuses["Soil"][1].soil_water_content === sim.statuses["Leaf"][1].soil_water_content + @test sim.statuses["Soil"][1].soil_water_content === sim.statuses["Leaf"][2].soil_water_content - leaf1_status = out.statuses["Leaf"][1] + leaf1_status = sim.statuses["Leaf"][1] # This is the model that computes the assimilation (testing manually that we get the right result here): - @test leaf1_status.carbon_assimilation == leaf1_status.aPPFD * out.models["Leaf"].carbon_assimilation.LUE * leaf1_status.soil_water_content + @test leaf1_status.carbon_assimilation == leaf1_status.aPPFD * sim.models["Leaf"].carbon_assimilation.LUE * leaf1_status.soil_water_content - @test out.statuses["Plant"][1].carbon_demand[[1, 3]] == [i.carbon_demand for i in out.statuses["Internode"]] - @test out.statuses["Plant"][1].carbon_demand[[2, 4]] == [i.carbon_demand for i in out.statuses["Leaf"]] + @test sim.statuses["Plant"][1].carbon_demand[[1, 3]] == [i.carbon_demand for i in sim.statuses["Internode"]] + @test sim.statuses["Plant"][1].carbon_demand[[2, 4]] == [i.carbon_demand for i in sim.statuses["Leaf"]] # Testing the reference directly: - ref_values_cdemand = getfield(out.statuses["Plant"][1].carbon_demand, :v) + ref_values_cdemand = getfield(sim.statuses["Plant"][1].carbon_demand, :v) for (j, i) in enumerate([1, 3]) - @test ref_values_cdemand[i] === PlantSimEngine.refvalue(out.statuses["Internode"][j], :carbon_demand) + @test ref_values_cdemand[i] === PlantSimEngine.refvalue(sim.statuses["Internode"][j], :carbon_demand) end for (j, i) in enumerate([2, 4]) - @test ref_values_cdemand[i] === PlantSimEngine.refvalue(out.statuses["Leaf"][j], :carbon_demand) + @test ref_values_cdemand[i] === PlantSimEngine.refvalue(sim.statuses["Leaf"][j], :carbon_demand) end # Testing that carbon allocation in Leaf and Internode was added as a variable from the model at the Plant scale: - @test hasproperty(out.statuses["Internode"][1], :carbon_allocation) - @test hasproperty(out.statuses["Leaf"][1], :carbon_allocation) + @test hasproperty(sim.statuses["Internode"][1], :carbon_allocation) + @test hasproperty(sim.statuses["Leaf"][1], :carbon_allocation) - @test out.statuses["Internode"][1].carbon_allocation == 0.5 - @test out.statuses["Leaf"][1].carbon_allocation == 0.5 + @test sim.statuses["Internode"][1].carbon_allocation == 0.5 + @test sim.statuses["Leaf"][1].carbon_allocation == 0.5 # Testing that we get the link between the node and its status: - @test out.statuses["Leaf"][2] == get_node(mtg_init, 7).plantsimengine_status + @test sim.statuses["Leaf"][2] == get_node(mtg_init, 7).plantsimengine_status # Testing the reference directly: - ref_values_callocation = getfield(out.statuses["Plant"][1].carbon_allocation, :v) + ref_values_callocation = getfield(sim.statuses["Plant"][1].carbon_allocation, :v) for (j, i) in enumerate([1, 3]) - @test ref_values_callocation[i] === PlantSimEngine.refvalue(out.statuses["Internode"][j], :carbon_allocation) + @test ref_values_callocation[i] === PlantSimEngine.refvalue(sim.statuses["Internode"][j], :carbon_allocation) end for (j, i) in enumerate([2, 4]) - @test ref_values_callocation[i] === PlantSimEngine.refvalue(out.statuses["Leaf"][j], :carbon_allocation) + @test ref_values_callocation[i] === PlantSimEngine.refvalue(sim.statuses["Leaf"][j], :carbon_allocation) end end @@ -537,12 +556,15 @@ end ) ) - out = @test_nowarn PlantSimEngine.run!(mtg, mapping, meteo) + #out = @test_nowarn PlantSimEngine.run!(mtg, mapping, meteo) + nsteps = PlantSimEngine.get_nsteps(meteo) + sim = PlantSimEngine.GraphSimulation(mtg, mapping, nsteps=nsteps, check=false, outputs=nothing) + out = run!(sim,meteo) - @test out.statuses["Leaf"][1].var1 === var1 - @test out.statuses["Leaf"][1].var2 === 1.0 - @test out.statuses["Leaf"][1].var3 === 2.0 - @test out.statuses["Leaf"][1].var6 === 40.4 + @test sim.statuses["Leaf"][1].var1 === var1 + @test sim.statuses["Leaf"][1].var2 === 1.0 + @test sim.statuses["Leaf"][1].var3 === 2.0 + @test sim.statuses["Leaf"][1].var6 === 40.4 end @testset "MTG with complex mapping" begin @@ -590,14 +612,17 @@ end ), ) - out = @test_nowarn PlantSimEngine.run!(mtg, mapping, meteo, executor=SequentialEx()) - - @test length(out.dependency_graph.roots) == 6 - @test out.statuses["Leaf"][1].var1 === 1.01 - @test out.statuses["Leaf"][1].var2 === 1.03 - @test out.statuses["Leaf"][1].var4 ≈ 8.1612000000000013 atol = 1e-6 - @test out.statuses["Leaf"][1].var5 == 32.4806 - @test out.statuses["Leaf"][1].var8 ≈ 1321.0700490800002 atol = 1e-6 + #out = @test_nowarn PlantSimEngine.run!(mtg, mapping, meteo, executor=SequentialEx()) + nsteps = PlantSimEngine.get_nsteps(meteo) + sim = PlantSimEngine.GraphSimulation(mtg, mapping, nsteps=nsteps, check=false, outputs=nothing) + out = run!(sim,meteo) + + @test length(sim.dependency_graph.roots) == 6 + @test sim.statuses["Leaf"][1].var1 === 1.01 + @test sim.statuses["Leaf"][1].var2 === 1.03 + @test sim.statuses["Leaf"][1].var4 ≈ 8.1612000000000013 atol = 1e-6 + @test sim.statuses["Leaf"][1].var5 == 32.4806 + @test sim.statuses["Leaf"][1].var8 ≈ 1321.0700490800002 atol = 1e-6 end @testset "MTG with dynamic output variables" begin @@ -651,21 +676,25 @@ end "Plant" => (:carbon_allocation,), "Soil" => (:soil_water_content,), ) - out = @test_nowarn PlantSimEngine.run!(mtg, mapping, meteo, outputs=out_vars, executor=SequentialEx()) - - @test length(out.dependency_graph.roots) == 6 - @test out.statuses["Leaf"][1].var1 === 1.01 - @test out.statuses["Leaf"][1].var2 === 1.03 - @test out.statuses["Leaf"][1].var4 ≈ 8.1612000000000013 atol = 1e-6 - @test out.statuses["Leaf"][1].var5 == 32.4806 - @test out.statuses["Leaf"][1].var8 ≈ 1321.0700490800002 atol = 1e-6 - - @test out.outputs["Leaf"][:carbon_demand] == [[0.5, 0.5], [0.5, 0.5]] - @test out.outputs["Leaf"][:soil_water_content][1] == fill(out.outputs["Soil"][:soil_water_content][1][1], 2) - @test out.outputs["Leaf"][:soil_water_content][2] == fill(out.outputs["Soil"][:soil_water_content][2][1], 2) - - @test out.outputs["Leaf"][:carbon_allocation] == out.outputs["Internode"][:carbon_allocation] - @test out.outputs["Plant"][:carbon_allocation][1][1][1] === out.outputs["Internode"][:carbon_allocation][1][1] + + #out = @test_nowarn PlantSimEngine.run!(mtg, mapping, meteo, tracked_outputs=out_vars, executor=SequentialEx()) + nsteps = PlantSimEngine.get_nsteps(meteo) + sim = PlantSimEngine.GraphSimulation(mtg, mapping, nsteps=nsteps, check=true, outputs=out_vars) + out = run!(sim,meteo) + + @test length(sim.dependency_graph.roots) == 6 + @test sim.statuses["Leaf"][1].var1 === 1.01 + @test sim.statuses["Leaf"][1].var2 === 1.03 + @test sim.statuses["Leaf"][1].var4 ≈ 8.1612000000000013 atol = 1e-6 + @test sim.statuses["Leaf"][1].var5 == 32.4806 + @test sim.statuses["Leaf"][1].var8 ≈ 1321.0700490800002 atol = 1e-6 + + @test sim.outputs["Leaf"][:carbon_demand] == [[0.5, 0.5], [0.5, 0.5]] + @test sim.outputs["Leaf"][:soil_water_content][1] == fill(sim.outputs["Soil"][:soil_water_content][1][1], 2) + @test sim.outputs["Leaf"][:soil_water_content][2] == fill(sim.outputs["Soil"][:soil_water_content][2][1], 2) + + @test sim.outputs["Leaf"][:carbon_allocation] == sim.outputs["Internode"][:carbon_allocation] + @test sim.outputs["Plant"][:carbon_allocation][1][1][1] === sim.outputs["Internode"][:carbon_allocation][1][1] # Testing the outputs if transformed into a DataFrame: outs = outputs(out, DataFrame) diff --git a/test/test-simulation.jl b/test/test-simulation.jl index a21e4f639..e39c5388d 100644 --- a/test/test-simulation.jl +++ b/test/test-simulation.jl @@ -173,7 +173,6 @@ end; ] end - #TODO @testset "simulation with a dict of objects" begin outputs_vector = run!(Dict("mod1" => models, "mod2" => models2), meteo) @test [[outputs_vector["mod1"][1][i], outputs_vector["mod1"][2][i]] for i in keys(outputs_vector["mod1"])] == [ @@ -212,10 +211,85 @@ end; leaf[:var1] = 15.0 - out = @test_nowarn run!(mtg, mapping, meteo) + #out = @test_nowarn run!(mtg, mapping, meteo) + nsteps = PlantSimEngine.get_nsteps(meteo) + sim = PlantSimEngine.GraphSimulation(mtg, mapping, nsteps=nsteps, check=true) + out = @test_nowarn run!(sim,meteo) vars = (:var4, :var6, :var5, :var1, :var2, :var3) - @test [out.statuses["Leaf"][1][i] for i in vars] == [ + @test [sim.statuses["Leaf"][1][i] for i in vars] == [ 22.0, 61.4, 39.4, 15.0, 0.3, 5.5 ] end; + + +@testset "Meteo+ModelList/mapping+outputs combos either valid or different status vector size vs meteo length either run successfully or return a DimensionMisMatch" begin + + meteos = get_simple_meteo_bank() + modellists, status_tuples, outputs_tuples_vectors = get_modellist_bank() + + for i in 1:length(modellists) +# i = 3 + modellist = modellists[i] + status_tuple = status_tuples[i] + outs_vector = outputs_tuples_vectors[i] + + for j in 1:length(meteos) +# j = 1 + meteo = meteos[j] + for k in 1:length(outs_vector) +# k = 7 + out_tuple = outs_vector[k] + @test try outs_modellist = run!(modellist, meteo; tracked_outputs=out_tuple) + true + catch e + print(i," ", j, " ", k) + println() + if isa(e, DimensionMismatch) + true + elseif isa(e, ErrorException) + showerror(stdout, e) + false + else + showerror(stdout, e) + false + end + end + end + end + end + + mtgs, mappings, outs_tuples_vectors_mappings = get_simple_mapping_bank() + + for i in 1:length(mappings) +# i = 1 + mapping = mappings[i] + outs_vector = outs_tuples_vectors_mappings[i] + + for j in 1:length(meteos) +# j = 1 + meteo = meteos[j] + for k in 1:length(outs_vector) +# k = 4 + out_tuple = outs_vector[k] + + mtg = deepcopy(mtgs[i]) + try + outs_multiscale = run!(mtg, mapping, meteo; tracked_outputs=out_tuple) + @test true + catch e + print(i," ", j, " ", k) + println() + if isa(e, DimensionMismatch) + @test true + #elseif isa(e, ErrorException) + else + #@enter outs_multiscale = run!(mtg, mapping, meteo; tracked_outputs=out_tuple) + showerror(stdout, e) + @test false + end + end + end + end + end +end \ No newline at end of file From 634b296dda5520b2a7f9d6d616dbb181fce18d33 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Tue, 11 Feb 2025 15:08:18 +0100 Subject: [PATCH 055/147] Fix some tests, fix some meteo handling --- src/run.jl | 70 ++++++++++++++++----------- test/downstream/test-PSE-benchmark.jl | 2 +- test/helper-functions.jl | 2 +- test/test-ModelList.jl | 10 ++-- test/test-simulation.jl | 4 +- 5 files changed, 53 insertions(+), 35 deletions(-) diff --git a/src/run.jl b/src/run.jl index 5600f4d16..56c4c90d0 100644 --- a/src/run.jl +++ b/src/run.jl @@ -86,10 +86,12 @@ julia> (models[:var4],models[:var6]) """ run! -function adjust_weather_timesteps_to_status_length(st::Status, meteo) + + +function adjust_weather_timesteps_to_given_length(desired_length, meteo) # This isn't ideal in terms of codeflow, but check_dimensions will kick in later # And determine whether there is a status vector length discrepancy - status_timesteps_len = get_status_vector_max_length(st) + meteo_adjusted = meteo if DataFormat(meteo_adjusted) == TableAlike() @@ -97,10 +99,10 @@ function adjust_weather_timesteps_to_status_length(st::Status, meteo) end if isnothing(meteo) - meteo_adjusted = Weather(repeat([Atmosphere(NamedTuple())], status_timesteps_len)) - elseif get_nsteps(meteo) == 1 + meteo_adjusted = Weather(repeat([Atmosphere(NamedTuple())], desired_length)) + elseif get_nsteps(meteo) == 1 && desired_length > 1 if isa(meteo, Atmosphere) - meteo_adjusted = Weather(repeat([meteo], status_timesteps_len)) + meteo_adjusted = Weather(repeat([meteo], desired_length)) end end @@ -182,7 +184,7 @@ function run!( executor=ThreadedEx() ) where {T<:ModelList} - meteo_adjusted = adjust_weather_timesteps_to_status_length(object.status, meteo) + meteo_adjusted = adjust_weather_timesteps_to_given_length(get_status_vector_max_length(object.status), meteo) nsteps = get_nsteps(meteo_adjusted) dep_graph = dep(object, nsteps) @@ -199,15 +201,17 @@ function run!( end end - #if !timestep_parallelizable(dep_graph) - if executor != SequentialEx() - is_ts_parallel = which_timestep_parallelizable(dep_graph) - mods_not_parallel = join([i.second.first for i in is_ts_parallel[findall(x -> x.second.second == false, is_ts_parallel)]], "; ") - - check && @warn string( - "A parallel executor was provided (`executor=$(executor)`) but some models cannot be run in parallel: $mods_not_parallel. ", - "The simulation will be run sequentially. Use `executor=SequentialEx()` to remove this warning." - ) maxlog = 1 + + if executor != SequentialEx() + if !timestep_parallelizable(dep_graph) + is_ts_parallel = which_timestep_parallelizable(dep_graph) + mods_not_parallel = join([i.second.first for i in is_ts_parallel[findall(x -> x.second.second == false, is_ts_parallel)]], "; ") + + check && @warn string( + "A parallel executor was provided (`executor=$(executor)`) but some models cannot be run in parallel: $mods_not_parallel. ", + "The simulation will be run sequentially. Use `executor=SequentialEx()` to remove this warning." + ) maxlog = 1 + end end outputs_preallocated = pre_allocate_outputs(object, tracked_outputs, nsteps) @@ -218,12 +222,13 @@ function run!( # the variables the user provided for all time-steps. roots = collect(dep_graph.roots) - #=if nsteps == 1 + # this bit is necessary for DataFrameRow meteos, see XPalm tests + if nsteps == 1 for (process, node) in roots run_node!(object, node, 1, status_flattened, meteo_adjusted, constants, extra) end save_results!(status_flattened, outputs_preallocated, 1) - else=# + else for (i, meteo_i) in enumerate(meteo_adjusted) for (process, node) in roots @@ -232,7 +237,7 @@ function run!( save_results!(status_flattened, outputs_preallocated, i) i + 1 <= nsteps && update_vector_variables(object.status, status_flattened, vector_variables, i + 1) end - #end + end return outputs_preallocated #=else @@ -352,12 +357,10 @@ function run!( executor=ThreadedEx() ) isnothing(nsteps) && (nsteps = get_nsteps(meteo)) + meteo_adjusted = adjust_weather_timesteps_to_given_length(nsteps, meteo) - meteo_adjusted = meteo - if nsteps == 1 - meteo_adjusted = Weather([meteo]) - end - + # NOTE : replace_mapping_status_vectors_with_generated_models is assumed to have already run if used + # otherwise there might be vector length conflicts with timesteps sim = GraphSimulation(object, mapping, nsteps=nsteps, check=check, outputs=tracked_outputs) run!( sim, @@ -388,13 +391,24 @@ function run!( !isnothing(extra) && error("Extra parameters are not allowed for the simulation of an MTG (already used for statuses).") - for (i, meteo_i) in enumerate(Tables.rows(meteo)) - roots = collect(dep_graph.roots) + nsteps = get_nsteps(meteo) + + # if this function is called directly with an atmosphere, don't use the Rows interface + if nsteps == 1 + roots = collect(dep_graph.roots) for (process_key, dependency_node) in roots - run_node_multiscale!(object, dependency_node, i, models, meteo_i, constants, object, check, executor) + run_node_multiscale!(object, dependency_node, 1, models, meteo, constants, object, check, executor) + end + save_results!(object, 1) + else + for (i, meteo_i) in enumerate(Tables.rows(meteo)) + roots = collect(dep_graph.roots) + for (process_key, dependency_node) in roots + run_node_multiscale!(object, dependency_node, i, models, meteo_i, constants, object, check, executor) + end + # At the end of the time-step, we save the results of the simulation in the object: + save_results!(object, i) end - # At the end of the time-step, we save the results of the simulation in the object: - save_results!(object, i) end return outputs(object) diff --git a/test/downstream/test-PSE-benchmark.jl b/test/downstream/test-PSE-benchmark.jl index ec5c14f4c..bf93d7288 100644 --- a/test/downstream/test-PSE-benchmark.jl +++ b/test/downstream/test-PSE-benchmark.jl @@ -118,5 +118,5 @@ function do_benchmark_on_heavier_mtg() "Soil" => (:soil_water_content,), ) - out = run!(mtg, mapping, meteo_day, outputs=out_vars, executor=SequentialEx()); + out = run!(mtg, mapping, meteo_day, tracked_outputs=out_vars, executor=SequentialEx()); end \ No newline at end of file diff --git a/test/helper-functions.jl b/test/helper-functions.jl index 8a0a48f7f..e61228e23 100644 --- a/test/helper-functions.jl +++ b/test/helper-functions.jl @@ -30,7 +30,7 @@ function check_multiscale_simulation_is_equivalent_begin(models::ModelList, stat end function check_multiscale_simulation_is_equivalent_end(modellist_outputs, mtg, mapping, out, meteo) - graph_sim = PlantSimEngine.GraphSimulation(mtg, mapping, nsteps=length(meteo), check=true, outputs=out) + graph_sim = PlantSimEngine.GraphSimulation(mtg, mapping, nsteps=PlantSimEngine.get_nsteps(meteo), check=true, outputs=out) sim = run!(graph_sim, meteo, diff --git a/test/test-ModelList.jl b/test/test-ModelList.jl index fe7d3631e..417f12364 100644 --- a/test/test-ModelList.jl +++ b/test/test-ModelList.jl @@ -278,9 +278,13 @@ end=# #insert_errors = true #outs_vector = generate_output_tuple(all_vars, insert_errors) - for meteo in meteos - for out_tuple in outs_vector - meteo_adjusted = PlantSimEngine.adjust_weather_timesteps_to_status_length(modellist.status, meteo) + for j in 1:length(meteos) + meteo = meteos[j] + for k in 1:length(outs_vector) + out_tuple = outs_vector[k] + #print(i, " ", j, " ", k) + meteo_adjusted = PlantSimEngine.adjust_weather_timesteps_to_given_length( + PlantSimEngine.get_status_vector_max_length(modellist.status) , meteo) mtg, mapping, outputs_mapping, nsteps, filtered_outputs_modellist = test_filtered_output_begin(modellist, status_tuple, out_tuple, meteo_adjusted) @test test_filtered_output(mtg, mapping, nsteps, outputs_mapping, meteo_adjusted, filtered_outputs_modellist) end diff --git a/test/test-simulation.jl b/test/test-simulation.jl index e39c5388d..4ff83a64d 100644 --- a/test/test-simulation.jl +++ b/test/test-simulation.jl @@ -51,8 +51,8 @@ end; vars = keys(modellist_outputs) @test [modellist_outputs[i][1] for i in vars] == [34.95, 22.0, 56.95, 15.0, 5.5, 0.3] - mtg, mapping, out = check_multiscale_simulation_is_equivalent_begin(models, status_nt, Weather([meteo])) - @test check_multiscale_simulation_is_equivalent_end(modellist_outputs, mtg, mapping, out, Weather([meteo])) + mtg, mapping, out = check_multiscale_simulation_is_equivalent_begin(models, status_nt, meteo) + @test check_multiscale_simulation_is_equivalent_end(modellist_outputs, mtg, mapping, out, meteo) end; @testset "Simulation: 1 time-step, 1 Atmosphere, 2 objects" begin From 85ea2728061da7b6422ae05e470a65abe911ab8f Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Tue, 11 Feb 2025 15:16:03 +0100 Subject: [PATCH 056/147] Reintroduce XPalm in downstream tests (and benchmarks), now that it is registered. Tests are expected to break until it is updated to conform to the PSE API changes. --- .github/workflows/Integration.yml | 2 +- test/downstream/Project.toml | 1 + test/downstream/test-all-benchmarks.jl | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/Integration.yml b/.github/workflows/Integration.yml index 05f3d0a8a..d4573d091 100644 --- a/.github/workflows/Integration.yml +++ b/.github/workflows/Integration.yml @@ -32,7 +32,7 @@ jobs: arch: - x64 package: - #- {user: PalmStudio, repo: XPalm.jl} + - {user: PalmStudio, repo: XPalm.jl} - {user: VEZY, repo: PlantBioPhysics.jl} steps: - uses: actions/checkout@v4 diff --git a/test/downstream/Project.toml b/test/downstream/Project.toml index 397e54380..783efd236 100644 --- a/test/downstream/Project.toml +++ b/test/downstream/Project.toml @@ -9,3 +9,4 @@ PlantSimEngine = "9a576370-710b-4269-adf9-4f603a9c6423" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +XPalm = "6b523e1e-d512-416c-8e51-a8fbef0064e7" diff --git a/test/downstream/test-all-benchmarks.jl b/test/downstream/test-all-benchmarks.jl index a6bc22c97..1bbfc5155 100644 --- a/test/downstream/test-all-benchmarks.jl +++ b/test/downstream/test-all-benchmarks.jl @@ -22,7 +22,7 @@ elseif Sys.islinux() suite_name = suite_name * "linux" end suite = BenchmarkGroup() -suite[suite_name] = BenchmarkGroup(["PSE", "PBP"])#, "XPalm"]) +suite[suite_name] = BenchmarkGroup(["PSE", "PBP", "XPalm"]) # "PSE benchmark" include("test-PSE-benchmark.jl") @@ -37,8 +37,8 @@ suite[suite_name]["PBP_multiple_timesteps_MT"] = @benchmarkable benchmark_plantb suite[suite_name]["PBP_multiple_timesteps_ST"] = @benchmarkable benchmark_plantbiophysics_multitimestep_ST($leaf, $meteo) # "XPalm benchmark" -#include("test-xpalm.jl") -#suite[suite_name]["XPalm"] = @benchmarkable xpalm_default_param_run() seconds = 120 +include("test-xpalm.jl") +suite[suite_name]["XPalm"] = @benchmarkable xpalm_default_param_run() seconds = 120 tune!(suite) results = run(suite, verbose=true) From e5aaf5cb8a929bbeb9c0e4b21a05cf4db264d9a6 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Fri, 14 Feb 2025 17:32:50 +0100 Subject: [PATCH 057/147] Documentation changes, some drafts --- .../model_coupling/tips_and_workarounds.md | 17 +- docs/src/new_doc/downstream_tests.md | 9 + docs/src/new_doc/implicit_contracts.md | 65 ++++ docs/src/new_doc/key_concepts.md | 92 ++++++ ...lantsimengine_and_julia_troubleshooting.md | 300 ++++++++++++++++++ docs/src/new_doc/why_julia.md | 81 +++++ 6 files changed, 561 insertions(+), 3 deletions(-) create mode 100644 docs/src/new_doc/downstream_tests.md create mode 100644 docs/src/new_doc/implicit_contracts.md create mode 100644 docs/src/new_doc/key_concepts.md create mode 100644 docs/src/new_doc/plantsimengine_and_julia_troubleshooting.md create mode 100644 docs/src/new_doc/why_julia.md diff --git a/docs/src/model_coupling/tips_and_workarounds.md b/docs/src/model_coupling/tips_and_workarounds.md index 8b156d6b9..d10b28f9c 100644 --- a/docs/src/model_coupling/tips_and_workarounds.md +++ b/docs/src/model_coupling/tips_and_workarounds.md @@ -26,9 +26,16 @@ One current limitation of `PlantSimEngine` that can be occasionally awkward is t The reason being that it is usually impossible to automatically determine how the coupling is supposed to work out, when other dependencies latch onto such a model. The user would have to explicitely declare some order of simulation between several models, and some amount of programmer work would also be necessary to implement that extra API feature into `PlantSimEngine`. -We haven't found an approach that was fully satisfactory from both a code simplicity and an API convenience POV. Especially when prototyping and adding in new models, as that might require redeclaring the simulation order for those specific variables. Ideas that came to mind felt like they might constrain the codebase for a more complex API, without enough benefit to justify it. +We haven't found an approach that was fully satisfactory from both a code simplicity and an API convenience POV. Especially when prototyping and adding in new models, as that might require redeclaring the simulation order for those specific variables. -The current workaround, while a little annoying, is very simple : *rename one of the variables*. It is not ideal, of course, as it means you might not be able to use a predefined model 'out of the box', but it does not have any of the tradeoffs and constrained mentioned above. +There are two workarounds : + +One awkward approach is to rename one of the variables. It is not ideal, of course, as it means you might not be able to use a predefined model 'out of the box', but it does not have any of the tradeoffs and constrained mentioned above. + +In many other situations one can work with what PlantSimEngine already provides. + +For example, one model in [XPalm.jl](TODO) handles leaf pruning, affecting biomass. A straightforward implementation would be to have a `leaf_biomass` variable as both input and output. The workaround is to instead output a variable `leaf_biomass_pruning_loss` and to have that as input in the next timestep to compute the new leaf biomass. +TODO example ## Passing in a vector in a mapping status at a specific scale @@ -55,4 +62,8 @@ It will parse your mapping, generate custom models to store and feed the vector cumsum(meteo_day.TT) actually returns a CSV.SentinelArray.ChainedVectors{T, Vector{T}}, which is not a subtype of AbstractVector. Replacing it with Vector(cumsum(meteo_day.TT)) will provide an adequate type. -This feature is likely to break in simulations that make use of planned future features (such as mixing models with different timesteps), without guarantee of a fix on a short notice. Again, bear in mind it is mostly a convenient shortcut for prototyping, when doing multi-scale simulations. \ No newline at end of file +This feature is likely to break in simulations that make use of planned future features (such as mixing models with different timesteps), without guarantee of a fix on a short notice. Again, bear in mind it is mostly a convenient shortcut for prototyping, when doing multi-scale simulations. + +TODO examples of other ad hoc models +TODO state machines ? +TODO workaround status initialisation bug ? \ No newline at end of file diff --git a/docs/src/new_doc/downstream_tests.md b/docs/src/new_doc/downstream_tests.md new file mode 100644 index 000000000..33f0e14d8 --- /dev/null +++ b/docs/src/new_doc/downstream_tests.md @@ -0,0 +1,9 @@ +## Downstream tests + +PlantSimEngine is open sourced on Github [](TODO), and so are its other companion packages, PlantGeom, PlantMeteo, PlantBioPhysics, MultiScaleTreeGraph, and XPalm. + +One handy Continuous Integration feature implemented for these packages is automated integration and downstream testing : after changes to a package, its known downstream dependencies are tested to ensure no breaking changes were introduced. For instance, PlantBioPhysics is used in PlantSimEngine, so an integration test ensures that PlantBioPhysics doesn't break in an unforeseen manner after a new PlantSimEngine release. + +This is something you can take advantage of if you wish to develop using PlantSimEngine, by providing us with your package name (or adding it to the CI yml file in a Pull Request) ; we can then add it to the list of downstream packages to test, and generate PR when breaking changes are introduced. + +## Help improve our documentation ! \ No newline at end of file diff --git a/docs/src/new_doc/implicit_contracts.md b/docs/src/new_doc/implicit_contracts.md new file mode 100644 index 000000000..2a366621f --- /dev/null +++ b/docs/src/new_doc/implicit_contracts.md @@ -0,0 +1,65 @@ +This page details some of the assumptions, coupling constraints and inner workings of PlantSimEngine which may be particular relevant when implementing new models. + +TODO est-ce le meilleur endroit ? + +## Weather data provides the simulation timestep, but models can veer away from it + +The weather data timesteps, whether hourly or daily, provide the pace at which most other models run. + +In XPalm, weather data for most models is provided daily, meaning biomass calculations are also provided daily. + +Many models are considered to be steady-state over that timeframe, but not all : the leaf pruning model pertubes the plant in a non-steady state fashion, for example. Models that require computations over several iterations to stabilise (often part of hard dependencies) might also have a timestep unrelated to the weather data. + +NOTE : TODO +Implicitely, this means any vector variables given as input to the simulation must be consistent with the number of weather timesteps. Providing one weather value but a larger vector variable is an exception : the weather data is replicated over each timestep. (This may be subject to change in the future when support for different timesteps in a single simulation is implemented) + +## Weather data must be interpolated prior to simulation + +If your weather data isn't adjusted to conform to a regular timestep, you will need to adjust it to fit that constraint. PlantSimEngine does no interpolation prior to simulation and expects regular weather timesteps. + +## No cyclic dependencies in the simplified dependency graph + +This has been explained elsewhere TODO : the dependency graph is comprised of soft and hard dependency nodes, and the final version only links soft dependency nodes and is guaranteed to contain no cycles. + +Any user model coupling which causes a cyclic dependency to occur will require some extra tinkering to run : either design models differently, create a hard dependency with some of the problematic models, or break the cycle by having a variable take the previous timestep's value as input. + +Note : Only the previous timestep is accessible in PlantSimEngine without any kind of dedicated model. How to create a model to store more past timesteps of a specific variable is described here TODO + +## Hard dependencies need to be declared in the model definition + +Hard dependencies are handled internally by their owning soft dependency model, ie the hard dep's run! function is directly called by the soft dependency's run!. + +The current way in which PlantSimEngine creates its dependency graph requires users to declare what process is required in the hard dependency and which scale it pulls the model and its variables from. + +## Parallelisation opportunities must be part of the model definition + +Traits that indicate that a model is independent or objects need to be part of the model definition. Modelers need to keep this in mind when implementing new models. + +This is currently mostly a concern for single-scale simulations, as multi-scale simulations are not currently parallelised ; a more involved scheduler would need to be implemented when MTGs are modified by models, and to handle more interesting parallelisation opportunities at specific scales. + +There may be new parallelisation features for multi-plant simulations further down the road. + +## Hard dependencies can only have one parent in the dependency graph + +The final dependency graph is comprised only of soft dependency nodes, and is guaranteed to contain no cycles. Hard dependencies are handled internally by their soft dependency ancestor. To avoid any ambiguity in terms of processing order, only one soft dependency node can 'own' a hard dependency And similarly, nested hard dependencies only have a single soft dependency ancestor. + +This is not solely an implementation detail of PlantSimEngine's internal mechanisms ; if your simulation requires complex coupling, you might need to carefully consider how to manage your hard dependencies, or insert an extra intermediate model to simplify thigns. + +## A model can only be used once per scale + +Similarly, both for graph creation non-ambiguity (and for simulation cohesion), PlantSimEngine currently assumes a model describing a process only occurs once per scale. + +Model renaming and duplicating works around this assumption. It may change once multi-plant/multi-species features are implemented. + +## No two variables with the same name at the same scale + +This rule avoids potential ambiguity which could then cause both problems in terms of model ordering during the simulation, as well as incorrectly coupling models with the wrong variable. + +A workaround for some situations is described here TODO + +## TODO Organs missing in the MTG but declared in the mapping ? + +## Status template intialisation order +TODO + +## Diffusion systems ? \ No newline at end of file diff --git a/docs/src/new_doc/key_concepts.md b/docs/src/new_doc/key_concepts.md new file mode 100644 index 000000000..b8d342bd0 --- /dev/null +++ b/docs/src/new_doc/key_concepts.md @@ -0,0 +1,92 @@ +# Key Concepts + +You'll find a brief description of some of the main concepts and terminology related to and used in PlantSimEngine. + +## Crop models + +## FSPM + +## PlantSimEngine terminology + +### Processes + +A process in this package defines a biological or physical phenomena. Think of any process happening in a system, such as light interception, photosynthesis, water, carbon and energy fluxes, growth, yield or even electricity produced by solar panels. + +## Models + +Models are then implemented for a particular process. + +There may be different models that can be used for the same process ; for instance, there are multiple hypotheses and ways of modeling photosynthesis, with different granularity and accuracy. A simple photosynthesis model might apply a simple formula and apply it to the total leaf surface, a more complex one might calculate interception and light extinction. + +The companion package PlantBiophysics.jl provides the [`Beer`](https://vezy.github.io/PlantBiophysics.jl/stable/functions/#PlantBiophysics.Beer) structure for the implementation of the Beer-Lambert law of light extinction. The process of `light_interception` and the `Beer` model are provided as an example +script in this package too at [`examples/Beer.jl`](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/master/examples/Beer.jl). + +Models can also be used for ad hoc computations that aren't directly tied to a specific literature-defined physiological process. In PlantSimEngine, everything is a model. There are many instances where a custom model might be practical to aggregate some computations or handle other information. To illustrate, XPalm, the Oil Palm model has a few models that handle the state of different organs, and a mdoel to handle leaf pruning, which you can find [here](https://github.com/PalmStudio/XPalm.jl/blob/main/src/plant/phytomer/leaves/leaf_pruning.jl). + +To prepare a simulation, you declare a ModelList with whatever models you wish to make use of. TODO example For multi-scale simulations, a more involved mapping is required, see below. TODO + +### Variables, inputs, outputs, and simple model coupling + +A model used in a simulation requires some input data and parameters, and will compute some other data which may be used by other models. +Depending on what models are combined in a simulation, some variables may be inputs of some models, outputs of other models, only be part of intermediary computations, or be a user input to the whole simulation. + +TODO exemple avec 2-3 modèles + +TODO image de graphe illustrant un couplage + +### Dependency graphs + +Coupling models together in this fashion creates what is known as a Directed Acyclic Graph or DAG. The order in which models are run is determined by the ordering of these models in that graph. + +TODO image + +PlantSimEngine creates this DAG under the hood by plugging the right variables in the right models. Users therefore only need to declare models, they do not need write the code to connect them as PlantSimEngine does that work for them. + +### 'Hard' dependencies + +### Process and Model implementation ? TODO + +### Weather data + +To run a simulation, we usually need the climatic/meteorological conditions measured close to the object or component. + +Users are strongly encouraged to use [`PlantMeteo.jl`](https://github.com/PalmStudio/PlantMeteo.jl), the companion package that helps manage such data, with default pre-computations and structures for efficient computations. The most basic data structure from this package is a type called [`Atmosphere`](https://palmstudio.github.io/PlantMeteo.jl/stable/#PlantMeteo.Atmosphere), which defines steady-state atmospheric conditions, *i.e.* the conditions are considered at equilibrium. Another structure is available to define different consecutive time-steps: [`TimeStepTable`](https://palmstudio.github.io/PlantMeteo.jl/stable/#PlantMeteo.TimeStepTable). + +The mandatory variables to provide for an [`Atmosphere`](https://palmstudio.github.io/PlantMeteo.jl/stable/#PlantMeteo.Atmosphere) are: `T` (air temperature in °C), `Rh` (relative humidity, 0-1) and `Wind` (the wind speed, m s⁻¹). + +In the example below, we also pass in the -optional- incoming photosynthetically active radiation flux (`Ri_PAR_f`, W m⁻²). We can declare such conditions like so: + +```@example usepkg +using PlantMeteo +meteo = Atmosphere(T = 20.0, Wind = 1.0, Rh = 0.65, Ri_PAR_f = 500.0) +``` + +More details are available from the [package documentation](https://vezy.github.io/PlantMeteo.jl/stable). If you do not wish to make use of this package, you can alternately provide your own data, as long as it respects the [Tables.jl interface](https://tables.juliadata.org/stable/#Implementing-the-Interface-(i.e.-becoming-a-Tables.jl-source)). + +If you wish to make use of more fine-grained weather data, it will likely require more advanced model creation and MTG manipulation, and more involved work on the modeling side. TODO + +### Organ/Scale + +Plants have different organs with distinct physiological properties and processes. When doing more fine-grained simulations of plant growth, many models will be tied to a particular organ of a plant. Models handling flowering state or root water absorption are such examples. Others, such as carbon allocation and demand, might be reused in slightly different ways for multiple organs of the plant. + +PlantSimEngine documentation tends to use the terms "organ" and "scale" mostly interchangeably. "Scale" is a bit more general and accurate, since some models might not operate at a specific organ level, but (for example) at the scene level, so a "Scene" scale might be present in the MTG, and in the user-provided data. + +### Mapping, multiscale simulations + +When running multi-scale simulations which contain models operating at different organ levels for the plant, extra information needs to be provided by the user to run models. Since some models are reused at different organ levels, it is necessary to indicate which organ level a model operates at. + +This is why multi-scale simulations make use of a 'mapping' : the ModelList in the single-scale examples does not have a way to tie models to plant organs,and the more versatile models could be used in various places. The user must also indicate how models operate with other scales, e.g. if an input variable comes from another scale TODO example, it is required to indicate which scale it is mapped from. + +### Multi-scale Tree Graphs + +Multi-scale Tree Graphs (MTG) are a data structure used to represent plants. A more detailed introduction to the format and its attributes can be found [in the MultiScaleTreeGraph.jl package documentation](https://vezy.github.io/MultiScaleTreeGraph.jl/stable/the_mtg/mtg_concept/). + +Multi-scale simulations can operate on MTG objects ; new nodes are added corresponding to new organs created during the plant's growth. + +Another companion package, [PlantGeom.jl](https://github.com/VEZY/PlantGeom.jl), can also create MTG objects from .opf files (corresponding to the [Open Plant Format](https://amap-dev.cirad.fr/projects/xplo/wiki/The_opf_format_(*opf)), an alternate means of describing plants computationally). + +TODO example ? +TODO image ? +TODO lien avec AMAP ? + +### State machines \ No newline at end of file diff --git a/docs/src/new_doc/plantsimengine_and_julia_troubleshooting.md b/docs/src/new_doc/plantsimengine_and_julia_troubleshooting.md new file mode 100644 index 000000000..dc6c12cd3 --- /dev/null +++ b/docs/src/new_doc/plantsimengine_and_julia_troubleshooting.md @@ -0,0 +1,300 @@ +# Troubleshooting error messages (both PlantSimEngine and Julia) + +PlantSimEngine attempts to be as comfortable and easy to use as possible for the user, and many kinds of user error will be caught and explanations provided to resolve them, but there are still many blind spots, as well as syntax errors that will often generate a Julia error (which can be unintuitive to decrypt) rather than a PlantSimEngine error. + +To help people newer to Julia with troubleshooting, here are a few common 'easy-to-make' mistakes with the current API that might not be obvious to interpret, and pointers on how to fix them. + +They are listed by 'nature of error', rather than by error message, so you may need to search the page to find your specific error. + +## Tips and workflow + +Some errors are very specific as to their cause, and the PlantSimEngine errors tend to be explicit about which parameter / variable / organ is causing the error, helping narrow down its origin. + +Some generic-looking errors usually do contain some extra information to help focus the debugging hunt. For instance, a dispatch failure on run! caused by some issue with args/kwargs may highlight explicitely indicate which arguments are currently causing conflict. In VSCode, such arguments are highlighted in red (the first and last arguments in the example below) : + +```julia +a = 1 +run!(a, simple_mtg, mapping, meteo_day, a) + +ERROR: MethodError: no method matching run!(::Int64, ::Node{NodeMTG, Dict{…}}, ::Dict{String, Tuple{…}}, ::DataFrame, ::Int64) +The function `run!` exists, but no method is defined for this combination of argument types. + +Closest candidates are: + run!(::ToyPlantLeafSurfaceModel, ::Any, ::Any, ::Any, ::Any, ::Any) + @ PlantSimEngine /PlantSimEngine/examples/ToyLeafSurfaceModel.jl:75 + ... +``` + +If you wish to search for a specific error in the current page, copy the part of the description that is not specific to your script, and Ctrl+F it here. In the above example, the generic part would be : +```julia +ERROR: MethodError: no method matching +``` + +TODO forum, github +TODO + +## Common Julia errors + +### NamedTuples with a single value require a comma : + +This one is easy to miss. + +Empty NamedTuple objects are initialised with x = NamedTuple(). Ones with more than one variable can be initialised like this : +```julia +a = (var1 = 0, var2 = 0) +``` +or like this : +```julia +a = (var1 = 0, var2 = 0,) +``` +The second comma being optional. + +However, if there is only a single variable, notation has to be : +```julia +a = (var1 = 0,) +``` +The comma is compulsory. If it is forgotten : +```julia +a = (var1 = 0) +``` +the line will be interpreted as setting the variable a to the value var1 is set to, hence a will be an Int64 of value 0. + +This is a liability when writing custom models as some functions work with NamedTuples : +```julia +function PlantSimEngine.inputs_(::HardDepSameScaleAvalModel) + (e2 = -Inf,) +end +``` + +The error returned will likely be a Julia error along the lines of : +```julia +[ERROR: MethodError: no method matching merge(::Float64, ::@NamedTuple{g::Float64}) + +Closest candidates are: +merge(::NamedTuple{()}, ::NamedTuple) +@ Base namedtuple.jl:337 +merge(::NamedTuple{an}, ::NamedTuple{bn}) where {an, bn} +@ Base namedtuple.jl:324 +merge(::NamedTuple, ::NamedTuple, NamedTuple...) +@ Base namedtuple.jl:343 + +Stacktrace: +[1] variables_multiscale(node::PlantSimEngine.HardDependencyNode{…}, organ::String, vars_mapping::Dict{…}, st::@NamedTuple{}) +... +``` +It is sometimes properly detected and explained on PlantSimEngine's side (when passing in tracked_outputs, for instance), but may also occur when declaring statuses. + +### Incorrectly declaring empty inputs or outputs + +The syntax for an empty NamedTuple is `NamedTuple()`. If instead one types `()` or `(,)`an error returned respectively by PlantSimEngine or Julia will be returned. + +### Forgetting a kwarg when declaring a MultiScaleModel + +A MultiScaleModel requires two kwargs, model and mapping : + +```julia +models = MultiScaleModel( + model=ToyLAIModel(), + mapping=[:TT_cu => "Scene",], + ) +``` + +Forgetting 'model=' : + +```julia +models = MultiScaleModel( + ToyLAIModel(), + mapping=[:TT_cu => "Scene",], + ) +ERROR: MethodError: no method matching MultiScaleModel(::ToyLAIModel; mapping::Vector{Pair{Symbol, String}}) +The type `MultiScaleModel` exists, but no method is defined for this combination of argument types when trying to construct it. + +Closest candidates are: + MultiScaleModel(::T, ::Any) where T<:AbstractModel got unsupported keyword argument "mapping" + @ PlantSimEngine PlantSimEngine/src/mtg/MultiScaleModel.jl:188 + MultiScaleModel(; model, mapping) + @ PlantSimEngine PlantSimEngine/src/mtg/MultiScaleModel.jl:191 +``` + +Forgetting 'mapping=' : +```julia +models = MultiScaleModel( + model=ToyLAIModel(), + [:TT_cu => "Scene",], + ) + +ERROR: MethodError: no method matching MultiScaleModel(::Vector{Pair{Symbol, String}}; model::ToyLAIModel) +The type `MultiScaleModel` exists, but no method is defined for this combination of argument types when trying to construct it. + +Closest candidates are: + MultiScaleModel(; model, mapping) + @ PlantSimEngine PlantSimEngine/src/mtg/MultiScaleModel.jl:191 + MultiScaleModel(::T, ::Any) where T<:AbstractModel got unsupported keyword argument "model" +``` + +The message 'got unsupported keyword argument "model"' can be misleading, as in the error in this case is not that a kwarg is *unsupported*, but rather that a keyword argument is *missing*. + +### Kwarg and arg parameter issues when calling run! + +There are, unfortunately, multiple ways of passing in arguments to the run! functions that will confuse dynamic dispatch. Some of it is due to imperfections in type declarations on PlantSimEngine's end and may be improved upon in the future. + +Here are a few examples when modifying the usual multiscale run! call in this working example : + +```julia + meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) + mtg = Node(MultiScaleTreeGraph.NodeMTG("/", "Plant", 1, 1)) + var1 = 15.0 + + mapping = Dict( + "Leaf" => ( + Process1Model(1.0), + Process2Model(), + Process3Model(), + Status(var1=var1,) + ) + ) + + outs = Dict( + "Leaf" => (:var1,), # :non_existing_variable is not computed by any model + ) + +run!(mtg, mapping, meteo_day, PlantMeteo.Constants(), tracked_outputs=outs) +``` + +The exact signature is this : +```julia +function run!( + object::MultiScaleTreeGraph.Node, + mapping::Dict{String,T} where {T}, + meteo=nothing, + constants=PlantMeteo.Constants(), + extra=nothing; + nsteps=nothing, + tracked_outputs=nothing, + check=true, + executor=ThreadedEx() +``` + +Arguments after the mtg and mapping all have a default value and are optional, and arguments after the ';' delimiter are kwargs and need to be named. + +If one forgets the mtg, a flaw in the way run! is defined will lead to this error : +```julia +run!(mapping, meteo_day, PlantMeteo.Constants(), tracked_outputs=outs) + +ERROR: MethodError: no method matching check_dimensions(::PlantSimEngine.TableAlike, ::Tuple{…}, ::DataFrame) +The function `check_dimensions` exists, but no method is defined for this combination of argument types. + +Closest candidates are: + check_dimensions(::Any, ::Any) + @ PlantSimEngine PlantSimEngine/src/checks/dimensions.jl:43 + ... +``` + +If one forgets the necessary 'tracked_outputs=' in the definition, outs will be interpreted as the 'extra' arg instead of a kwarg. 'extra' usually defaults to nothing, and is reserved in multiscale mode, leading to the following error : + +```julia +run!(mtg, mapping, meteo_day, PlantMeteo.Constants(), outs) + +ERROR: Extra parameters are not allowed for the simulation of an MTG (already used for statuses). +Stacktrace: + [1] error(s::String) + @ Base ./error.jl:35 + [2] run!(::PlantSimEngine.TreeAlike, object::PlantSimEngine.GraphSimulation{…}, meteo::DataFrames.DataFrameRows{…}, constants::Constants{…}, extra::Dict{…}; tracked_outputs::Nothing, check::Bool, executor::ThreadedEx{…}) +``` + +In case of a more generic error that returns a +For example, if one does the opposite and adds a non-existent kwarg, the generic dispatch failure has some more specific information : +`got unsupported keyword argument "constants"` + +```julia +run!(mtg, mapping, meteo_day, constants=PlantMeteo.Constants(), tracked_outputs=outs) + +ERROR: MethodError: no method matching run!(::Node{…}, ::Dict{…}, ::DataFrame, ::Dict{…}, ::Nothing; constants::Constants{…}) +This error has been manually thrown, explicitly, so the method may exist but be intentionally marked as unimplemented. + +Closest candidates are: + run!(::Node, ::Dict{String}, ::Any, ::Any, ::Any; nsteps, tracked_outputs, check, executor) got unsupported keyword argument "constants" +``` + +### Hard dependency process not present in the mapping + +Another weakness in the current error checking leads to an unclear Julia error if a model A is present in a mapping and has a hard dependency on a model B, but B is absent from the mapping. + +In the following example, A corresponds to Process3Model, which requires a model B implementing 'Process2Model' and referred to as 'process2'. +Looking at the source code for Process3Model, the hard dependency is declared here : +```julia +PlantSimEngine.dep(::Process3Model) = (process2=Process2Model,) +``` + +However, the model provided in the examples, Process2Model is absent from the mapping : + +```julia + simple_mtg = Node(MultiScaleTreeGraph.NodeMTG("/", "Plant", 1, 1)) + mapping = Dict( + "Leaf" => ( + Process3Model(), + Status(var5=15.0,) + ) + ) + outs = Dict( + "Leaf" => (:var5,), + ) +run!(simple_mtg, mapping, meteo_day, tracked_outputs=outs) + +ERROR: type NamedTuple has no field process2 +Stacktrace: + [1] getproperty(x::@NamedTuple{process3::Process3Model}, f::Symbol) + @ Base ./Base.jl:49 + [2] run!(::Process3Model, models::@NamedTuple{…}, status::Status{…}, meteo::DataFrameRow{…}, constants::Constants{…}, extra::PlantSimEngine.GraphSimulation{…}) + ... + ``` + +The fix is to add Process2Model() -or an other model for the same process- to the mapping. + +### Status kwargs ? +TODO + +## Forgetting to declare a scale in the mapping but having variables point to it + +If there is a need to collect variables at two different scales, and one scale is completely absent from the mapping, the error currently occurs on the Julia side : + +```julia +"E2" => ( + MultiScaleModel( + model = HardDepSameScaleEchelle2Model(), + mapping = [:c => "E1" => :c, :e3 => "E3" => :e3, :f3 => "E3" => :f3,], + ), + ), +# No E3 in the mapping ! + +Exception has occurred: KeyError +* +KeyError: key "E3" not found +Stacktrace: +[1] hard_dependencies(mapping::Dict{String, Tuple{Any, Any}}; verbose::Bool) +@ PlantSimEngine ......./src/dependencies/hard_dependencies.jl:175 +... +``` + +### Parenthesis placement when declaring a mapping + +An unintuitive error encountered in the past when defining a mapping : + +```julia +ERROR: ArgumentError: AbstractDict(kv): kv needs to be an iterator of 2-tuples or pairs +``` +may occur when forgetting the parenthesis after '=>' in a mapping declaration, and combining it with another parenthesis error. +```julia +mapping = Dict( "Scale" => (ToyAssimGrowthModel(0.0, 0.0, 0.0), ToyCAllocationModel(), Status( TT_cu=Vector(cumsum(meteo_day.TT))), ), ) +``` + +Other errors such as : +```julia +ERROR: MethodError: no method matching Dict(::Pair{String, ToyAssimGrowthModel{Float64}}, ::ToyCAllocationModel, ::Status{(:TT_cu,), Tuple{Base.RefValue{…}}}) +The type `Dict` exists, but no method is defined for this combination of argument types when trying to construct it. + +Closest candidates are: + Dict(::Pair{K, V}...) where {K, V} +``` +often indicate a likely syntax error somewhere in the mapping definition. + diff --git a/docs/src/new_doc/why_julia.md b/docs/src/new_doc/why_julia.md new file mode 100644 index 000000000..90288a14f --- /dev/null +++ b/docs/src/new_doc/why_julia.md @@ -0,0 +1,81 @@ +PlantSimEngine is implemented in Julia. It arose from a particular combination of [needs and requirements](TODO), a combination which Julia seemed to fill adequately. + +Other modelling frameworks, FSPMs and crop models are -often- written in combinations of Java, C++, Python, or Fortran. Given that it isn't the language many researchers (and developers !) are most familiar with, this page provides a short explanation of the reasoning behind that language choice. It might not have been the only possible valid choice, of course. + +## A researcher-developer tool + +PlantSimEngine is a goal oriented framework. Its features arose -and continue to evolve- out of necessity for more and more complex simulations. It wasn't a pre-designed piece of software. + +It was therefore originally a means to an end, and not a product in itself. + +TODO + +## PlantSimEngine's Constraints + +### Performance + +While computers have gained several orders of magnitude of power and memory over the past few decades, to the point where many prior performance bottlenecks have vanished, performance can still be a limiting factor. + +Simulating multiple processes with user-provided variables over many plants with tens of thousands of leaves requires a lot of computation. Using a higher-level language such as Python or R would not lead to adequate simulation times. + +In fact, part of the initial motivation to commit to Julia happened after porting an ecophysiological simulation from R to Julia and getting an order of magnitude difference in performance 'out-of-the-box'. + +Julia, with its 'Just-ahead-of-time' compilation model and its flexibility allowing to do some lower-level optimisation, doesn't suffer from the limitations one would encounter by using only Python or R. + +### Flexibility, ease of use + +PlantSimEngine was also developed with a few goals in mind, one of them being to make hypothesis testing quite easy. It is currently difficult to validate FSPM, crop model or ecophysiological hypotheses because TODO + +Similarly, when developing a full-featured FSPM, there might be a need to test different models for a specific process, or to switch a model for a more complex one. API and language ease of use is as much of a factor as automated model coupling in keeping these changes smooth. + +### Packages destined to be used by a wider community + +As mentioned earlier, PlantSimEngine is intended for a wide audience. Only few of them are expected to have a strong development background. Many other potential users might be researchers more well-versed in ecophysiology or plant architecture and only know a little bit of Python, Matlab or R. Reducing friction for these users is paramount. + +Open-source libraries/packages, ease of installation and low entry barrier also factor in the decision. + +### Modularity and flexibility while retaining performance + +One approach could be to combine, say, Python, with a more performant language such as C++ or Fortran. The slower but flexible language being used for prototyping, and when performance is required, some chunks are reimplmented in the other language. + +This fits the performance constraint, but has a few caveats. + +### Low developer bandwidth + +And of course, budget, time and resources are a concern. The more autonomous researchers and modelers are, and the less specialist developer/engineering resources are required, the easier it is for the project to keep evolving. + +## Comparison + +### The Two-Language Problem + +Combining two different languages requires a lot of language expertise, with constant knowledge refreshing, as one might only occasionally work with and debug with the lower-level language. Or more engineering resources. + +Speed of iteration is also lost whenever performance is a concern, which happens often in our context. However modular and easy-to-use a language like Python might be, whenever it's time to switch to a low-level language, development speed will slow down. + +Julia, while likely being a little harder to learn than Python, and require extra knowledge to properly make use of its flexibility and performance capabilities, leads to a significantly smoother development experience. + +Everything can be done using Julia exclusively, so there is no need to learn two languages. No need to interface between them. Iteration speed doesn't suddenly grind to a halt if a low-level implementation is needed. A competent researcher-developer might have less need of engineering resources, while still being able to work a lot on modeling and the actual plant side of things. + +TODO image ML + +It seems we aren't the only ones to feel Julia is the right tool for our job for those reasons. Indeed, other niches where Julia seems to thrive tend to be other computationally heavy areas with much active research, such as ML and climate modeling. + +### A good balance in terms of accessibility + +Another argument in favour of Julia is that one of the aims for PlantSimEngine is to be easy-to-use for researchers wishing to test hypothesis, or reproduce results from other papers. TODO + +Many researchers are not developers by trade or heart, and a Java-only or C++-only implementation, on top of the earlier points, would not be accessible enough and would not gain much traction. + +Julia, while less ubiquitous than other languages in research circles, resembles Python and R and is more beginner-friendly than Java or C++. It is easier for a Python user to learn to use a simple Julia package than a C++ one. + +Users will also find it easier to quickly implement new models without the potential hurdle of a low-level implementation, or some language interfacing also being required. The prototyping phase doesn't require a subsequent performance tuning phase. + +### Ease of environment setup + +Similarly, Julia's language and package installation is -mostly- fairly straightforward and requires little additional knowledge. + +### Downsides acceptable + +While very practical for a 'researcher-developer', Julia is of course far from being the perfect language in every discipline. It is massive in terms of features, has a heavy runtime, is more involved to learn and master quickly compared to Python, has a few hurdles for beginners, some quirks that can be awkward for developers, tools that aren't fully mature, no clear 'recommended' workflow, and so on. + +The cost for switching may not be worth it in many other circumstances. However, several of these downsides, while very relevant for embedded systems, or game development, are much less relevant regarding PlantSimEngine. And others can be mitigated with, hopefully, adequate learning resources and documentation. \ No newline at end of file From 66bbdbdd8dda45792ae505099367dca2052a6459 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 17 Feb 2025 16:46:40 +0100 Subject: [PATCH 058/147] Simple and completely unrealistic example plant simulation to illustrate MTG growth --- examples/ToyPlantSimulation.jl | 239 +++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 examples/ToyPlantSimulation.jl diff --git a/examples/ToyPlantSimulation.jl b/examples/ToyPlantSimulation.jl new file mode 100644 index 000000000..2d239bbd8 --- /dev/null +++ b/examples/ToyPlantSimulation.jl @@ -0,0 +1,239 @@ +########################################### +# Toy plant model +# Physiologically and physically completely meaningless +# (no dimension for units, arbitrary values, stores water and carbon in abstract stocks, +# arbitrary max leaf count and root length, constant and non-coupled photosynthesis and water absorption, ...) +# But it should illustrate the basics of simulating a growing multiscale plant with PlantSimEngine's model approach +########################################### + +function get_root_end_node(node::MultiScaleTreeGraph.Node) + root = MultiScaleTreeGraph.get_root(node) + return MultiScaleTreeGraph.traverse(root, x->x, symbol="Root", filter_fun = MultiScaleTreeGraph.isleaf) +end + +function get_roots_count(node::MultiScaleTreeGraph.Node) + root = MultiScaleTreeGraph.get_root(node) + return length(MultiScaleTreeGraph.traverse(root, x->x, symbol="Root")) +end + +function get_leaves_total_surface(node::MultiScaleTreeGraph.Node, leaf_surface) + root = MultiScaleTreeGraph.get_root(node) + nleaves = length(MultiScaleTreeGraph.traverse(root, x->1, symbol="Leaf")) + return leaf_surface*nleaves +end + +PlantSimEngine.@process "organ_emergence" verbose = false + +struct ToyInternodeEmergence <: AbstractOrgan_EmergenceModel + TT_emergence::Float64 + carbon_internode_creation_cost::Float64 + leaves_max_surface_area::Float64 + water_leaf_threshold::Float64 +end + +ToyInternodeEmergence(;TT_emergence=300.0, carbon_internode_creation_cost=200.0, leaves_max_surface_area=100.0, +water_leaf_threshold=30.0) = ToyInternodeEmergence(TT_emergence, carbon_internode_creation_cost, leaves_max_surface_area, water_leaf_threshold) + +PlantSimEngine.inputs_(m::ToyInternodeEmergence) = (TT_cu=0.0,water_stock=0.0, carbon_stock=0.0) +PlantSimEngine.outputs_(m::ToyInternodeEmergence) = (TT_cu_emergence=0.0, carbon_organ_creation_consumed=0.0) + +function PlantSimEngine.run!(m::ToyInternodeEmergence, models, status, meteo, constants=nothing, sim_object=nothing) + + leaves_surface_area = get_leaves_total_surface(status.node, 3.0) + status.carbon_organ_creation_consumed = 0.0 + + if leaves_surface_area > m.leaves_max_surface_area + return nothing + end + + # if water levels are low, prioritise roots + if status.water_stock < m.water_leaf_threshold + return nothing + end + + # if not enough carbon, no organ creation + if status.carbon_stock < m.carbon_internode_creation_cost + return nothing + end + + if length(MultiScaleTreeGraph.children(status.node)) == 2 && + 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) + add_organ!(status_new_internode.node, sim_object, "+", "Leaf", 2, index=1) + + status_new_internode.TT_cu_emergence = m.TT_emergence - status.TT_cu + status.carbon_organ_creation_consumed = m.carbon_internode_creation_cost + end + + return nothing +end + +############################ +# Naive water absorption model +# Absorbs precipitation water depending on quantity of roots +############################ +PlantSimEngine.@process "water_absorption" verbose = false + +struct ToyWaterAbsorptionModel <: AbstractWater_AbsorptionModel +end + +PlantSimEngine.inputs_(::ToyWaterAbsorptionModel) = (root_water_assimilation=1.0,) +PlantSimEngine.outputs_(::ToyWaterAbsorptionModel) = (water_absorbed=0.0,) + +function PlantSimEngine.run!(m::ToyWaterAbsorptionModel, models, status, meteo, constants=nothing, extra=nothing) + #root_end = get_root_end_node(status.node) + #root_len = root_end[:Root_len] + status.water_absorbed = meteo.Precipitations * status.root_water_assimilation #* root_len +end + +PlantSimEngine.TimeStepDependencyTrait(::Type{<:ToyWaterAbsorptionModel}) = PlantSimEngine.IsTimeStepIndependent() +PlantSimEngine.ObjectDependencyTrait(::Type{<:ToyWaterAbsorptionModel}) = PlantSimEngine.IsObjectIndependent() + + +########################## +### Root growth : when water stocks are low, expand root +########################## + +PlantSimEngine.@process "root_growth" verbose = false + +struct ToyRootGrowthModel <: AbstractRoot_GrowthModel + water_threshold::Float64 + carbon_root_creation_cost::Float64 + root_max_len::Int +end + +PlantSimEngine.inputs_(::ToyRootGrowthModel) = (water_stock=0.0,carbon_stock=0.0,) +PlantSimEngine.outputs_(::ToyRootGrowthModel) = (carbon_root_creation_consumed=0.0,) + +function PlantSimEngine.run!(m::ToyRootGrowthModel, models, status, meteo, constants=nothing, extra=nothing) + if status.water_stock < m.water_threshold && status.carbon_stock > m.carbon_root_creation_cost + + root_end = get_root_end_node(status.node) + + if length(root_end) != 1 + throw(AssertionError("Couldn't find MTG leaf node with symbol \"Root\"")) + end + root_len = get_roots_count(root_end[1]) + if root_len < m.root_max_len + st = add_organ!(root_end[1], extra, "<", "Root", 2, index=1) + status.carbon_root_creation_consumed = m.carbon_root_creation_cost + end + else + status.carbon_root_creation_consumed = 0.0 + end +end + +########################## +### Model accumulating carbon and water resources +########################## + +PlantSimEngine.@process "resource_stock_computation" verbose = false + +struct ToyStockComputationModel <: AbstractResource_Stock_ComputationModel +end +#status.water_stock += meteo.precipitations * root_water_assimilation_ratio + +PlantSimEngine.inputs_(::ToyStockComputationModel) = +(water_absorbed=0.0,carbon_captured=0.0,carbon_organ_creation_consumed=0.0,carbon_root_creation_consumed=0.0) + +PlantSimEngine.outputs_(::ToyStockComputationModel) = (water_stock=-Inf,carbon_stock=-Inf) + +function PlantSimEngine.run!(m::ToyStockComputationModel, models, status, meteo, constants=nothing, extra=nothing) + status.water_stock += sum(status.water_absorbed) #- status.water_transpiration + status.carbon_stock += sum(status.carbon_captured) - sum(status.carbon_organ_creation_consumed) - sum(status.carbon_root_creation_consumed) + + if status.water_stock < 0.0 + status.water_stock = 0.0 + end +end + +PlantSimEngine.TimeStepDependencyTrait(::Type{<:ToyStockComputationModel}) = PlantSimEngine.IsTimeStepIndependent() +PlantSimEngine.ObjectDependencyTrait(::Type{<:ToyStockComputationModel}) = PlantSimEngine.IsObjectIndependent() + +######################## +## Leaf model capturing some arbitrary carbon quantity +######################## + +PlantSimEngine.@process "leaf_carbon_capture" verbose = false + +struct ToyLeafCarbonCaptureModel<: AbstractLeaf_Carbon_CaptureModel end + +function PlantSimEngine.inputs_(::ToyLeafCarbonCaptureModel) + NamedTuple()#(TT_cu=-Inf) +end + +function PlantSimEngine.outputs_(::ToyLeafCarbonCaptureModel) + (carbon_captured=0.0,) +end + +function PlantSimEngine.run!(::ToyLeafCarbonCaptureModel, models, status, meteo, constants, extra) + # very crude approximation with LAI of 1 and constant PPFD + status.carbon_captured = 200.0 *(1.0 - exp(-0.2)) +end + +PlantSimEngine.ObjectDependencyTrait(::Type{<:ToyLeafCarbonCaptureModel}) = PlantSimEngine.IsObjectIndependent() +PlantSimEngine.TimeStepDependencyTrait(::Type{<:ToyLeafCarbonCaptureModel}) = PlantSimEngine.IsTimeStepIndependent() + +mapping = Dict( +"Scene" => ToyDegreeDaysCumulModel(), +"Plant" => ( + MultiScaleModel( + model=ToyStockComputationModel(), + mapping=[ + :carbon_captured=>["Leaf"], + :water_absorbed=>["Root"], + :carbon_root_creation_consumed=>["Root"], + :carbon_organ_creation_consumed=>["Internode"] + + ], + ), + Status(water_stock = 0.0, carbon_stock = 0.0) + ), +"Internode" => ( + MultiScaleModel( + model=ToyInternodeEmergence(),#TT_emergence=20.0), + mapping=[:TT_cu => "Scene", + PreviousTimeStep(:water_stock)=>"Plant", + PreviousTimeStep(:carbon_stock)=>"Plant"], + ), + Status(carbon_organ_creation_consumed=0.0), + ), +"Root" => ( MultiScaleModel( + model=ToyRootGrowthModel(10.0, 50.0, 10), + mapping=[PreviousTimeStep(:carbon_stock)=>"Plant", + PreviousTimeStep(:water_stock)=>"Plant"], + ), + ToyWaterAbsorptionModel(), + Status(carbon_root_creation_consumed=0.0, root_water_assimilation=1.0), + ), +"Leaf" => ( ToyLeafCarbonCaptureModel(),), +) + + mtg = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Scene", 1, 0)) +#MultiScaleTreeGraph.Node(mtg, MultiScaleTreeGraph.NodeMTG("/", "Soil", 1, 1)) + plant = MultiScaleTreeGraph.Node(mtg, MultiScaleTreeGraph.NodeMTG("+", "Plant", 1, 1)) + + internode1 = MultiScaleTreeGraph.Node(plant, MultiScaleTreeGraph.NodeMTG("/", "Internode", 1, 2)) + MultiScaleTreeGraph.Node(internode1, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) + MultiScaleTreeGraph.Node(internode1, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) + + internode2 = MultiScaleTreeGraph.Node(internode1, MultiScaleTreeGraph.NodeMTG("<", "Internode", 1, 2)) + MultiScaleTreeGraph.Node(internode2, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) + MultiScaleTreeGraph.Node(internode2, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) + + plant_root_start = MultiScaleTreeGraph.Node( + #MultiScaleTreeGraph.new_id(MultiScaleTreeGraph.get_root(plant)), + plant, + MultiScaleTreeGraph.NodeMTG("+", "Root", 1, 3), + #Dict{String, Any}("Root_len"=> 1) + ) + #plant_root_start[:Root_len]=1 + + meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) + + outputs = run!(mtg, mapping, meteo_day) + mtg + + + length(MultiScaleTreeGraph.traverse(mtg,x->x, symbol="Leaf")) \ No newline at end of file From 603bfce8bfce3efd86ad0047fd00eb3d118ec17a Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 17 Feb 2025 17:01:45 +0100 Subject: [PATCH 059/147] Multiscale : expected default outputs behaviour (save everything if nothing provided) was being overriden by existing dispatch --- src/mtg/save_results.jl | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/mtg/save_results.jl b/src/mtg/save_results.jl index a912cabdf..29f5e50ae 100644 --- a/src/mtg/save_results.jl +++ b/src/mtg/save_results.jl @@ -115,19 +115,19 @@ function pre_allocate_outputs(statuses, statuses_template, reverse_multiscale_ma # default behaviour : track everything if isnothing(outs) for organ in keys(statuses) - outs_[organ] = keys(statuses_template[organ]) + outs_[organ] = [keys(statuses_template[organ])...] end - # No outputs requested by user : just return the timestep and node + # No outputs requested by user : just return the timestep and node elseif length(outs) == 0 for i in keys(statuses) outs_[i] = [] end + else + for i in keys(outs) # i = "Plant" + @assert isa(outs[i], Tuple{Vararg{Symbol}}) """Outputs for scale $i should be a tuple of symbols, *e.g.* `"$i" => (:a, :b)`, found `"$i" => $(outs[i])` instead.""" + outs_[i] = [outs[i]...] + end end - for i in keys(outs) # i = "Plant" - @assert isa(outs[i], Tuple{Vararg{Symbol}}) """Outputs for scale $i should be a tuple of symbols, *e.g.* `"$i" => (:a, :b)`, found `"$i" => $(outs[i])` instead.""" - outs_[i] = [outs[i]...] - end - statuses_ = copy(statuses_template) # Checking that organs in outputs exist in the mtg (in the statuses): if !all(i in keys(statuses) for i in keys(outs_)) @@ -202,7 +202,6 @@ function pre_allocate_outputs(statuses, statuses_template, reverse_multiscale_ma # without the reference types, e.g. RefVector{Float64} becomes Vector{Float64}. end -pre_allocate_outputs(statuses, status_templates, reverse_multiscale_mapping, vars_need_init, ::Nothing, nsteps; type_promotion=nothing, check=true) = Dict{String,Tuple{Symbol,Vararg{Symbol}}}() """ save_results!(object::GraphSimulation, i) From 283c675db4dfc7166ea9c51ea2c341e46ca8eeaf Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Fri, 21 Feb 2025 13:46:57 +0100 Subject: [PATCH 060/147] Documentation, lots of changes. Added some multiscale plant examples that require more work --- docs/make.jl | 74 ++++- docs/src/FAQ/translate_a_model.md | 2 +- docs/src/credits.md | 0 docs/src/extending/implement_a_process.md | 152 ---------- docs/src/index.md | 14 +- .../{new_doc => introduction}/why_julia.md | 2 +- docs/src/introduction/why_plantsimengine.md | 24 ++ docs/src/model_execution.md | 42 +-- .../multiscale.md | 127 +-------- .../multiscale_coupling.md} | 64 +---- docs/src/multiscale/multiscale_cyclic.md | 115 ++++++++ docs/src/multiscale/multiscale_example_1.md | 262 ++++++++++++++++++ docs/src/multiscale/multiscale_example_2.md | 223 +++++++++++++++ docs/src/planned_features.md | 35 +++ docs/src/{ => prerequisites}/design.md | 18 +- docs/src/prerequisites/julia_basics.md | 71 +++++ .../key_concepts.md | 36 ++- docs/src/step_by_step/advanced_coupling.md | 64 +++++ .../implement_a_model.md | 57 +++- docs/src/step_by_step/implement_a_process.md | 50 ++++ .../src/{ => step_by_step}/model_switching.md | 46 +-- docs/src/step_by_step/parallelization.md | 38 +++ .../step_by_step/quick_and_dirty_examples.md | 85 ++++++ .../src/step_by_step/simple_model_coupling.md | 105 +++++++ .../downstream_tests.md | 2 +- .../implicit_contracts.md | 4 +- ...lantsimengine_and_julia_troubleshooting.md | 29 +- .../tips_and_workarounds.md | 8 +- docs/src/{ => working_with_data}/fitting.md | 0 .../inputs.md | 0 .../{ => working_with_data}/reducing_dof.md | 10 +- .../working_with_data/visualising_outputs.md | 80 ++++++ .../ToyPlantSimulation1.jl | 148 ++++++++++ .../ToyPlantSimulation2.jl} | 23 +- 34 files changed, 1529 insertions(+), 481 deletions(-) create mode 100644 docs/src/credits.md delete mode 100644 docs/src/extending/implement_a_process.md rename docs/src/{new_doc => introduction}/why_julia.md (99%) create mode 100644 docs/src/introduction/why_plantsimengine.md rename docs/src/{model_coupling => multiscale}/multiscale.md (61%) rename docs/src/{model_coupling/model_coupling_modeler.md => multiscale/multiscale_coupling.md} (59%) create mode 100644 docs/src/multiscale/multiscale_cyclic.md create mode 100644 docs/src/multiscale/multiscale_example_1.md create mode 100644 docs/src/multiscale/multiscale_example_2.md create mode 100644 docs/src/planned_features.md rename docs/src/{ => prerequisites}/design.md (94%) create mode 100644 docs/src/prerequisites/julia_basics.md rename docs/src/{new_doc => prerequisites}/key_concepts.md (69%) create mode 100644 docs/src/step_by_step/advanced_coupling.md rename docs/src/{extending => step_by_step}/implement_a_model.md (90%) create mode 100644 docs/src/step_by_step/implement_a_process.md rename docs/src/{ => step_by_step}/model_switching.md (54%) create mode 100644 docs/src/step_by_step/parallelization.md create mode 100644 docs/src/step_by_step/quick_and_dirty_examples.md create mode 100644 docs/src/step_by_step/simple_model_coupling.md rename docs/src/{new_doc => troubleshooting_and_testing}/downstream_tests.md (94%) rename docs/src/{new_doc => troubleshooting_and_testing}/implicit_contracts.md (98%) rename docs/src/{new_doc => troubleshooting_and_testing}/plantsimengine_and_julia_troubleshooting.md (93%) rename docs/src/{model_coupling => troubleshooting_and_testing}/tips_and_workarounds.md (93%) rename docs/src/{ => working_with_data}/fitting.md (100%) rename docs/src/{extending => working_with_data}/inputs.md (100%) rename docs/src/{ => working_with_data}/reducing_dof.md (98%) create mode 100644 docs/src/working_with_data/visualising_outputs.md create mode 100644 examples/ToyMultiScalePlantTutorial/ToyPlantSimulation1.jl rename examples/{ToyPlantSimulation.jl => ToyMultiScalePlantTutorial/ToyPlantSimulation2.jl} (89%) diff --git a/docs/make.jl b/docs/make.jl index 9c82169c5..dd96c4b2c 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -18,28 +18,58 @@ makedocs(; assets=String[], size_threshold=300000 ), + pages=[ "Home" => "index.md", - "Design" => "design.md", - "Model Switching" => "model_switching.md", - "Reducing DoF" => "reducing_dof.md", + "Introduction" => [ + #"Organization of the documentation" => "./introduction/TODO.md", + "Why PlantSimEngine ?" => "./introduction/why_plantsimengine.md", + "Why Julia ?" => "./introduction/why_julia.md", + #"Overview" => "design.md" TODO, "Feature list ? Companion packages ? TODO" + ], + "Prerequisites" => [ + "Key Concepts" => "./prerequisites/key_concepts.md", + #"Setup" => "TODO.md", + "Julia language basics" => "./prerequisites/julia_basics.md", + "Design" =>"./prerequisites/design.md", + ], + "Step by step" => [ + #"First Simulation" => "./step_by_step/TODO.md", + "Coupling" => "./step_by_step/simple_model_coupling.md", + "Model Switching" => "./step_by_step/model_switching.md", + "Processes" => "./step_by_step/implement_a_process.md", + "Implementing a model" => "./step_by_step/implement_a_model.md", + "Parallelization" => "./step_by_step/parallelization.md", + "Advanced coupling" => "./step_by_step/advanced_coupling.md" + ], "Execution" => "model_execution.md", - "Fitting" => "fitting.md", - "Extending" => [ - "Processes" => "./extending/implement_a_process.md", - "Models" => "./extending/implement_a_model.md", - "Input types" => "./extending/inputs.md", + "Working with data" => [ + # Quick and dirty examples + "Reducing DoF" => "./working_with_data/reducing_dof.md", + "Fitting" => "./working_with_data/fitting.md", + "Input types" => "./working_with_data/inputs.md", + "Visualizing outputs" => "./working_with_data/visualising_outputs.md" ], - "Coupling" => [ - "Users" => [ - "Simple case" => "./model_coupling/model_coupling_user.md", - "Multi-scale modelling" => "./model_coupling/multiscale.md", + "Multiscale" => [ + "Detailed example" => "./multiscale/multiscale.md", + "Handling cyclic dependencies" => "./multiscale/multiscale_cyclic.md", + "Multiscale coupling considerations" => "./multiscale/multiscale_coupling.md", + "Building a simple plant" => [ + "A rudimentary plant simulation" => "./multiscale/multiscale_example_1.md", + "Expanding the plant simulation" => "./multiscale/multiscale_example_2.md", ], - "Modelers" => "./model_coupling/model_coupling_modeler.md", - "Tips and Workarounds" => "./model_coupling/tips_and_workarounds.md", ], - "FAQ" => ["./FAQ/translate_a_model.md"], + + "Troubleshooting and testing" => [ + "Troubleshooting" => "./troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md", + "Automated testing" => "./troubleshooting_and_testing/downstream_tests.md", + "Tips and Workarounds" => "./troubleshooting_and_testing/tips_and_workarounds.md", + ], + "API" => "API.md", + "Credits" => "credits.md", + "Planned features" => "planned_features.md", + #"developer section ?" ] ) @@ -47,3 +77,17 @@ deploydocs(; repo="github.com/VirtualPlantLab/PlantSimEngine.jl.git", devbranch="main" ) + + +using PlantSimEngine, PlantMeteo +using Pkg +Pkg.develop("PlantSimEngine") +# Import the examples defined in the `Examples` sub-module +using PlantSimEngine.Examples + +meteo = Atmosphere(T = 20.0, Wind = 1.0, Rh = 0.65, Ri_PAR_f = 500.0) + +leaf = ModelList(Beer(0.5), status = (LAI = 2.0,)) + +outputs_example = @enter run!(leaf, meteo) +outputs_example[:aPPFD] \ No newline at end of file diff --git a/docs/src/FAQ/translate_a_model.md b/docs/src/FAQ/translate_a_model.md index ebea01d07..73d834cc8 100644 --- a/docs/src/FAQ/translate_a_model.md +++ b/docs/src/FAQ/translate_a_model.md @@ -64,7 +64,7 @@ The model can be implemented using `PlantSimEngine` as follows: #### Define a process -If the process of LAI dynamic is not implement yet, we can define it like so: +If the process of LAI dynamic is not implemented yet, we can define it like so: ```julia @process LAI_Dynamic diff --git a/docs/src/credits.md b/docs/src/credits.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/src/extending/implement_a_process.md b/docs/src/extending/implement_a_process.md deleted file mode 100644 index 5cf1ac8a0..000000000 --- a/docs/src/extending/implement_a_process.md +++ /dev/null @@ -1,152 +0,0 @@ -# Implement a new process - -```@setup usepkg -using PlantSimEngine -using PlantMeteo -PlantSimEngine.@process growth -``` - -## Introduction - -`PlantSimEngine.jl` was designed to make the implementation of new processes and models easy and fast. Let's learn about how to implement a new process with a simple example: implementing a growth model. - -## Implement a process - -To implement a new process, we need to define an abstract structure that will help us associate the models to this process. We also need to generate some boilerplate code, such as a method for the `process` function. Fortunately, PlantSimEngine provides a macro to generate all that at once: [`@process`](@ref). This macro takes only one argument: the name of the process. - -For example, the photosynthesis process in [PlantBiophysics.jl](https://github.com/VEZY/PlantBiophysics.jl) is declared using just this tiny line of code: - -```julia -@process "photosynthesis" -``` - -If we want to simulate the growth of a plant, we could add a new process called `growth`: - -```julia -@process "growth" -``` - -And that's it! Note that the function guides you in the steps you can make after creating a process. Let's break it up here. - -!!! tip - If you know what you're doing, you can directly define a process by hand just by defining an abstract type that is a subtype of `AbstractModel`: - ```julia - abstract type AbstractGrowthModel <: PlantSimEngine.AbstractModel end - ``` - And by adding a method for the `process_` function that returns the name of the process: - ```julia - PlantSimEngine.process_(::Type{AbstractGrowthModel}) = :growth - ``` - But this way, you don't get the nice tutorial adapted to your process 🙃. - -So what you just did is to create a new process called `growth`. By doing so, you created a new abstract structure called `AbstractGrowthModel`, which is used as a supertype of the models. This abstract type is always named using the process name in title case (using `titlecase()`), prefixed with `Abstract` and suffixed with `Model`. - -!!! note - If you don't understand what a supertype is, no worries, you'll understand by seeing the examples below - -## Implement a new model for the process - -To better understand how models are implemented, you can read the detailed instructions from the [next section](@ref model_implementation_page). But for the sake of completeness, we'll implement a growth model here. - -This growth model needs the absorbed photosynthetically active radiation (aPPFD) as an input, and outputs the assimilation, the maintenance respiration, the growth respiration, the biomass increment and the biomass. The assimilation is computed as the product of the aPPFD and the light use efficiency (LUE). The maintenance respiration is a fraction of the assimilation, and the growth respiration is a fraction of the net primary productivity (NPP), which is the assimilation minus the maintenance respiration. The biomass increment is the NPP minus the growth respiration, and the biomass is the sum of the biomass increment and the previous biomass. Note that the previous biomass is always available in the `status` as long as you don't modify it. - -The model is available in the example script [ToyAssimGrowthModel.jl](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/ToyAssimGrowthModel.jl), and is reproduced below: - -```@example usepkg -# Make the struct to hold the parameters, with its documentation: -""" - ToyAssimGrowthModel(Rm_factor, Rg_cost) - ToyAssimGrowthModel(; LUE=0.2, Rm_factor = 0.5, Rg_cost = 1.2) - -Computes the biomass growth of a plant. - -# Arguments - -- `LUE=0.2`: the light use efficiency, in gC mol[PAR]⁻¹ -- `Rm_factor=0.5`: the fraction of assimilation that goes into maintenance respiration -- `Rg_cost=1.2`: the cost of growth maintenance, in gram of carbon biomass per gram of assimilate - -# Inputs - -- `aPPFD`: the absorbed photosynthetic photon flux density, in mol[PAR] m⁻² time-step⁻¹ - -# Outputs - -- `carbon_assimilation`: the assimilation, in gC m⁻² time-step⁻¹ -- `Rm`: the maintenance respiration, in gC m⁻² time-step⁻¹ -- `Rg`: the growth respiration, in gC m⁻² time-step⁻¹ -- `biomass_increment`: the daily biomass increment, in gC m⁻² time-step⁻¹ -- `biomass`: the plant biomass, in gC m⁻² time-step⁻¹ -""" -struct ToyAssimGrowthModel{T} <: AbstractGrowthModel - LUE::T - Rm_factor::T - Rg_cost::T -end - -# Note that ToyAssimGrowthModel is a subtype of AbstractGrowthModel, this is important - -# Instantiate the `struct` with keyword arguments and default values: -function ToyAssimGrowthModel(; LUE=0.2, Rm_factor=0.5, Rg_cost=1.2) - ToyAssimGrowthModel(promote(LUE, Rm_factor, Rg_cost)...) -end - -# Define inputs: -function PlantSimEngine.inputs_(::ToyAssimGrowthModel) - (aPPFD=-Inf,) -end - -# Define outputs: -function PlantSimEngine.outputs_(::ToyAssimGrowthModel) - (carbon_assimilation=-Inf, Rm=-Inf, Rg=-Inf, biomass_increment=-Inf, biomass=0.0) -end - -# Tells Julia what is the type of elements: -Base.eltype(x::ToyAssimGrowthModel{T}) where {T} = T - -# Implement the growth model: -function PlantSimEngine.run!(::ToyAssimGrowthModel, models, status, meteo, constants, extra) - - # The assimilation is simply the absorbed photosynthetic photon flux density (aPPFD) times the light use efficiency (LUE): - status.carbon_assimilation = status.aPPFD * models.growth.LUE - # The maintenance respiration is simply a factor of the assimilation: - status.Rm = status.carbon_assimilation * models.growth.Rm_factor - # Note that we use models.growth.Rm_factor to access the parameter of the model - - # Net primary productivity of the plant (NPP) is the assimilation minus the maintenance respiration: - NPP = status.carbon_assimilation - status.Rm - - # The NPP is used with a cost (growth respiration Rg): - status.Rg = 1 - (NPP / models.growth.Rg_cost) - - # The biomass increment is the NPP minus the growth respiration: - status.biomass_increment = NPP - status.Rg - - # The biomass is the biomass from the previous time-step plus the biomass increment: - status.biomass += status.biomass_increment -end - -# And optionally, we can tell PlantSimEngine that we can safely parallelize our model over space (objects): -PlantSimEngine.ObjectDependencyTrait(::Type{<:ToyAssimGrowthModel}) = PlantSimEngine.IsObjectIndependent() -``` - -Now we can make a simulation as usual: - -```@example usepkg -model = ModelList(ToyAssimGrowthModel(), status = (aPPFD = 20.0,)) -run!(model) -model[:biomass] # biomass in gC m⁻² -``` - -We can also run the simulation over more time-steps: - -```@example usepkg -model = ModelList( - ToyAssimGrowthModel(), - status=(aPPFD=[10.0, 30.0, 25.0],), -) - -run!(model) - -model.status[:biomass] # biomass in gC m⁻² -``` \ No newline at end of file diff --git a/docs/src/index.md b/docs/src/index.md index 855b89038..bba7e2e5c 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -17,7 +17,7 @@ model = ModelList( status=(TT_cu=1.0:2000.0,), # Pass the cumulated degree-days as input to the model ) -run!(model) +out = run!(model) # Define the list of models for coupling: model2 = ModelList( @@ -25,7 +25,7 @@ model2 = ModelList( 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!(model2, meteo_day) +out2 = run!(model2, meteo_day) ``` @@ -96,9 +96,7 @@ model = ModelList( status=(TT_cu=1.0:2000.0,), # Pass the cumulated degree-days as input to the model ) -run!(model) # run the model - -status(model) # extract the status, i.e. the output of the model +out = run!(model) # run the model and extract its outputs ``` > **Note** @@ -135,9 +133,7 @@ model2 = ModelList( ) # Run the simulation: -run!(model2, meteo_day) - -status(model2) +out2 = run!(model2, meteo_day) ``` The `ModelList` couples the models by automatically computing the dependency graph of the models. The resulting dependency graph is: @@ -263,7 +259,7 @@ out_vars = Dict( "Soil" => (:soil_water_content,), ) -out = run!(mtg, mapping, meteo, outputs=out_vars, executor=SequentialEx()); +out = run!(mtg, mapping, meteo, tracked_outputs=out_vars, executor=SequentialEx()); nothing # hide ``` diff --git a/docs/src/new_doc/why_julia.md b/docs/src/introduction/why_julia.md similarity index 99% rename from docs/src/new_doc/why_julia.md rename to docs/src/introduction/why_julia.md index 90288a14f..eb674723c 100644 --- a/docs/src/new_doc/why_julia.md +++ b/docs/src/introduction/why_julia.md @@ -10,7 +10,7 @@ It was therefore originally a means to an end, and not a product in itself. TODO -## PlantSimEngine's Constraints +## PlantSimEngine's constraints ### Performance diff --git a/docs/src/introduction/why_plantsimengine.md b/docs/src/introduction/why_plantsimengine.md new file mode 100644 index 000000000..d06756ee2 --- /dev/null +++ b/docs/src/introduction/why_plantsimengine.md @@ -0,0 +1,24 @@ + +PlantSimEngine arose out of a perceived need for a new modelling framework for ecophysiological and FSPM simulations. + +## Goals + +## Existing FSPM systems + +### Monoliths +Often massive codebases +Rigid +Implicit hypothesis +Parameters hard to change + +### Distributed systems + +two-language problem + +### other tools + +Architectural primary focus +Adding functional and environmental models is less straightforward +C++, Java, less accessible +User interface +Less tailored to autonomous 'researcher-developer', requires a 'developer-modeller' diff --git a/docs/src/model_execution.md b/docs/src/model_execution.md index 2c261e763..d208ede91 100644 --- a/docs/src/model_execution.md +++ b/docs/src/model_execution.md @@ -7,44 +7,4 @@ 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: 1. Hard dependencies are always run before soft dependencies. A hard dependency is a model that list dependencies in their own method for `dep`. See [this example](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/3d91bb053ddbd087d38dcffcedd33a9db35a0fcc/examples/dummy.jl#L39) that shows `Process2Model` defining a hard dependency on any model that simulate `process1`. Inner hard dependency graphs (*i.e.* consecutive hard-dependency children) are considered as a single soft dependency. - 2. Soft dependencies are then run sequentially. A model has a soft dependency on another model if one or more of its inputs is computed by another model. If a soft dependency has several parent nodes (*e.g.* two different models compute two inputs of the model), it is run only if all its parent nodes have been run already. In practice, when we visit a node that has one of its parent that did not run already, we stop the visit of this branch. The node will eventually be visited from the branch of the last parent that was run. - -## Parallel execution - -### FLoops - -`PlantSimEngine.jl` uses the [`Floops`](https://juliafolds.github.io/FLoops.jl/stable/) package to run the simulation in sequential, parallel (multi-threaded) or distributed (multi-process) computations over objects, time-steps and independent processes. - -That means that you can provide any compatible executor to the `executor` argument of [`run!`](@ref). By default, [`run!`](@ref) uses the [`ThreadedEx`](https://juliafolds.github.io/FLoops.jl/stable/reference/api/#executor) executor, which is a multi-threaded executor. You can also use the [`SequentialEx`](https://juliafolds.github.io/Transducers.jl/dev/reference/manual/#Transducers.SequentialEx)for sequential execution (non-parallel), or [`DistributedEx`](https://juliafolds.github.io/Transducers.jl/dev/reference/manual/#Transducers.DistributedEx) for distributed computations. - -### Parallel traits - -`PlantSimEngine.jl` uses [Holy traits](https://invenia.github.io/blog/2019/11/06/julialang-features-part-2/) to define if a model can be run in parallel. - -!!! note - A model is executable in parallel over time-steps if it does not uses or set values from other time-steps, and over objects if it does not uses or set values from other objects. - -You can define a model as executable in parallel by defining the traits for time-steps and objects. For example, the `ToyLAIModel` model from the [examples folder](https://github.com/VirtualPlantLab/PlantSimEngine.jl/tree/main/examples) can be run in parallel over time-steps and objects, so it defines the following traits: - -```julia -PlantSimEngine.TimeStepDependencyTrait(::Type{<:ToyLAIModel}) = PlantSimEngine.IsTimeStepIndependent() -PlantSimEngine.ObjectDependencyTrait(::Type{<:ToyLAIModel}) = PlantSimEngine.IsObjectIndependent() -``` - -By default all models are considered not executable in parallel, because it is the safest option to avoid bugs that are difficult to catch, so you only need to define these traits if it is executable in parallel for them. - -!!! tip - A model that is defined executable in parallel will not necessarily will. First, the user has to pass a parallel `executor` to [`run!`](@ref) (*e.g.* `ThreadedEx`). Second, if the model is coupled with another model that is not executable in parallel, `PlantSimEngine` will run all models in sequential. - -### Further executors - -You can also take a look at [FoldsThreads.jl](https://github.com/JuliaFolds/FoldsThreads.jl) for extra thread-based executors, [FoldsDagger.jl](https://github.com/JuliaFolds/FoldsDagger.jl) for -Transducers.jl-compatible parallel fold implemented using the Dagger.jl framework, and soon [FoldsCUDA.jl](https://github.com/JuliaFolds/FoldsCUDA.jl) for GPU computations -(see [this issue](https://github.com/VirtualPlantLab/PlantSimEngine.jl/issues/22)) and [FoldsKernelAbstractions.jl](https://github.com/JuliaFolds/FoldsKernelAbstractions.jl). You can also take a look at -[ParallelMagics.jl](https://github.com/JuliaFolds/ParallelMagics.jl) to check if automatic parallelization is possible. - -Finally, you can take a look into [Transducers.jl's documentation](https://github.com/JuliaFolds/Transducers.jl) for more information, for example if you don't know what is an executor, you can look into [this explanation](https://juliafolds.github.io/Transducers.jl/stable/explanation/glossary/#glossary-executor). - -## Tutorial - -You can learn how to run a simulation from [the home page](@ref PlantSimEngine), or from the [documentation of PlantBiophysics.jl](https://vezy.github.io/PlantBiophysics.jl/stable/simulation/first_simulation/). \ No newline at end of file + 2. Soft dependencies are then run sequentially. A model has a soft dependency on another model if one or more of its inputs is computed by another model. If a soft dependency has several parent nodes (*e.g.* two different models compute two inputs of the model), it is run only if all its parent nodes have been run already. In practice, when we visit a node that has one of its parent that did not run already, we stop the visit of this branch. The node will eventually be visited from the branch of the last parent that was run. \ No newline at end of file diff --git a/docs/src/model_coupling/multiscale.md b/docs/src/multiscale/multiscale.md similarity index 61% rename from docs/src/model_coupling/multiscale.md rename to docs/src/multiscale/multiscale.md index 750fbad7d..3c106c17a 100644 --- a/docs/src/model_coupling/multiscale.md +++ b/docs/src/multiscale/multiscale.md @@ -1,16 +1,12 @@ # Multi-scale modeling -## What is multi-scale modeling? +## Moving to multi-scale -Multi-scale modeling is the process of simulating a system at multiple levels of detail simultaneously. For example, some models can run at the organ scale while others run at the plot scale. Each model can access variables at its scale and other scales if needed, allowing for a more comprehensive system representation. It can also help identify emergent properties that are not apparent at a single level of detail. - -For example, a model of photosynthesis at the leaf scale can be combined with a model of carbon allocation at the plant scale to simulate the growth and development of the plant. Another example is a combination of models to simulate the energy balance of a forest. To simulate it, you need a model for each organ type of the plant, another for the soil, and finally, one at the plot scale, integrating all others. - -PlantSimEngine provides a framework for multi-scale modeling to seamlessly integrate models at different scales, keeping all nice functionalities provided at one scale. A nice feature is that models do not need to be aware of the scale at which they are running, nor about the scales at which their inputs are computed, or outputs will be given, which means the model can be reused at different scales or no scale. +PlantSimEngine provides a framework for multi-scale modeling to seamlessly integrate models at different scales, keeping all nice functionalities provided at one scale. A nice feature is that many models do not need to be aware of the scale at which they are running, nor about the scales at which their inputs are computed, or outputs will be given, which means those models can be reused at different scales or in single-scale simulations. PlantSimEngine automatically computes the dependency graph between mono and multi-scale models, considering every combination of models at any scale, to determine the order of model execution. This means that the user does not need to worry about the order of model execution and can focus on the model definition and the mapping between models and scales. -Using PlantSimEngine for multi-scale modeling is relatively easy and follows the same rules as mono-scale models. Let's dive into the details with a short tutorial. +Using PlantSimEngine for multi-scale modeling is relatively easy and mostly follows the same rules as mono-scale models. Let's dive into the details with a short tutorial. ## Simple mapping between models and scales @@ -248,123 +244,6 @@ nothing # hide This is a dictionary with the scale as the key and a vector of `Status` as values, one per node of that scale. So, in this example, the `"Leaf"` scale has two nodes, so the value is a vector of two `Status` objects, and the `"Soil"` scale has only one node, so the value is a vector of one `Status` object. - -## Avoiding cyclic dependencies - -When defining a mapping between models and scales, it is important to avoid cyclic dependencies. A cyclic dependency occurs when a model at a given scale depends on a model at another scale that depends on the first model. Cyclic dependencies are bad because they lead to an infinite loop in the simulation (the dependency graph keeps cycling indefinitely). - -PlantSimEngine will detect cyclic dependencies and raise an error if one is found. The error message indicates the models involved in the cycle, and the model that is causing the cycle will be highlighted in red. - -For example the following mapping will raise an error: - -!!! details - Example mapping - - ```julia - mapping_cyclic = Dict( - "Plant" => ( - MultiScaleModel( - model=ToyCAllocationModel(), - mapping=[ - :carbon_demand => ["Leaf", "Internode"], - :carbon_allocation => ["Leaf", "Internode"] - ], - ), - MultiScaleModel( - model=ToyPlantRmModel(), - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], - ), - Status(total_surface=0.001, aPPFD=1300.0, soil_water_content=0.6), - ), - "Internode" => ( - ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), - ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004), - Status(TT=10.0, carbon_biomass=1.0), - ), - "Leaf" => ( - ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), - ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), - ToyCBiomassModel(1.2), - Status(TT=10.0), - ) - ) - ``` - -Let's see what happens when we try to build the dependency graph for this mapping: - -```julia -julia> dep(mapping_cyclic) -ERROR: Cyclic dependency detected in the graph. Cycle: - Plant: ToyPlantRmModel - └ Leaf: ToyMaintenanceRespirationModel - └ Leaf: ToyCBiomassModel - └ Plant: ToyCAllocationModel - └ Plant: ToyPlantRmModel - - You can break the cycle using the `PreviousTimeStep` variable in the mapping. -``` - -How can we interpret the message? We have a list of five models involved in the cycle. The first model is the one causing the cycle, and the others are the ones that depend on it. In this case, the `ToyPlantRmModel` is the one causing the cycle, and the others are inter-dependent. We can read this as follows: - -1. `ToyPlantRmModel` depends on `ToyMaintenanceRespirationModel`, the plant-scale respiration sums up all organs respiration; -2. `ToyMaintenanceRespirationModel` depends on `ToyCBiomassModel`, the organs respiration depends on the organs biomass; -3. `ToyCBiomassModel` depends on `ToyCAllocationModel`, the organs biomass depends on the organs carbon allocation; -4. And finally `ToyCAllocationModel` depends on `ToyPlantRmModel` again, hence the cycle because the carbon allocation depends on the plant scale respiration. - -The models can not be ordered in a way that satisfies all dependencies, so the cycle can not be broken. To solve this issue, we need to re-think how models are mapped together, and break the cycle. - -There are several ways to break a cyclic dependency: - -- **Merge models**: If two models depend on each other because they need *e.g.* recursive computations, they can be merged into a third model that handles the computation and takes the two models as hard dependencies. Hard dependencies are models that are explicitly called by another model and do not participate on the building of the dependency graph. -- **Change models**: Of course models can be interchanged to avoid cyclic dependencies, but this is not really a solution, it is more a workaround. -- **PreviousTimeStep**: We can break the dependency graph by defining some variables as taken from the previous time step. A very well known example is the computation of the light interception by a plant that depends on the leaf area, which is usually the result of a model that also depends on the light interception. The cyclic dependency is usually broken by using the leaf area from the previous time step in the interception model, which is a good approximation for most cases. - -We can fix our previous mapping by computing the organs respiration using the carbon biomass from the previous time step instead. Let's see how to fix the cyclic dependency in our mapping (look at the leaf and internode scales): - -!!! details - ```@julia - mapping_nocyclic = Dict( - "Plant" => ( - MultiScaleModel( - model=ToyCAllocationModel(), - mapping=[ - :carbon_demand => ["Leaf", "Internode"], - :carbon_allocation => ["Leaf", "Internode"] - ], - ), - MultiScaleModel( - model=ToyPlantRmModel(), - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], - ), - Status(total_surface=0.001, aPPFD=1300.0, soil_water_content=0.6, carbon_assimilation=5.0), - ), - "Internode" => ( - ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), - MultiScaleModel( - model=ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004), - mapping=[PreviousTimeStep(:carbon_biomass),], #! this is where we break the cyclic dependency (first break) - ), - Status(TT=10.0, carbon_biomass=1.0), - ), - "Leaf" => ( - ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), - MultiScaleModel( - model=ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), - mapping=[PreviousTimeStep(:carbon_biomass),], #! this is where we break the cyclic dependency (second break) - ), - ToyCBiomassModel(1.2), - Status(TT=10.0), - ) - ); - nothing # hide - ``` - -The `ToyMaintenanceRespirationModel` models are now defined as `MultiScaleModel`, and the `carbon_biomass` variable is wrapped in a `PreviousTimeStep` structure. This structure tells PlantSimEngine to take the value of the variable from the previous time step, breaking the cyclic dependency. - -!!! note - `PreviousTimeStep` tells PlantSimEngine to take the value of the previous time step for the variable it wraps, or the value at initialization for the first time step. The value at initialization is the one provided by default in the models inputs, but is usually provided in the `Status` structure to override this default. - A `PreviousTimeStep` is used to wrap the **input** variable of a model, with or without a mapping to another scale *e.g.* `PreviousTimeStep(:carbon_biomass) => "Leaf"`. - ### Wrapping up In this section, we saw how to define a mapping between models and scales, run a simulation, and access the outputs. diff --git a/docs/src/model_coupling/model_coupling_modeler.md b/docs/src/multiscale/multiscale_coupling.md similarity index 59% rename from docs/src/model_coupling/model_coupling_modeler.md rename to docs/src/multiscale/multiscale_coupling.md index 1ed852df8..3b486be61 100644 --- a/docs/src/model_coupling/model_coupling_modeler.md +++ b/docs/src/multiscale/multiscale_coupling.md @@ -1,67 +1,5 @@ -# Model coupling for modelers - -```@setup usepkg -using PlantSimEngine, PlantMeteo -# Import the example models defined in the `Examples` sub-module: -using PlantSimEngine.Examples - -m = ModelList( - Process1Model(2.0), - Process2Model(), - Process3Model(), - Process4Model(), - Process5Model(), - Process6Model(), - Process7Model(), -) -``` - -This section uses notions from the previous section. If you are not familiar with the concepts of model coupling in PlantSimEngine, please read the previous section first: [Model coupling for users](@ref). - -## Hard coupling - -A model that calls explicitly another process is called a hard-coupled model. It is implemented by calling the process function directly. - -Let's go through the example processes and models from a script provided by the package here [examples/dummy.jl](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/dummy.jl) - -In this script, we declare seven processes and seven models, one for each process. The processes are simply called "process1", "process2"..., and the model implementations are called `Process1Model`, `Process2Model`... - -`Process2Model` calls `Process1Model` explicitly, which defines `Process1Model` as a hard-dependency of `Process2Model`. The is as follows: - -```julia -function PlantSimEngine.run!(::Process2Model, models, status, meteo, constants, extra) - # computing var3 using process1: - run!(models.process1, models, status, meteo, constants) - # computing var4 and var5: - status.var4 = status.var3 * 2.0 - status.var5 = status.var4 + 1.0 * meteo.T + 2.0 * meteo.Wind + 3.0 * meteo.Rh -end -``` - -We see that coupling a model (`Process2Model`) to another process (`process1`) is done by calling the `run!` function again. The `run!` function is called with the same arguments as the `run!` function of the model that calls it, except that we pass the process we want to simulate as the first argument. - -!!! note - We don't enforce any type of model to simulate `process1`. This is the reason why we can switch so easily between model implementations for any process, by just changing the model in the `ModelList`. - -A hard-dependency must always be declared to PlantSimEngine. This is done by adding a method to the `dep` function. For example, the hard-dependency to `process1` into `Process2Model` is declared as follows: - -```julia -PlantSimEngine.dep(::Process2Model) = (process1=AbstractProcess1Model,) -``` - -This way PlantSimEngine knows that `Process2Model` needs a model for the simulation of the `process1` process. Note that we don't add any constraint to the type of model we have to use (we use `AbstractProcess1Model`), because we want any model implementation to work with the coupling, as we only are interested in the value of a variable, not the way it is computed. - -Even if it is discouraged, you may have a valid reason to force the coupling with a particular model, or a kind of models though. For example, if we want to use only `Process1Model` for the simulation of `process1`, we would declare the dependency as follows: - -```julia -PlantSimEngine.dep(::Process2Model) = (process1=Process1Model,) -``` - -## Soft coupling - -A model that takes outputs of another model as inputs is called a soft-coupled model. There is nothing to do on the modeler side to declare a soft-dependency. The detection is done automatically by PlantSimEngine using the inputs and outputs of the models. -## Handling dependencies in a multiscale context +# Handling dependencies in a multiscale context If a model requires some input variable that is computed at another scale, providing the appropriate mapping will resolve name conflicts and enable proper use of that variable and there will be no extra steps for the user or the modeler. diff --git a/docs/src/multiscale/multiscale_cyclic.md b/docs/src/multiscale/multiscale_cyclic.md new file mode 100644 index 000000000..5bdb3bcc4 --- /dev/null +++ b/docs/src/multiscale/multiscale_cyclic.md @@ -0,0 +1,115 @@ +# Avoiding cyclic dependencies + +When defining a mapping between models and scales, it is important to avoid cyclic dependencies. A cyclic dependency occurs when a model at a given scale depends on a model at another scale that depends on the first model. Cyclic dependencies are bad because they lead to an infinite loop in the simulation (the dependency graph keeps cycling indefinitely). + +PlantSimEngine will detect cyclic dependencies and raise an error if one is found. The error message indicates the models involved in the cycle, and the model that is causing the cycle will be highlighted in red. + +For example the following mapping will raise an error: + +!!! details + Example mapping + + ```julia + mapping_cyclic = Dict( + "Plant" => ( + MultiScaleModel( + model=ToyCAllocationModel(), + mapping=[ + :carbon_demand => ["Leaf", "Internode"], + :carbon_allocation => ["Leaf", "Internode"] + ], + ), + MultiScaleModel( + model=ToyPlantRmModel(), + mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], + ), + Status(total_surface=0.001, aPPFD=1300.0, soil_water_content=0.6), + ), + "Internode" => ( + ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), + ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004), + Status(TT=10.0, carbon_biomass=1.0), + ), + "Leaf" => ( + ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), + ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), + ToyCBiomassModel(1.2), + Status(TT=10.0), + ) + ) + ``` + +Let's see what happens when we try to build the dependency graph for this mapping: + +```julia +julia> dep(mapping_cyclic) +ERROR: Cyclic dependency detected in the graph. Cycle: + Plant: ToyPlantRmModel + └ Leaf: ToyMaintenanceRespirationModel + └ Leaf: ToyCBiomassModel + └ Plant: ToyCAllocationModel + └ Plant: ToyPlantRmModel + + You can break the cycle using the `PreviousTimeStep` variable in the mapping. +``` + +How can we interpret the message? We have a list of five models involved in the cycle. The first model is the one causing the cycle, and the others are the ones that depend on it. In this case, the `ToyPlantRmModel` is the one causing the cycle, and the others are inter-dependent. We can read this as follows: + +1. `ToyPlantRmModel` depends on `ToyMaintenanceRespirationModel`, the plant-scale respiration sums up all organs respiration; +2. `ToyMaintenanceRespirationModel` depends on `ToyCBiomassModel`, the organs respiration depends on the organs biomass; +3. `ToyCBiomassModel` depends on `ToyCAllocationModel`, the organs biomass depends on the organs carbon allocation; +4. And finally `ToyCAllocationModel` depends on `ToyPlantRmModel` again, hence the cycle because the carbon allocation depends on the plant scale respiration. + +The models can not be ordered in a way that satisfies all dependencies, so the cycle can not be broken. To solve this issue, we need to re-think how models are mapped together, and break the cycle. + +There are several ways to break a cyclic dependency: + +- **Merge models**: If two models depend on each other because they need *e.g.* recursive computations, they can be merged into a third model that handles the computation and takes the two models as hard dependencies. Hard dependencies are models that are explicitly called by another model and do not participate on the building of the dependency graph. +- **Change models**: Of course models can be interchanged to avoid cyclic dependencies, but this is not really a solution, it is more a workaround. +- **PreviousTimeStep**: We can break the dependency graph by defining some variables as taken from the previous time step. A very well known example is the computation of the light interception by a plant that depends on the leaf area, which is usually the result of a model that also depends on the light interception. The cyclic dependency is usually broken by using the leaf area from the previous time step in the interception model, which is a good approximation for most cases. + +We can fix our previous mapping by computing the organs respiration using the carbon biomass from the previous time step instead. Let's see how to fix the cyclic dependency in our mapping (look at the leaf and internode scales): + +!!! details + ```@julia + mapping_nocyclic = Dict( + "Plant" => ( + MultiScaleModel( + model=ToyCAllocationModel(), + mapping=[ + :carbon_demand => ["Leaf", "Internode"], + :carbon_allocation => ["Leaf", "Internode"] + ], + ), + MultiScaleModel( + model=ToyPlantRmModel(), + mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], + ), + Status(total_surface=0.001, aPPFD=1300.0, soil_water_content=0.6, carbon_assimilation=5.0), + ), + "Internode" => ( + ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), + MultiScaleModel( + model=ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004), + mapping=[PreviousTimeStep(:carbon_biomass),], #! this is where we break the cyclic dependency (first break) + ), + Status(TT=10.0, carbon_biomass=1.0), + ), + "Leaf" => ( + ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), + MultiScaleModel( + model=ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), + mapping=[PreviousTimeStep(:carbon_biomass),], #! this is where we break the cyclic dependency (second break) + ), + ToyCBiomassModel(1.2), + Status(TT=10.0), + ) + ); + nothing # hide + ``` + +The `ToyMaintenanceRespirationModel` models are now defined as `MultiScaleModel`, and the `carbon_biomass` variable is wrapped in a `PreviousTimeStep` structure. This structure tells PlantSimEngine to take the value of the variable from the previous time step, breaking the cyclic dependency. + +!!! note + `PreviousTimeStep` tells PlantSimEngine to take the value of the previous time step for the variable it wraps, or the value at initialization for the first time step. The value at initialization is the one provided by default in the models inputs, but is usually provided in the `Status` structure to override this default. + A `PreviousTimeStep` is used to wrap the **input** variable of a model, with or without a mapping to another scale *e.g.* `PreviousTimeStep(:carbon_biomass) => "Leaf"`. \ No newline at end of file diff --git a/docs/src/multiscale/multiscale_example_1.md b/docs/src/multiscale/multiscale_example_1.md new file mode 100644 index 000000000..8f8207b4a --- /dev/null +++ b/docs/src/multiscale/multiscale_example_1.md @@ -0,0 +1,262 @@ +# Writing a multiscale simulation + +TODO change Toy To Example ? + +This section iteratively walks you through building a multi-scale simulation. + +The actual plant being simulated, as well as some of the ad hoc processes, mostly have no physical meaning and are very much ad hoc (which is why they aren't part of the TODO examples folder). Similarly, some of the parameter values are pulled out of thin air, and have no ties to research papers or data. + +The main purpose here is to showcase PlantSimEngine's multi-scale features and how to structure your models, not accuracy, realism or performance. + +You can find the full script for this simulation in TODO + +## A basic growing plant + +At minimul, to simulate some kind of fake growth, we need : + +- A MultiScale Tree Graph representing the plant +- Some way of adding organs to the plant +- Some kind of temporality to this dynamic + +Let's have some concept of 'leaves' that capture the (carbon) resource necessary for organ growth, and let's have the organ emergence happen at the 'internode' level, to illustrate multiple organs with different behavior. + +We'll make the assumption the internodes make use of carbon from a common pool. We'll also make use of thermal time as a growth delay factor. + +One way of modeling this approach translates into several scales and models : + +- Scene scale, for thermal time. The `ToyDegreeDaysCumulModel()` from the examples folder provides thermal time from temperature data *TODO +- Plant scale, where we'll define the carbon pool +- Internode scale, which draws from the pool to create new organs +- Leaf scale, which captures carbon + +Let's also add a very artificial limiting factor : if the total leaf surface area is above a threshold no new organs are created. + +We can expect the simulation mapping to look like a more complex version of the following : + +```julia +mapping = Dict( +"Scene" => ToyDegreeDaysCumulModel(), +"Plant" => ToyStockComputationModel(), +"Internode" => ToyCustomInternodeEmergence(), +"Leaf" => ToyLeafCarbonCaptureModel(), +) +``` + +Some of the models will need to gather variables from scales other than their own, meaning they will need to be converted into MultiScaleModels. + +## Implementation + +### Carbon Capture + +Let's start with the simplest model. Leaves continuously capture some constant amount of carbon every timestep. No inputs are required. + +```julia +PlantSimEngine.@process "leaf_carbon_capture" verbose = false + +struct ToyLeafCarbonCaptureModel<: AbstractLeaf_Carbon_CaptureModel end + +function PlantSimEngine.inputs_(::ToyLeafCarbonCaptureModel) + NamedTuple() # No inputs +end + +function PlantSimEngine.outputs_(::ToyLeafCarbonCaptureModel) + (carbon_captured=0.0,) +end + +function PlantSimEngine.run!(::ToyLeafCarbonCaptureModel, models, status, meteo, constants, extra) + status.carbon_captured = 40 +end +``` + +### Resource storage + +The model storing resources for the whole plant needs a couple of inputs : the amount of carbon captured by the leaves, as well as the amount consumed by the creation of new organs. It outputs the current stock. + +TODO + +```julia +PlantSimEngine.@process "resource_stock_computation" verbose = false + +struct ToyStockComputationModel <: AbstractResource_Stock_ComputationModel +end + +PlantSimEngine.inputs_(::ToyStockComputationModel) = +(carbon_captured=0.0,carbon_organ_creation_consumed=0.0) + +PlantSimEngine.outputs_(::ToyStockComputationModel) = (carbon_stock=-Inf,) + +function PlantSimEngine.run!(m::ToyStockComputationModel, models, status, meteo, constants=nothing, extra=nothing) + status.carbon_stock += sum(status.carbon_captured) - sum(status.carbon_organ_creation_consumed) +end +``` + +### Organ creation + +This model is a modified version of the `ToyInternodeEmergence()` model found in the examples folder TODO. An internode produces two leaves and a new internode. + +Let's first define a helper function that iterates across a Multiscale Tree Graph and returns the number of leaves : + +```julia +function get_n_leaves(node::MultiScaleTreeGraph.Node) + root = MultiScaleTreeGraph.get_root(node) + nleaves = length(MultiScaleTreeGraph.traverse(root, x->1, symbol="Leaf")) + return nleaves +end +``` + +Now that we have that, let's define a few parameters to the model. It requires : +- a thermal time emergence threshold +- a carbon cost for organ creation + +We'll also add a couple of other parameters, which could go elsewhere : +- the surface area of a leaf (no variation, no growth stages) +- the max leaf surface area beyond which organ creation stops + +```julia +PlantSimEngine.@process "organ_emergence" verbose = false + +struct ToyCustomInternodeEmergence <: AbstractOrgan_EmergenceModel + TT_emergence::Float64 + carbon_internode_creation_cost::Float64 + leaf_surface_area::Float64 + leaves_max_surface_area::Float64 +end + +``` + +And give them some default values : + +```julia +ToyCustomInternodeEmergence(;TT_emergence=300.0, carbon_internode_creation_cost=200.0, leaf_surface_area=3.0, leaves_max_surface_area=100.0) = ToyCustomInternodeEmergence(TT_emergence, carbon_internode_creation_cost, leaf_surface_area, leaves_max_surface_area) +``` + +Our internode model requires thermal time, and the amount of available carbon, and outputs the amount of carbon consumed, as well as the last thermal time where emergence happened (this is useful when new organs can be produced multiple times, which won't be the case here). + +```julia +PlantSimEngine.inputs_(m::ToyCustomInternodeEmergence) = (TT_cu=0.0, carbon_stock=0.0) +PlantSimEngine.outputs_(m::ToyCustomInternodeEmergence) = (TT_cu_emergence=0.0, carbon_organ_creation_consumed=0.0) +``` +Finally, the `run!` function checks that conditions are met for new organ creation : +- thermal time threshold exceeded +- total leaf surface area not above limit +- carbon available +- no organs already created by that internode + +and then updates the MTG. + +```julia +function PlantSimEngine.run!(m::ToyCustomInternodeEmergence, models, status, meteo, constants=nothing, sim_object=nothing) + + leaves_surface_area = m.leaf_surface * get_n_leaves(status.node) + status.carbon_organ_creation_consumed = 0.0 + + if leaves_surface_area > m.leaves_max_surface_area + return nothing + end + + # if not enough carbon, no organ creation + if status.carbon_stock < m.carbon_internode_creation_cost + return nothing + end + + if length(MultiScaleTreeGraph.children(status.node)) == 2 && + 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) + add_organ!(status_new_internode.node, sim_object, "+", "Leaf", 2, index=1) + + status_new_internode.TT_cu_emergence = m.TT_emergence - status.TT_cu + status.carbon_organ_creation_consumed = m.carbon_internode_creation_cost + end + + return nothing +end +``` + +## Updated mapping + +We can now define the final mapping for this simulation. + +The carbon capture and thermal time models don't need to be changed from the earlier version. +The organ creation model at the "Internode" scale needs the carbon stock from the "Plant" scale, as well as thermal time from the "Scene" scale. +The resource storing model at the "Plant" scale needs the carbon captured by **every** leaf, and the carbon consumed by **every** internode that created new organs this timestep. This requires mapping vector variables : + +```julia + mapping=[ + :carbon_captured=>["Leaf"], + :carbon_organ_creation_consumed=>["Internode"] + ], +``` +as opposed to the single-valued carbon stock mapped variable : + +```julia + mapping=[:TT_cu => "Scene", + PreviousTimeStep(:carbon_stock)=>"Plant"], +``` + +And of course, some variables need to be initialized in the status + +```julia +mapping = Dict( +"Scene" => ToyDegreeDaysCumulModel(), +"Plant" => ( + MultiScaleModel( + model=ToyStockComputationModel(), + mapping=[ + :carbon_captured=>["Leaf"], + :carbon_organ_creation_consumed=>["Internode"] + ], + ), + Status(carbon_stock = 0.0) + ), +"Internode" => ( + MultiScaleModel( + model=ToyCustomInternodeEmergence(),#TT_emergence=20.0), + mapping=[:TT_cu => "Scene", + PreviousTimeStep(:carbon_stock)=>"Plant"], + ), + Status(carbon_organ_creation_consumed=0.0), + ), +"Leaf" => ToyLeafCarbonCaptureModel(), +) +``` +### Running a simulation + +We only need an MTG, and some weather data, and then we'll be set. Let's create a simple MTG : + +```julia + mtg = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Scene", 1, 0)) + plant = MultiScaleTreeGraph.Node(mtg, MultiScaleTreeGraph.NodeMTG("+", "Plant", 1, 1)) + + internode1 = MultiScaleTreeGraph.Node(plant, MultiScaleTreeGraph.NodeMTG("/", "Internode", 1, 2)) + MultiScaleTreeGraph.Node(internode1, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) + MultiScaleTreeGraph.Node(internode1, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) + + internode2 = MultiScaleTreeGraph.Node(internode1, MultiScaleTreeGraph.NodeMTG("<", "Internode", 1, 2)) + MultiScaleTreeGraph.Node(internode2, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) + MultiScaleTreeGraph.Node(internode2, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) +``` + +Import some weather data : + +```julia +meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) +``` + +And we're good to go ! + +```julia +outs = run!(mtg, mapping, meteo_day) +``` + +And that's it. If you query or display the MTG after simulation, you'll see it expanded and grew multiple internodes and leaves : + +```julia +mtg +get_n_leaves(mtg) +``` + +Feel free to tinker with the parameters and see when things break down, to get a feel for the simulation. + +Of course, this is a very crude and unrealistic simulation, with many dubious assumptions and parameters. But significantly more complex modelling is possible using the same approach : XPalm runs using a few dozen models spread out over nine scales. + diff --git a/docs/src/multiscale/multiscale_example_2.md b/docs/src/multiscale/multiscale_example_2.md new file mode 100644 index 000000000..7cb35430a --- /dev/null +++ b/docs/src/multiscale/multiscale_example_2.md @@ -0,0 +1,223 @@ +# Expanding on the multiscale simulation + +Let's build on the previous example and add some other organ growth, as well as some very mild coupling between the two. + +## Adding roots to our plant + +We'll add a root that extracts water and adds it to the stock. Initial water stocks are low, so root growth is prioritized, then the plant also grows leaves and a new internode like it did before. Roots only grow up to a certain point, and don't branch. + +This leads to adding a new scale, "Root" to the mapping, as well as two more models, one for water absorption, the other for root growth. Other models are updated here and there to account for water. The carbon capture model remains unchanged. + +## Root models + +### Water absorption + +Let's implement a very fake model of root water absorption. It'll capture the amount of precipitation in the weather data multiplied by some assimilation factor. + +```julia +PlantSimEngine.@process "water_absorption" verbose = false + +struct ToyWaterAbsorptionModel <: AbstractWater_AbsorptionModel +end + +PlantSimEngine.inputs_(::ToyWaterAbsorptionModel) = (root_water_assimilation=1.0,) +PlantSimEngine.outputs_(::ToyWaterAbsorptionModel) = (water_absorbed=0.0,) + +function PlantSimEngine.run!(m::ToyWaterAbsorptionModel, models, status, meteo, constants=nothing, extra=nothing) + status.water_absorbed = meteo.Precipitations * status.root_water_assimilation +end +``` + +### Root growth + +The root growth model is similar to the internode growth one : it checks for a water threshold and that there is enough carbon, and adds a new organ to the MTG if the maximum length hasn't been reached. + +It also makes use of a couple of helper functions to find the end root and compute root length : + +```julia +function get_root_end_node(node::MultiScaleTreeGraph.Node) + root = MultiScaleTreeGraph.get_root(node) + return MultiScaleTreeGraph.traverse(root, x->x, symbol="Root", filter_fun = MultiScaleTreeGraph.isleaf) +end + +function get_roots_count(node::MultiScaleTreeGraph.Node) + root = MultiScaleTreeGraph.get_root(node) + return length(MultiScaleTreeGraph.traverse(root, x->x, symbol="Root")) +end + +PlantSimEngine.@process "root_growth" verbose = false + +struct ToyRootGrowthModel <: AbstractRoot_GrowthModel + water_threshold::Float64 + carbon_root_creation_cost::Float64 + root_max_len::Int +end + +PlantSimEngine.inputs_(::ToyRootGrowthModel) = (water_stock=0.0,carbon_stock=0.0,) +PlantSimEngine.outputs_(::ToyRootGrowthModel) = (carbon_root_creation_consumed=0.0,) + +function PlantSimEngine.run!(m::ToyRootGrowthModel, models, status, meteo, constants=nothing, extra=nothing) + if status.water_stock < m.water_threshold && status.carbon_stock > m.carbon_root_creation_cost + + root_end = get_root_end_node(status.node) + + if length(root_end) != 1 + throw(AssertionError("Couldn't find MTG leaf node with symbol \"Root\"")) + end + root_len = get_roots_count(root_end[1]) + if root_len < m.root_max_len + st = add_organ!(root_end[1], extra, "<", "Root", 2, index=1) + status.carbon_root_creation_consumed = m.carbon_root_creation_cost + end + else + status.carbon_root_creation_consumed = 0.0 + end +end +``` + +## Updating other models to account for water + +### Resource storage + +Water absorbed must now be accumulated, and root carbon creation costs taken into account. + +```julia +PlantSimEngine.@process "resource_stock_computation" verbose = false + +struct ToyStockComputationModel <: AbstractResource_Stock_ComputationModel +end + +PlantSimEngine.inputs_(::ToyStockComputationModel) = +(water_absorbed=0.0,carbon_captured=0.0,carbon_organ_creation_consumed=0.0,carbon_root_creation_consumed=0.0) + +PlantSimEngine.outputs_(::ToyStockComputationModel) = (water_stock=-Inf,carbon_stock=-Inf) + +function PlantSimEngine.run!(m::ToyStockComputationModel, models, status, meteo, constants=nothing, extra=nothing) + status.water_stock += sum(status.water_absorbed) + status.carbon_stock += sum(status.carbon_captured) - sum(status.carbon_organ_creation_consumed) - sum(status.carbon_root_creation_consumed) +end +``` + +### Internode creation + +The minor chagne is that new organs are now created only if the water stock is above a given threshold. + +```julia +struct ToyCustomInternodeEmergence <: AbstractOrgan_EmergenceModel + TT_emergence::Float64 + carbon_internode_creation_cost::Float64 + leaf_surface_area::Float64 + leaves_max_surface_area::Float64 + water_leaf_threshold::Float64 +end + +ToyCustomInternodeEmergence(;TT_emergence=300.0, carbon_internode_creation_cost=200.0, leaf_surface_area=3.0,leaves_max_surface_area=100.0, +water_leaf_threshold=30.0) = ToyCustomInternodeEmergence(TT_emergence, carbon_internode_creation_cost, leaf_surface_area, leaves_max_surface_area, water_leaf_threshold) + +PlantSimEngine.inputs_(m::ToyCustomInternodeEmergence) = (TT_cu=0.0,water_stock=0.0, carbon_stock=0.0) +PlantSimEngine.outputs_(m::ToyCustomInternodeEmergence) = (TT_cu_emergence=0.0, carbon_organ_creation_consumed=0.0) + +function PlantSimEngine.run!(m::ToyCustomInternodeEmergence, models, status, meteo, constants=nothing, sim_object=nothing) + + leaves_surface_area = m.leaf_surface_area * get_n_leaves(status.node) + status.carbon_organ_creation_consumed = 0.0 + + if leaves_surface_area > m.leaves_max_surface_area + return nothing + end + + # if water levels are low, prioritise roots + if status.water_stock < m.water_leaf_threshold + return nothing + end + + # if not enough carbon, no organ creation + if status.carbon_stock < m.carbon_internode_creation_cost + return nothing + end + + if length(MultiScaleTreeGraph.children(status.node)) == 2 && + 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) + add_organ!(status_new_internode.node, sim_object, "+", "Leaf", 2, index=1) + + status_new_internode.TT_cu_emergence = m.TT_emergence - status.TT_cu + status.carbon_organ_creation_consumed = m.carbon_internode_creation_cost + end + + return nothing +end +``` + +## Updating the mapping + +The resource storage and internode emergence models now need a couple of extra water-related mapped variables. +The "Root" organ is added to the mapping with its own models. New parameters need to be initialized. + +```julia +mapping = Dict( +"Scene" => ToyDegreeDaysCumulModel(), +"Plant" => ( + MultiScaleModel( + model=ToyStockComputationModel(), + mapping=[ + :carbon_captured=>["Leaf"], + :water_absorbed=>["Root"], + :carbon_root_creation_consumed=>["Root"], + :carbon_organ_creation_consumed=>["Internode"] + + ], + ), + Status(water_stock = 0.0, carbon_stock = 0.0) + ), +"Internode" => ( + MultiScaleModel( + model=ToyCustomInternodeEmergence(),#TT_emergence=20.0), + mapping=[:TT_cu => "Scene", + PreviousTimeStep(:water_stock)=>"Plant", + PreviousTimeStep(:carbon_stock)=>"Plant"], + ), + Status(carbon_organ_creation_consumed=0.0), + ), +"Root" => ( MultiScaleModel( + model=ToyRootGrowthModel(10.0, 50.0, 10), + mapping=[PreviousTimeStep(:carbon_stock)=>"Plant", + PreviousTimeStep(:water_stock)=>"Plant"], + ), + ToyWaterAbsorptionModel(), + Status(carbon_root_creation_consumed=0.0, root_water_assimilation=1.0), + ), +"Leaf" => ( ToyLeafCarbonCaptureModel(),), +) +``` + +## Running the simulation + +Running this new simulation is almost the same as before. The weather data is unchanged, but a new "Root" node was added to the MTG. + +```julia +mtg = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Scene", 1, 0)) + plant = MultiScaleTreeGraph.Node(mtg, MultiScaleTreeGraph.NodeMTG("+", "Plant", 1, 1)) + + internode1 = MultiScaleTreeGraph.Node(plant, MultiScaleTreeGraph.NodeMTG("/", "Internode", 1, 2)) + MultiScaleTreeGraph.Node(internode1, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) + MultiScaleTreeGraph.Node(internode1, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) + + internode2 = MultiScaleTreeGraph.Node(internode1, MultiScaleTreeGraph.NodeMTG("<", "Internode", 1, 2)) + MultiScaleTreeGraph.Node(internode2, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) + MultiScaleTreeGraph.Node(internode2, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) + + plant_root_start = MultiScaleTreeGraph.Node( + plant, + MultiScaleTreeGraph.NodeMTG("+", "Root", 1, 3), + ) + +meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) + +outs = run!(mtg, mapping, meteo_day) +``` + +And that's it ! We now have a plant with two different growth directions. Roots are added at the beginning, until water is considered abundant enough. + +Of course, there are several design issues with this implementation. It is as utterly unrealistic as the previous one, and doesn't even consume water. \ No newline at end of file diff --git a/docs/src/planned_features.md b/docs/src/planned_features.md new file mode 100644 index 000000000..ab39b592f --- /dev/null +++ b/docs/src/planned_features.md @@ -0,0 +1,35 @@ +Roadmap + +more examples ? +reworked mapping API, and other API changes + +Handling varying timesteps +CI/downstream +multi-species + +better tracking of memory usage and type stability + +meteo required variables checking + +avoid printing the whole shebang by default + +Better dependency graph visualisation +Improving user errors +MTG couple of new features #106, other bugs +other bugs +cyclic management for modellists +dependency graph traversal + +run! unrolling +Improved parallelisation ? + +state machine checker +graph fuzzing + +iteratively build and validate mappings and modellists ? +documenting FP errors, more examples for fitting/type conversion/error propagation +Improve multiscale dependency API ? + +other packages ? + +TODO credits ? \ No newline at end of file diff --git a/docs/src/design.md b/docs/src/prerequisites/design.md similarity index 94% rename from docs/src/design.md rename to docs/src/prerequisites/design.md index d6037c03f..2b43dff28 100644 --- a/docs/src/design.md +++ b/docs/src/prerequisites/design.md @@ -82,13 +82,7 @@ fieldnames(Beer) Variables are either input or outputs (*i.e.* computed) of models. Variables and their values are stored in the [`ModelList`](@ref) structure, and are initialized automatically or manually. -Hence, [`ModelList`](@ref) objects stores two fields: - -```@example usepkg -fieldnames(ModelList) -``` - -The first field is a list of models associated to the processes they simulate. The second, `:status`, is used to hold all inputs and outputs of our models, called variables. For example the `Beer` model needs the leaf area index (`LAI`, m² m⁻²) to run. +For example, the `Beer` model needs the leaf area index (`LAI`, m² m⁻²) to run. We can see which variables are needed as inputs using [`inputs`](@ref): @@ -168,9 +162,9 @@ More details are available from the [package documentation](https://vezy.github. ### Simulation of processes -Making a simulation is rather simple, we simply use [`run!`](@ref) on the `ModelList`: +Making a simulation is rather simple, we simply call the [`run!`](@ref) method on the `ModelList`. If some meteorological data is required for models to be simulated over several timesteps, that can be passed in as a parameter as well. -The call to [`run!`](@ref) is the same whatever the models you choose for simulating the processes. This is some magic allowed by `PlantSimEngine.jl`! Here is an example: +Here is an example: ```julia run!(model_list, meteo) @@ -194,13 +188,13 @@ meteo = Atmosphere(T = 20.0, Wind = 1.0, Rh = 0.65, Ri_PAR_f = 500.0) leaf = ModelList(Beer(0.5), status = (LAI = 2.0,)) -run!(leaf, meteo) +outputs_example = run!(leaf, meteo) -leaf[:aPPFD] +outputs_example[:aPPFD] ``` ### Outputs - +TODO The `status` field of a [`ModelList`](@ref) is used to initialize the variables before simulation and then to keep track of their values during and after the simulation. We can extract the simulation outputs of a model list using the [`status`](@ref) function. The status is usually stored in a `TimeStepTable` structure from `PlantMeteo.jl`, which is a fast DataFrame-alike structure with each time step being a [`Status`](@ref). It can be also be any `Tables.jl` structure, such as a regular `DataFrame`. The weather is also usually stored in a `TimeStepTable` but with each time step being an `Atmosphere`. diff --git a/docs/src/prerequisites/julia_basics.md b/docs/src/prerequisites/julia_basics.md new file mode 100644 index 000000000..87973d07c --- /dev/null +++ b/docs/src/prerequisites/julia_basics.md @@ -0,0 +1,71 @@ +# Getting started with Julia + +PlantSimEngine (as well as its related packages) is written in Julia. The reasons why Julia was chosen are briefly discussed here TODO + +Julia is a language that is gaining traction, but it isn't the most widely used in research and data science. + +Many elements will be familiar to those with an R, Python or Matlab background, but there are some noteworthy differences, and if you are new to the language, there will be a few hurdles you might have to overcome to be comfortable using the language. + +This section is here to help you with that, and provides a short introduction to the parts of Julia that are most relevant regarding usage of PlantSimEngine. + +It is not meant as a full-fledged Julia tutorial. If you are completely new to programming, you may wish to check some other resources first, such as ones found [here](https://docs.julialang.org/en/v1/manual/getting-started/). + +If you wish to compare Julia to a specific language, [this page](https://docs.julialang.org/en/v1/manual/noteworthy-differences/#Noteworthy-differences-from-Python) will provide you with a quick overview of the differences. + +You can also find a few cheatsheets [here](https://palmstudio.github.io/Biophysics_database_palm/cheatsheets/) as well as a [short introductory notebook](https://palmstudio.github.io/Biophysics_database_palm/basic_syntax/) along with [install instructions](https://palmstudio.github.io/Biophysics_database_palm/installation/) + + +### Installing Julia + +### Installing PlantSimEngine and its dependencies + +### Julia environments + +### Running an environment + +Once your environment is set up, you can launch a command prompt and type 'julia'. This will launch Julia, and you should see +julia> +in the command prompt. + +You can always type '?' from there to enter help mode, and type the name of a function or language feature you wish to know more about. + +You can find out which directory you are in by typing pwd() in a Julia session. + +Handling environments and dependencies is done in Julia through a specific Package called Pkg, which comes with the base install. You can either call Pkg features the same way you would for another package, or enter Pkg mode by typing ']', which will change the display from +julia> to something like (@v1.11) pkg>, indicating your current environment (in this case, the default julia environment, which we don't recommend bloating). + +Once in Pkg mode, you can choose to create an environment by typing 'activate path/to/environemnt'. + +You can then add packages that have been added to Julia's online global registry by typing add packagename and you can remove them by typing remove packagename. Typing status or st will indicate what your current environment is comprised of. To update packages in need of updating (a '^' symbol will display next to their name), type update or up. + +If you are editing/developing a package or using one locally, typing develop path/to/package source/ (or dev path/to/package/source) will cause your environment to use that version instead of the registered one. + +Typing instantiate will download all the packages declared in the manifest file (if it exists) of an environment. + +For instance, PlantSimEngine has a test folder used in development. If you wanted to run tests, you would type +']' +'activate ../path/to/PlantSimEngine/test' +'instantiate' +and then you would be ready to go. + + + +### Variables, functions and arrays -> See the palmstudio basic syntax page, or the diff eq notebook ? + +### Really noteworthy differences : +- Array indexing starts at 1. + +### Typing + +### Custom types + +### Dictionaries + +PlantSimEngine makes use of dictionaries to declare and store data, indexed by scale/organ. +For example : + +### Functions + +### Function arguments and kwargs + +### NamedTuples \ No newline at end of file diff --git a/docs/src/new_doc/key_concepts.md b/docs/src/prerequisites/key_concepts.md similarity index 69% rename from docs/src/new_doc/key_concepts.md rename to docs/src/prerequisites/key_concepts.md index b8d342bd0..54daab0a8 100644 --- a/docs/src/new_doc/key_concepts.md +++ b/docs/src/prerequisites/key_concepts.md @@ -12,9 +12,11 @@ You'll find a brief description of some of the main concepts and terminology rel A process in this package defines a biological or physical phenomena. Think of any process happening in a system, such as light interception, photosynthesis, water, carbon and energy fluxes, growth, yield or even electricity produced by solar panels. +TODO link + ## Models -Models are then implemented for a particular process. +Models are then implemented for a particular process. TODO link There may be different models that can be used for the same process ; for instance, there are multiple hypotheses and ways of modeling photosynthesis, with different granularity and accuracy. A simple photosynthesis model might apply a simple formula and apply it to the total leaf surface, a more complex one might calculate interception and light extinction. @@ -36,15 +38,27 @@ TODO image de graphe illustrant un couplage ### Dependency graphs -Coupling models together in this fashion creates what is known as a Directed Acyclic Graph or DAG. The order in which models are run is determined by the ordering of these models in that graph. +Coupling models together in this fashion creates what is known as a Directed Acyclic Graph or DAG, a type of dependency graph. The order in which models are run is determined by the ordering of these models in that graph. TODO image PlantSimEngine creates this DAG under the hood by plugging the right variables in the right models. Users therefore only need to declare models, they do not need write the code to connect them as PlantSimEngine does that work for them. -### 'Hard' dependencies +### "Hard" and "Soft" dependencies + +Linking models by finding which output variables are used as input of another model handles many of the coupling situations that can occur (with more situations occurring with multi-scale models and variables), but what if two models are interdependent ? If they need to iterate on some computation and pass variables back and forth ? + +Model couplings that cause simulation to flow both ways break the 'acyclic' assumption of the dependency graph. + +PlantSimEngine handles this internally by not having those "heavily-coupled" models -called "hard dependencies" from now on- be part of the main dependency graph. Instead, they are made to be children nodes of the parent/ancestor model, which handles them internally, so they aren't tied to other nodes of the dependency graph. The resulting higher-level graph therefore only links models without any two-way interdependencies, and remains a directed graph, enabling a cohesive simulation order. The simpler couplings in that top-level graph are called "soft dependencies". + +This approach does have implications when developing interdependent models : hard dependencies need to be made explicit, and the ancestor needs to call the hard dependency model's `run!` function explicitely in its own `run!` function. Hard dependency models therefore must have only one parent model. + +You can find an example TODO + +This makes them slightly more complex to develop and validate, and less versatile than other models. Occasional refactoring may be necessary to handle a hard dependency creeping up when adding new models to a simulation. -### Process and Model implementation ? TODO +Note that hard dependencies can also have their own hard dependencies, and some complex couplings are therefore possible. A hard dependency model can have another hard dependency model as a parent. ### Weather data @@ -71,7 +85,11 @@ Plants have different organs with distinct physiological properties and processe PlantSimEngine documentation tends to use the terms "organ" and "scale" mostly interchangeably. "Scale" is a bit more general and accurate, since some models might not operate at a specific organ level, but (for example) at the scene level, so a "Scene" scale might be present in the MTG, and in the user-provided data. -### Mapping, multiscale simulations +### Multiscale modeling + +Multi-scale modeling is the process of simulating a system at multiple levels of detail simultaneously. For example, some models can run at the organ scale while others run at the plot scale. Each model can access variables at its scale and other scales if needed, allowing for a more comprehensive system representation. It can also help identify emergent properties that are not apparent at a single level of detail. + +For example, a model of photosynthesis at the leaf scale can be combined with a model of carbon allocation at the plant scale to simulate the growth and development of the plant. Another example is a combination of models to simulate the energy balance of a forest. To simulate it, you need a model for each organ type of the plant, another for the soil, and finally, one at the plot scale, integrating all others. When running multi-scale simulations which contain models operating at different organ levels for the plant, extra information needs to be provided by the user to run models. Since some models are reused at different organ levels, it is necessary to indicate which organ level a model operates at. @@ -89,4 +107,10 @@ TODO example ? TODO image ? TODO lien avec AMAP ? -### State machines \ No newline at end of file +TODO scale, symbol terminology ambiguity + +### State machines + + + +TODO differences mono/multiscale ? \ No newline at end of file diff --git a/docs/src/step_by_step/advanced_coupling.md b/docs/src/step_by_step/advanced_coupling.md new file mode 100644 index 000000000..1f81439a2 --- /dev/null +++ b/docs/src/step_by_step/advanced_coupling.md @@ -0,0 +1,64 @@ +# Coupling more complex models + +```@setup usepkg +using PlantSimEngine, PlantMeteo +# Import the example models defined in the `Examples` sub-module: +using PlantSimEngine.Examples + +m = ModelList( + Process1Model(2.0), + Process2Model(), + Process3Model(), + Process4Model(), + Process5Model(), + Process6Model(), + Process7Model(), +) +``` + +When two or more models have a two-way interdependency (rather than variables flowing out only one-way from one model into the next), we describe it as a [hard dependency](TODO). + +This kind of interdependency requires a little more work from the user/modeler for PlantSimEngine to be able to automatically create the dependency graph. + +## Declaring hard dependencies + +A model that explicitly and directly calls another process in its `run!` function is part of a hard dependency, or a hard-coupled model. + +Let's go through the example processes and models from a script provided by the package here [examples/dummy.jl](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/dummy.jl) + +In this script, we declare seven processes and seven models, one for each process. The processes are simply called "process1", "process2"..., and the model implementations are called `Process1Model`, `Process2Model`... + +When run, `Process2Model` calls another process's `run!` function which requires defining that process as a hard-dependency of `Process2Model` : + +```julia +function PlantSimEngine.run!(::Process2Model, models, status, meteo, constants, extra) + # computing var3 using process1: + run!(models.process1, models, status, meteo, constants) + # computing var4 and var5: + status.var4 = status.var3 * 2.0 + status.var5 = status.var4 + 1.0 * meteo.T + 2.0 * meteo.Wind + 3.0 * meteo.Rh +end +``` + +We see that coupling a model (`Process2Model`) to another process (`process1`) is done by calling the `run!` function again. The `run!` function is called with the same arguments as the `run!` function of the model that calls it, except that we pass the process we want to simulate as the first argument. + +!!! note + We don't enforce any type of model to simulate `process1`. This is the reason why we can switch so easily between model implementations for any process, by just changing the model in the `ModelList`. + +A hard-dependency must always be declared to PlantSimEngine. This is done by adding a method to the `dep` function when implementing the model. For example, the hard-dependency to `process1` into `Process2Model` is declared as follows: + +```julia +PlantSimEngine.dep(::Process2Model) = (process1=AbstractProcess1Model,) +``` + +This way PlantSimEngine knows that `Process2Model` needs a model for the simulation of the `process1` process. To avoid imposing a specific model to be coupled with `Process2Model`, the dependency only requires a model that is a subtype of the abstract parent type `AbstractProcess1Model`. This avoids constraining to the specific `Process1Model` implementation, meaning an alternate model computing the same variables for the same process is still interchangeable with `Process1Model`. + +While not encouraged, if you have a valid reason to force the coupling with a particular model, you can force the dependency to require that model specifically. For example, if we want to use only `Process1Model` for the simulation of `process1`, we would declare the dependency as follows: + +```julia +PlantSimEngine.dep(::Process2Model) = (process1=Process1Model,) +``` + +##  + +There are examples in PlantBioPhysics of such models TODO. \ No newline at end of file diff --git a/docs/src/extending/implement_a_model.md b/docs/src/step_by_step/implement_a_model.md similarity index 90% rename from docs/src/extending/implement_a_model.md rename to docs/src/step_by_step/implement_a_model.md index c9b13aa67..09aa3bba4 100644 --- a/docs/src/extending/implement_a_model.md +++ b/docs/src/step_by_step/implement_a_model.md @@ -8,7 +8,62 @@ struct Beer{T} <: AbstractLight_InterceptionModel end ``` -## Introduction +You'll probably want to move beyond simple usage at some point and implement your own models. + +## Quick version + +Declare a new process : + +```julia +@process "light_interception" verbose = false +``` + +Declare your model struct, and its parameters : + +```@example usepkg +struct Beer{T} <: AbstractLight_InterceptionModel + k::T +end +``` + +Declare the `inputs_` and `outputs_` methods for that model (note the '_', these methods are distinct from `inputs` and `outputs`) + +```@example usepkg +function PlantSimEngine.inputs_(::Beer) + (LAI=-Inf,) +end + +function PlantSimEngine.outputs_(::Beer) + (aPPFD=-Inf,) +end +``` + +Write the `run!` function that operates on a single timestep : + +```@example usepkg +function run!(::Beer, models, status, meteo, constants, extras) + status.PPFD = + meteo.Ri_PAR_f * + exp(-models.light_interception.k * status.LAI) * + constants.J_to_umol +end +``` + +Determine if parallelization is possible, and which traits to declare : + +```@example usepkg +PlantSimEngine.ObjectDependencyTrait(::Type{<:Beer}) = PlantSimEngine.IsObjectIndependent() +PlantSimEngine.TimeStepDependencyTrait(::Type{<:Beer}) = PlantSimEngine.IsTimeStepIndependent() +``` + +And that is all you need to get going, for this simple example with a single parameter and no interdependencies. + +The `@process` macro does a few things under the hood described [here](TODO) + +If you have more than one parameter, then type conversion utility functions might also be interesting to implement. See here TODO +If you need to deal with more complex couplings, the hard dependency section will detail + +## Detailed version `PlantSimEngine.jl` was designed to make new model implementation very simple. So let's learn about how to implement your own model with a simple example: implementing a new light interception model. diff --git a/docs/src/step_by_step/implement_a_process.md b/docs/src/step_by_step/implement_a_process.md new file mode 100644 index 000000000..9e055d69e --- /dev/null +++ b/docs/src/step_by_step/implement_a_process.md @@ -0,0 +1,50 @@ +# Implement a new process + +```@setup usepkg +using PlantSimEngine +using PlantMeteo +PlantSimEngine.@process growth +``` + +## Introduction + +`PlantSimEngine.jl` was designed to make the implementation of new processes and models easy and fast. Let's learn about how to implement a new process with a simple example: implementing a growth model. + +## Implement a process + +To implement a new process, we need to define an abstract structure that will help us associate the models to this process. We also need to generate some boilerplate code, such as a method for the `process` function. Fortunately, PlantSimEngine provides a macro to generate all that at once: [`@process`](@ref). This macro takes only one argument: the name of the process. + +For example, the photosynthesis process in [PlantBiophysics.jl](https://github.com/VEZY/PlantBiophysics.jl) is declared using just this tiny line of code: + +```julia +@process "photosynthesis" +``` + +If we want to simulate the growth of a plant, we could add a new process called `growth`: + +```julia +@process "growth" +``` + +And that's it! Note that the function guides you in the steps you can make after creating a process. Let's break it up here. + +## Implement a new model for the process + +Once process implementation is done, you can write a corresponding model implementation. The corresponding tutorial page can be found [here](TODO) + +A full model implementation for this process is available in the example script [ToyAssimGrowthModel.jl](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/ToyAssimGrowthModel.jl). + +## Under the hood + +The `@process` macro is just a shorthand reducing boilerplate. + +You can in its stead directly define a process by hand by defining an abstract type that is a subtype of `AbstractModel`: +```julia +abstract type AbstractGrowthModel <: PlantSimEngine.AbstractModel end +``` +And by adding a method for the `process_` function that returns the name of the process: +```julia +PlantSimEngine.process_(::Type{AbstractGrowthModel}) = :growth +``` + +So in the earlier example, a new process was created called `growth`. This defined a new abstract structure called `AbstractGrowthModel`, which is used as a supertype of the models. This abstract type is always named using the process name in title case (using `titlecase()`), prefixed with `Abstract` and suffixed with `Model`. \ No newline at end of file diff --git a/docs/src/model_switching.md b/docs/src/step_by_step/model_switching.md similarity index 54% rename from docs/src/model_switching.md rename to docs/src/step_by_step/model_switching.md index 765c736c8..7d07bd232 100644 --- a/docs/src/model_switching.md +++ b/docs/src/step_by_step/model_switching.md @@ -23,15 +23,13 @@ models2 = ModelList( run!(models2, meteo_day) ``` -One of the main objective of PlantSimEngine is allowing users to switch between model implementations for a given process **without making any change to the code**. +One of the main objective of PlantSimEngine is allowing users to switch between model implementations for a given process **without making any change to the PlantSimEngine codebase**. -The package was carefully designed around this idea to make it easy and computationally efficient. This is done by using the `ModelList`, which is used to list models, and the `run!` function to run the simulation following the dependency graph and leveraging Julia's multiple dispatch to run the models. +The package was designed around this idea to make easy changes easy and efficient. Switch models in the `ModelList`, and call the `run!` function again. No other changes are required if no new variables are introduced. -## ModelList +## Example model switching -The `ModelList` is a container that holds a list of models, their parameter values, and the status of the variables associated to them. - -Model coupling is done by adding models to the `ModelList`. Let's create a `ModelList` with several models from the example scripts in the [`examples`](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/master/examples/) folder: +Let's create a `ModelList` with several models from the example scripts in the [`examples`](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/master/examples/) folder: Importing the models from the scripts: @@ -54,9 +52,7 @@ models = ModelList( nothing # hide ``` -PlantSimEngine uses the `ModelList` to compute the dependency graph of the models. Here we have seven models, one for each process. The dependency graph is computed automatically by PlantSimEngine, and is used to run the simulation in the correct order. - -We can run the simulation by calling the `run!` function with a meteorology. Here we use an example meteorology: +We can the simulation by calling the `run!` function with meteorology data. Here we use an example data set: ```@example usepkg meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) @@ -64,21 +60,12 @@ nothing # hide ``` !!! tip - To reproduce this meteorology, you can check the code presented [in this section in the FAQ](@ref defining_the_meteo) + For meteorology data details, you can check the code presented [in this section in the FAQ](@ref defining_the_meteo) We can now run the simulation: ```@example usepkg -run!(models, meteo_day) -``` - -!!! note - You'll notice a warning returned by `run!` here. If you read its content, you'll see it says that `ToyRUEGrowthModel` does not allow for parallel computations over time-steps. This is because it uses values from the previous time-steps in its computations. By default, `run!` makes the simulations in parallel, so to avoid the warning, you must explicitly tell it to use a sequential execution instead. To do so, you can use the `executor=SequentialEx()` keyword argument. - -And then we can access the status of the `ModelList` using the [`status`](@ref) function: - -```@example usepkg -status(models) +output_initial = run!(models, meteo_day) ``` Now what if we want to switch the model that computes growth ? We can do this by simply replacing the model in the `ModelList`, and PlantSimEngine will automatically update the dependency graph, and adapt the simulation to the new model. @@ -96,24 +83,19 @@ models2 = ModelList( nothing # hide ``` -`ToyAssimGrowthModel` is a little bit more complex than `ToyRUEGrowthModel`, as it also computes the maintenance and growth respiration of the plant, so it has more parameters (we use the default values here). +`ToyAssimGrowthModel` is a little bit more complex than `ToyRUEGrowthModel`, as it also computes the maintenance and growth respiration of the plant, so it has more parameters (we use the default values here). -We can run a new simulation: +We can run a new simulation and see that the simultion's results are different from the previous simulation: ```@example usepkg -run!(models2, meteo_day) +output_updated = run!(models2, meteo_day) ``` -And we can see that the status of the variables is different from the previous simulation: - -```@example usepkg -status(models2) -``` +And that's it! We can switch between models without changing the code, and without having to recompute the dependency graph manually. This is a very powerful feature of PlantSimEngine!💪 !!! note - In our example we replaced a soft-dependency model, but the same principle applies to hard-dependency models. - -And that's it! We can switch between models without changing the code, and without having to recompute the dependency graph manually. This is a very powerful feature of PlantSimEngine!💪 + This was a very standard but easy example. Sometimes other models will require to add other models to the `ModelList`. For example `ToyAssimGrowthModel` could have required a maintenance respiration model. In this case `PlantSimEngine` will tell you that this kind of model is required for the simulation. !!! note - This was a very standard but easy example. Sometimes other models will require to add other models to the `ModelList`. For example `ToyAssimGrowthModel` could have required a maintenance respiration model. In this case `PlantSimEngine` will tell you that this kind of model is required for the simulation. \ No newline at end of file + In our example we replaced a soft-dependency model, but the same principle applies to hard-dependency models. Hard and Soft dependencies are concepts explained in TODO + diff --git a/docs/src/step_by_step/parallelization.md b/docs/src/step_by_step/parallelization.md new file mode 100644 index 000000000..060a358e9 --- /dev/null +++ b/docs/src/step_by_step/parallelization.md @@ -0,0 +1,38 @@ +## Parallel execution + +!!! note + This section is likely to change and become outdated. In any case, parallel execution only currently applies to single-scale simulations (multi-scale simulations' changing MTGs and extra complexity don't allow for straightforward parallelisation) + +### FLoops + +`PlantSimEngine.jl` uses the [`Floops`](https://juliafolds.github.io/FLoops.jl/stable/) package to run the simulation in sequential, parallel (multi-threaded) or distributed (multi-process) computations over objects, time-steps and independent processes. + +That means that you can provide any compatible executor to the `executor` argument of [`run!`](@ref). By default, [`run!`](@ref) uses the [`ThreadedEx`](https://juliafolds.github.io/FLoops.jl/stable/reference/api/#executor) executor, which is a multi-threaded executor. You can also use the [`SequentialEx`](https://juliafolds.github.io/Transducers.jl/dev/reference/manual/#Transducers.SequentialEx)for sequential execution (non-parallel), or [`DistributedEx`](https://juliafolds.github.io/Transducers.jl/dev/reference/manual/#Transducers.DistributedEx) for distributed computations. + +### Parallel traits + +`PlantSimEngine.jl` uses [Holy traits](https://invenia.github.io/blog/2019/11/06/julialang-features-part-2/) to define if a model can be run in parallel. + +!!! note + A model is executable in parallel over time-steps if it does not uses or set values from other time-steps, and over objects if it does not uses or set values from other objects. + +You can define a model as executable in parallel by defining the traits for time-steps and objects. For example, the `ToyLAIModel` model from the [examples folder](https://github.com/VirtualPlantLab/PlantSimEngine.jl/tree/main/examples) can be run in parallel over time-steps and objects, so it defines the following traits: + +```julia +PlantSimEngine.TimeStepDependencyTrait(::Type{<:ToyLAIModel}) = PlantSimEngine.IsTimeStepIndependent() +PlantSimEngine.ObjectDependencyTrait(::Type{<:ToyLAIModel}) = PlantSimEngine.IsObjectIndependent() +``` + +By default all models are considered not executable in parallel, because it is the safest option to avoid bugs that are difficult to catch, so you only need to define these traits if it is executable in parallel for them. + +!!! tip + A model that is defined executable in parallel will not necessarily will. First, the user has to pass a parallel `executor` to [`run!`](@ref) (*e.g.* `ThreadedEx`). Second, if the model is coupled with another model that is not executable in parallel, `PlantSimEngine` will run all models in sequential. + +### Further executors + +You can also take a look at [FoldsThreads.jl](https://github.com/JuliaFolds/FoldsThreads.jl) for extra thread-based executors, [FoldsDagger.jl](https://github.com/JuliaFolds/FoldsDagger.jl) for +Transducers.jl-compatible parallel fold implemented using the Dagger.jl framework, and soon [FoldsCUDA.jl](https://github.com/JuliaFolds/FoldsCUDA.jl) for GPU computations +(see [this issue](https://github.com/VirtualPlantLab/PlantSimEngine.jl/issues/22)) and [FoldsKernelAbstractions.jl](https://github.com/JuliaFolds/FoldsKernelAbstractions.jl). You can also take a look at +[ParallelMagics.jl](https://github.com/JuliaFolds/ParallelMagics.jl) to check if automatic parallelization is possible. + +Finally, you can take a look into [Transducers.jl's documentation](https://github.com/JuliaFolds/Transducers.jl) for more information, for example if you don't know what is an executor, you can look into [this explanation](https://juliafolds.github.io/Transducers.jl/stable/explanation/glossary/#glossary-executor). diff --git a/docs/src/step_by_step/quick_and_dirty_examples.md b/docs/src/step_by_step/quick_and_dirty_examples.md new file mode 100644 index 000000000..353fef47c --- /dev/null +++ b/docs/src/step_by_step/quick_and_dirty_examples.md @@ -0,0 +1,85 @@ +# Quick examples + +This page is meant for people who have set up their environment and just want to copy-paste an example or two, see what the REPL returns and start tinkering. + +If you are less comfortable with Julia, or need to set up an environment first, see this section TODO. +If you wish for a more detailed rundown of the examples, you can instead have a look at the step by step section, which will go into more detail. TODO + +These examples are all for single-scale simulations. For multi-scale modelling and examples, refer to this section TODO + +You can find the implementaiton for these example models, as well as other toy models in TODO + +## Example with a single light interception model and a single weather timestep + +```@setup usepkg +using PlantSimEngine, PlantMeteo +using PlantSimEngine.Examples +meteo = Atmosphere(T = 20.0, Wind = 1.0, Rh = 0.65, Ri_PAR_f = 500.0) +leaf = ModelList(Beer(0.5), status = (LAI = 2.0,)) +out = run!(leaf, meteo) +``` + +## Coupling the light interception model with a Leaf Area Index model + +The weather data in this example contains data over 365 days, meaning the simulation will have as many timesteps. + +```@setup usepkg +using PlantSimEngine +using PlantMeteo, CSV + +using PlantSimEngine.Examples + +meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) + +models = ModelList( + ToyLAIModel(), + Beer(0.5), + status=(TT_cu=cumsum(meteo_day.TT),), +) + +outputs_coupled = run!(models, meteo_day) +``` + +## Coupling the light interception model with a Leaf Area Index model + + +```@setup usepkg +using PlantSimEngine +using PlantMeteo, CSV + +using PlantSimEngine.Examples + +meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) + +models = ModelList( + ToyLAIModel(), + Beer(0.5), + ToyRUEGrowthModel(0.2), + status=(TT_cu=cumsum(meteo_day.TT),), +) + +outputs_coupled = run!(models, meteo_day) +``` + +## Example using PlantBioPhysics + +A companion package, PlantBioPhysics, uses PlantSimEngine, and contains other models used in ecophysiological simulations. + +You can have a look at its documentation [here](https://vezy.github.io/PlantBiophysics.jl/stable/) + +Several example simulations are provided there. Here's one taken from [this page](https://vezy.github.io/PlantBiophysics.jl/stable/simulation/first_simulation/) : + +```julia +using PlantBiophysics, PlantSimEngine + +meteo = Atmosphere(T = 22.0, Wind = 0.8333, P = 101.325, Rh = 0.4490995) + +leaf = ModelList( + Monteith(), + Fvcb(), + Medlyn(0.03, 12.0), + status = (Ra_SW_f = 13.747, sky_fraction = 1.0, aPPFD = 1500.0, d = 0.03) + ) + +out = run!(leaf,meteo) +``` \ No newline at end of file diff --git a/docs/src/step_by_step/simple_model_coupling.md b/docs/src/step_by_step/simple_model_coupling.md new file mode 100644 index 000000000..7095ea2aa --- /dev/null +++ b/docs/src/step_by_step/simple_model_coupling.md @@ -0,0 +1,105 @@ +# Simple Model coupling + +## ModelList + +The `ModelList` is a container that holds a list of models, their parameter values, and the status of the variables associated to them. + +If one looks at prior examples, the Modellists so far have only contained a single model, whose input variables are initialised in the Modellist `status` keyword argument. + +Example models are all taken from the example scripts in the [`examples`](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/master/examples/) folder. + +Here's a first `ModelList` declaration with a light interception model, requiring input Leaf Area Index : + +```julia +leaf = ModelList(Beer(0.5), status = (LAI = 2.0,)) +``` + +Here's a second one with a Leaf Area Index model, with some example Cumulated Thermal Time as input. (This TT_cu is usually computed from weather data): + +```julia +model = ModelList( + ToyLAIModel(), + status=(TT_cu=1.0:2000.0,), # Pass the cumulated degree-days as input to the model +) +``` + +## Combining models + +Suppose we want our `ToyLAIModel()` to compute the `LAI` for the light interception model. + +We can couple the two models by having them be part of a single `ModelList`. The `LAI` variable will then be a coupled output-input and no longer will need to be declared. + +Here's a first attempt : + +```julia +using PlantSimEngine +# Import the examples defined in the `Examples` sub-module: +using PlantSimEngine.Examples + +# A ModelList with two coupled models +models = ModelList( + ToyLAIModel(), + Beer(0.5), + status=(TT_cu=1.0:2000.0,), +) + +run!(models) +``` + +Oops, we get an error related to the weather data : + +```julia +ERROR: type NamedTuple has no field Ri_PAR_f +Stacktrace: + [1] getindex(mnt::Atmosphere{(), Tuple{}}, i::Symbol) + @ PlantMeteo ~/Documents/CIRAD/dev/PlantMeteo/src/structs/atmosphere.jl:147 + [2] getcolumn(row::PlantMeteo.TimeStepRow{Atmosphere{(), Tuple{}}}, nm::Symbol) + @ PlantMeteo ~/Documents/CIRAD/dev/PlantMeteo/src/structs/TimeStepTable.jl:205 + ... +``` + +The `Beer()` model requires a specific meteorological parameter. Let's fix that by importing the example weather data : + +```julia +using PlantSimEngine + +# PlantMeteo and CSV packages are now used +using PlantMeteo, CSV + +# Import the examples defined in the `Examples` sub-module: +using PlantSimEngine.Examples + +# Import example weather data +meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) + +# A ModelList with two coupled models +models = ModelList( + ToyLAIModel(), + Beer(0.5), + status=(TT_cu=cumsum(meteo_day.TT),), # We can now compute a genuine cumulative thermal time from the weather data +) + +# Add the weather data to the run! call +outputs_coupled = run!(models, meteo_day) + +``` + +And there you have it. The light interception model made its computations using the Leaf Area Index computed by `ToyLAIModel`. + +## Further coupling + +Of course, one can keep adding models. Here's an example ModelList with another model, `ToyRUEGrowthModel`, which computes the carbon biomass increment caused by photosynthesis. + +```@example usepkg +models = ModelList( + ToyLAIModel(), + Beer(0.5), + ToyRUEGrowthModel(0.2), + status=(TT_cu=cumsum(meteo_day.TT),), +) + +nothing # hide +``` + +!!! note + You'll notice a warning returned by `run!` here. If you read its content, you'll see it says that `ToyRUEGrowthModel` does not allow for parallel computations over time-steps. This is because it uses values from the previous time-steps in its computations. By default, `run!` makes the simulations in parallel, so to avoid the warning, you must explicitly tell it to use a sequential execution instead. To do so, you can use the `executor=SequentialEx()` keyword argument. diff --git a/docs/src/new_doc/downstream_tests.md b/docs/src/troubleshooting_and_testing/downstream_tests.md similarity index 94% rename from docs/src/new_doc/downstream_tests.md rename to docs/src/troubleshooting_and_testing/downstream_tests.md index 33f0e14d8..5b3ac00c5 100644 --- a/docs/src/new_doc/downstream_tests.md +++ b/docs/src/troubleshooting_and_testing/downstream_tests.md @@ -1,4 +1,4 @@ -## Downstream tests +# Automated tests : downstream dependency checking PlantSimEngine is open sourced on Github [](TODO), and so are its other companion packages, PlantGeom, PlantMeteo, PlantBioPhysics, MultiScaleTreeGraph, and XPalm. diff --git a/docs/src/new_doc/implicit_contracts.md b/docs/src/troubleshooting_and_testing/implicit_contracts.md similarity index 98% rename from docs/src/new_doc/implicit_contracts.md rename to docs/src/troubleshooting_and_testing/implicit_contracts.md index 2a366621f..3035b5705 100644 --- a/docs/src/new_doc/implicit_contracts.md +++ b/docs/src/troubleshooting_and_testing/implicit_contracts.md @@ -62,4 +62,6 @@ A workaround for some situations is described here TODO ## Status template intialisation order TODO -## Diffusion systems ? \ No newline at end of file +## Diffusion systems ? + +## TODO simulation order, node order, etc. \ No newline at end of file diff --git a/docs/src/new_doc/plantsimengine_and_julia_troubleshooting.md b/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md similarity index 93% rename from docs/src/new_doc/plantsimengine_and_julia_troubleshooting.md rename to docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md index dc6c12cd3..1a00413af 100644 --- a/docs/src/new_doc/plantsimengine_and_julia_troubleshooting.md +++ b/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md @@ -1,6 +1,6 @@ -# Troubleshooting error messages (both PlantSimEngine and Julia) +# Troubleshooting error messages -PlantSimEngine attempts to be as comfortable and easy to use as possible for the user, and many kinds of user error will be caught and explanations provided to resolve them, but there are still many blind spots, as well as syntax errors that will often generate a Julia error (which can be unintuitive to decrypt) rather than a PlantSimEngine error. +PlantSimEngine attempts to be as comfortable and easy to use as possible for the user, and many kinds of user error will be caught and explanations provided to resolve them, but there are still blind spots, as well as syntax errors that will often generate a Julia error (which can be less intuitive to decrypt) rather than a PlantSimEngine error. To help people newer to Julia with troubleshooting, here are a few common 'easy-to-make' mistakes with the current API that might not be obvious to interpret, and pointers on how to fix them. @@ -134,6 +134,31 @@ Closest candidates are: The message 'got unsupported keyword argument "model"' can be misleading, as in the error in this case is not that a kwarg is *unsupported*, but rather that a keyword argument is *missing*. +### MultiScaleModel : variable not defined in Module + +A possible cause for this error is that a variable was declared instead of a symbol in a mapping for a multiscale model : + +```julia +mapping = Dict("Scale" => +MultiScaleModel( + model = ToyModel(), + mapping = [should_be_symbol => "Other_Scale"] # should_be_symbol is a variable, likely not found in the current module +), +... +), +``` + +Here's the correct version : +```julia +mapping = Dict("Scale" => +MultiScaleModel( + model = ToyModel(), + mapping = [:should_be_symbol => "Other_Scale"] # should_be_symbol is now a symbol +), +... +), +``` + ### Kwarg and arg parameter issues when calling run! There are, unfortunately, multiple ways of passing in arguments to the run! functions that will confuse dynamic dispatch. Some of it is due to imperfections in type declarations on PlantSimEngine's end and may be improved upon in the future. diff --git a/docs/src/model_coupling/tips_and_workarounds.md b/docs/src/troubleshooting_and_testing/tips_and_workarounds.md similarity index 93% rename from docs/src/model_coupling/tips_and_workarounds.md rename to docs/src/troubleshooting_and_testing/tips_and_workarounds.md index d10b28f9c..7ca33c113 100644 --- a/docs/src/model_coupling/tips_and_workarounds.md +++ b/docs/src/troubleshooting_and_testing/tips_and_workarounds.md @@ -16,7 +16,7 @@ It is possible to make use of the value of a variable in the past simulation tim However, it is not possible to go beyond that through the mapping API. Something like `PreviousTimeStep(PreviousTimeStep(PreviousTimeStep(:carbon_biomass)))` is not supported. Don't do that. -One way to access prior variable states is simply to write an ad hoc model that stores a few values into an array or however many variables you might need, which you can then feed into other models that might need it. +One way to access prior variable states is simply to write an ad hoc model that stores a few values into an array or however many variables you might need, which you can then update every timestep and feed into other models that might need it. ## Having a variable simultaneously as input and output of a model @@ -66,4 +66,8 @@ This feature is likely to break in simulations that make use of planned future f TODO examples of other ad hoc models TODO state machines ? -TODO workaround status initialisation bug ? \ No newline at end of file +TODO workaround status initialisation bug ? + +## Cyclic dependencies in single-scale simulations + +Cyclic dependencies can happen in single-scale simulations, but the PreviousTimestep feature currently isn't available. Hard dependencies are one way to deal with them, creating a multi-scale simulation with a single effective scale is also an option. \ No newline at end of file diff --git a/docs/src/fitting.md b/docs/src/working_with_data/fitting.md similarity index 100% rename from docs/src/fitting.md rename to docs/src/working_with_data/fitting.md diff --git a/docs/src/extending/inputs.md b/docs/src/working_with_data/inputs.md similarity index 100% rename from docs/src/extending/inputs.md rename to docs/src/working_with_data/inputs.md diff --git a/docs/src/reducing_dof.md b/docs/src/working_with_data/reducing_dof.md similarity index 98% rename from docs/src/reducing_dof.md rename to docs/src/working_with_data/reducing_dof.md index 8f6b53aa3..5b010920b 100644 --- a/docs/src/reducing_dof.md +++ b/docs/src/working_with_data/reducing_dof.md @@ -16,7 +16,7 @@ end ## Introduction -### Why reducing the degrees of freedom +### Why reduce the degrees of freedom Reducing the degrees of freedom in a model, by forcing certain variables to measurements, can be useful for several reasons: @@ -78,9 +78,7 @@ m2 = ModelList( status=(var0 = 0.5, var9 = 10.0), ) -run!(m2, meteo) - -status(m2) +out = run!(m2, meteo) ``` And that's it ! The models that depend on `var9` will now use the measured value of `var9` instead of the one computed by `Process7Model`. @@ -116,9 +114,7 @@ m3 = ModelList( status = (var0=0.5,var3 = 10.0) ) -run!(m3, meteo) - -status(m3) +out = run!(m3, meteo) ``` !!! note diff --git a/docs/src/working_with_data/visualising_outputs.md b/docs/src/working_with_data/visualising_outputs.md new file mode 100644 index 000000000..760629490 --- /dev/null +++ b/docs/src/working_with_data/visualising_outputs.md @@ -0,0 +1,80 @@ +TODO example environment ? + +## Output structure + +PlantSimEngine's run! functions return for each timestep the state of the variables that were requested using the `tracked_outputs` kwarg (or the state of every variable if this kwarg was left unspecified). Multi-scale simulations also indicate which organ and MTG node these state variables are related to. + +Here's an example indicating how to plot output data using CairoMakie, a package used for plotting. + +```julia +# ] add PlantSimEngine, DataFrames, CSV +using PlantSimEngine, PlantMeteo, DataFrames, CSV + +# Include the model definition from the examples folder: +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: +model = 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: +sim_outputs = run!(model, meteo_day) + +``` + +The output data is displayed as : + +``` +TimeStepTable{Status{(:TT_cu, :LAI...}(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 │ +│ ⋮ │ ⋮ │ ⋮ │ ⋮ │ +╰─────┴────────────────┴────────────┴───────────╯ + 360 rows omitted +``` + +And using CairoMakie, one can plot out selected variables : + +```julia +# Plot the results: +using CairoMakie + +fig = Figure(resolution=(800, 600)) +ax = Axis(fig[1, 1], ylabel="LAI (m² m⁻²)") +lines!(ax, model[:TT_cu], model[:LAI], color=:mediumseagreen) + +ax2 = Axis(fig[2, 1], xlabel="Cumulated growing degree days since sowing (°C)", ylabel="aPPFD (mol m⁻² d⁻¹)") +lines!(ax2, model[:TT_cu], model[:aPPFD], color=:firebrick1) + +fig +``` + +![LAI Growth and light interception](examples/LAI_growth2.png) + +## TimeStepTables and DataFrames + +The output data is usually stored in a `TimeStepTable` structure defined in `PlantMeteo.jl`, which is a fast DataFrame-alike structure with each time step being a [`Status`](@ref). It can be also be any `Tables.jl` structure, such as a regular `DataFrame`. Weather data is also usually stored in a `TimeStepTable` but with each time step being an `Atmosphere`. + +TODO example extracting specific variables + +Another simple way to get the results is to transform the outputs into a `DataFrame`. Which is very easy because the `TimeStepTable` implements the Tables.jl interface: + +```@example usepkg +using DataFrames +outputs(sim_outputs, DataFrames) +``` + +TODO other examples ? \ No newline at end of file diff --git a/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation1.jl b/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation1.jl new file mode 100644 index 000000000..37bd2a2c5 --- /dev/null +++ b/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation1.jl @@ -0,0 +1,148 @@ + +########################################### +# Toy plant model +# Physiologically meaningless but illustrates organ creation +########################################### + +function get_n_leaves(node::MultiScaleTreeGraph.Node) + root = MultiScaleTreeGraph.get_root(node) + nleaves = length(MultiScaleTreeGraph.traverse(root, x->1, symbol="Leaf")) + return nleaves +end + +PlantSimEngine.@process "organ_emergence" verbose = false + +struct ToyCustomInternodeEmergence <: AbstractOrgan_EmergenceModel + TT_emergence::Float64 + carbon_internode_creation_cost::Float64 + leaf_surface_area::Float64 + leaves_max_surface_area::Float64 +end + +ToyCustomInternodeEmergence(;TT_emergence=300.0, carbon_internode_creation_cost=200.0, leaf_surface_area=3.0, leaves_max_surface_area=100.0) = ToyCustomInternodeEmergence(TT_emergence, carbon_internode_creation_cost, leaf_surface_area, leaves_max_surface_area) + +PlantSimEngine.inputs_(m::ToyCustomInternodeEmergence) = (TT_cu=0.0, carbon_stock=0.0) +PlantSimEngine.outputs_(m::ToyCustomInternodeEmergence) = (TT_cu_emergence=0.0, carbon_organ_creation_consumed=0.0) + +function PlantSimEngine.run!(m::ToyCustomInternodeEmergence, models, status, meteo, constants=nothing, sim_object=nothing) + + leaves_surface_area = m.leaf_surface * get_n_leaves(status.node) + status.carbon_organ_creation_consumed = 0.0 + + if leaves_surface_area > m.leaves_max_surface_area + return nothing + end + + # if not enough carbon, no organ creation + if status.carbon_stock < m.carbon_internode_creation_cost + return nothing + end + + if length(MultiScaleTreeGraph.children(status.node)) == 2 && + 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) + add_organ!(status_new_internode.node, sim_object, "+", "Leaf", 2, index=1) + + status_new_internode.TT_cu_emergence = m.TT_emergence - status.TT_cu + status.carbon_organ_creation_consumed = m.carbon_internode_creation_cost + end + + return nothing +end + +########################## +### Model accumulating carbon resources +########################## + +PlantSimEngine.@process "resource_stock_computation" verbose = false + +struct ToyStockComputationModel <: AbstractResource_Stock_ComputationModel +end + +PlantSimEngine.inputs_(::ToyStockComputationModel) = +(carbon_captured=0.0,carbon_organ_creation_consumed=0.0) + +PlantSimEngine.outputs_(::ToyStockComputationModel) = (carbon_stock=-Inf,) + +function PlantSimEngine.run!(m::ToyStockComputationModel, models, status, meteo, constants=nothing, extra=nothing) + status.carbon_stock += sum(status.carbon_captured) - sum(status.carbon_organ_creation_consumed) +end + +PlantSimEngine.TimeStepDependencyTrait(::Type{<:ToyStockComputationModel}) = PlantSimEngine.IsTimeStepIndependent() +PlantSimEngine.ObjectDependencyTrait(::Type{<:ToyStockComputationModel}) = PlantSimEngine.IsObjectIndependent() + +######################## +## Leaf model capturing some arbitrary carbon quantity +######################## + +PlantSimEngine.@process "leaf_carbon_capture" verbose = false + +struct ToyLeafCarbonCaptureModel<: AbstractLeaf_Carbon_CaptureModel end + +function PlantSimEngine.inputs_(::ToyLeafCarbonCaptureModel) + NamedTuple()#(TT_cu=-Inf) +end + +function PlantSimEngine.outputs_(::ToyLeafCarbonCaptureModel) + (carbon_captured=0.0,) +end + +function PlantSimEngine.run!(::ToyLeafCarbonCaptureModel, models, status, meteo, constants, extra) + # very crude approximation with LAI of 1 and constant PPFD + status.carbon_captured = 200.0 *(1.0 - exp(-0.2)) +end + +PlantSimEngine.ObjectDependencyTrait(::Type{<:ToyLeafCarbonCaptureModel}) = PlantSimEngine.IsObjectIndependent() +PlantSimEngine.TimeStepDependencyTrait(::Type{<:ToyLeafCarbonCaptureModel}) = PlantSimEngine.IsTimeStepIndependent() + +mapping = Dict( +"Scene" => ToyDegreeDaysCumulModel(), +"Plant" => ( + MultiScaleModel( + model=ToyStockComputationModel(), + mapping=[ + :carbon_captured=>["Leaf"], + :carbon_organ_creation_consumed=>["Internode"] + ], + ), + Status(carbon_stock = 0.0) + ), +"Internode" => ( + MultiScaleModel( + model=ToyCustomInternodeEmergence(),#TT_emergence=20.0), + mapping=[:TT_cu => "Scene", + PreviousTimeStep(:carbon_stock)=>"Plant"], + ), + Status(carbon_organ_creation_consumed=0.0), + ), +"Leaf" => ( ToyLeafCarbonCaptureModel(),), +) + + mtg = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Scene", 1, 0)) +#MultiScaleTreeGraph.Node(mtg, MultiScaleTreeGraph.NodeMTG("/", "Soil", 1, 1)) + plant = MultiScaleTreeGraph.Node(mtg, MultiScaleTreeGraph.NodeMTG("+", "Plant", 1, 1)) + + internode1 = MultiScaleTreeGraph.Node(plant, MultiScaleTreeGraph.NodeMTG("/", "Internode", 1, 2)) + MultiScaleTreeGraph.Node(internode1, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) + MultiScaleTreeGraph.Node(internode1, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) + + internode2 = MultiScaleTreeGraph.Node(internode1, MultiScaleTreeGraph.NodeMTG("<", "Internode", 1, 2)) + MultiScaleTreeGraph.Node(internode2, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) + MultiScaleTreeGraph.Node(internode2, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) + + + meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) + + outs = run!(mtg, mapping, meteo_day) + mtg + + length(MultiScaleTreeGraph.traverse(mtg,x->x, symbol="Leaf")) + + for i in 1:365 + for j in 1:length(outs["Plant"][:carbon_organ_creation_consumed][i]) + if outs["Plant"][:carbon_organ_creation_consumed][i][1][j] != 0.0 + println(i) + end + end + end \ No newline at end of file diff --git a/examples/ToyPlantSimulation.jl b/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation2.jl similarity index 89% rename from examples/ToyPlantSimulation.jl rename to examples/ToyMultiScalePlantTutorial/ToyPlantSimulation2.jl index 2d239bbd8..c717ebe78 100644 --- a/examples/ToyPlantSimulation.jl +++ b/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation2.jl @@ -16,30 +16,31 @@ function get_roots_count(node::MultiScaleTreeGraph.Node) return length(MultiScaleTreeGraph.traverse(root, x->x, symbol="Root")) end -function get_leaves_total_surface(node::MultiScaleTreeGraph.Node, leaf_surface) +function get_n_leaves(node::MultiScaleTreeGraph.Node) root = MultiScaleTreeGraph.get_root(node) nleaves = length(MultiScaleTreeGraph.traverse(root, x->1, symbol="Leaf")) - return leaf_surface*nleaves + return nleaves end PlantSimEngine.@process "organ_emergence" verbose = false -struct ToyInternodeEmergence <: AbstractOrgan_EmergenceModel +struct ToyCustomInternodeEmergence <: AbstractOrgan_EmergenceModel TT_emergence::Float64 carbon_internode_creation_cost::Float64 + leaf_surface_area::Float64 leaves_max_surface_area::Float64 water_leaf_threshold::Float64 end -ToyInternodeEmergence(;TT_emergence=300.0, carbon_internode_creation_cost=200.0, leaves_max_surface_area=100.0, -water_leaf_threshold=30.0) = ToyInternodeEmergence(TT_emergence, carbon_internode_creation_cost, leaves_max_surface_area, water_leaf_threshold) +ToyCustomInternodeEmergence(;TT_emergence=300.0, carbon_internode_creation_cost=200.0, leaf_surface_area=3.0,leaves_max_surface_area=100.0, +water_leaf_threshold=30.0) = ToyCustomInternodeEmergence(TT_emergence, carbon_internode_creation_cost, leaf_surface_area, leaves_max_surface_area, water_leaf_threshold) -PlantSimEngine.inputs_(m::ToyInternodeEmergence) = (TT_cu=0.0,water_stock=0.0, carbon_stock=0.0) -PlantSimEngine.outputs_(m::ToyInternodeEmergence) = (TT_cu_emergence=0.0, carbon_organ_creation_consumed=0.0) +PlantSimEngine.inputs_(m::ToyCustomInternodeEmergence) = (TT_cu=0.0,water_stock=0.0, carbon_stock=0.0) +PlantSimEngine.outputs_(m::ToyCustomInternodeEmergence) = (TT_cu_emergence=0.0, carbon_organ_creation_consumed=0.0) -function PlantSimEngine.run!(m::ToyInternodeEmergence, models, status, meteo, constants=nothing, sim_object=nothing) +function PlantSimEngine.run!(m::ToyCustomInternodeEmergence, models, status, meteo, constants=nothing, sim_object=nothing) - leaves_surface_area = get_leaves_total_surface(status.node, 3.0) + leaves_surface_area = m.leaf_surface_area * get_n_leaves(status.node) status.carbon_organ_creation_consumed = 0.0 if leaves_surface_area > m.leaves_max_surface_area @@ -192,7 +193,7 @@ mapping = Dict( ), "Internode" => ( MultiScaleModel( - model=ToyInternodeEmergence(),#TT_emergence=20.0), + model=ToyCustomInternodeEmergence(),#TT_emergence=20.0), mapping=[:TT_cu => "Scene", PreviousTimeStep(:water_stock)=>"Plant", PreviousTimeStep(:carbon_stock)=>"Plant"], @@ -232,7 +233,7 @@ mapping = Dict( meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) - outputs = run!(mtg, mapping, meteo_day) + outs = run!(mtg, mapping, meteo_day) mtg From 41891ed9df67d714e319245f64ec667773557aa7 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Fri, 21 Feb 2025 15:48:15 +0100 Subject: [PATCH 061/147] More documentation updates. This commit is mostly minor edits, fixing links, etc --- docs/make.jl | 33 +++++---------- docs/src/introduction/why_julia.md | 6 ++- docs/src/multiscale/multiscale_example_1.md | 8 ++-- docs/src/multiscale/multiscale_example_2.md | 2 + docs/src/planned_features.md | 4 +- docs/src/prerequisites/design.md | 2 +- docs/src/prerequisites/julia_basics.md | 2 +- docs/src/prerequisites/key_concepts.md | 12 +++--- docs/src/step_by_step/advanced_coupling.md | 2 +- docs/src/step_by_step/implement_a_model.md | 41 ++++++++++--------- docs/src/step_by_step/implement_a_process.md | 6 +-- docs/src/step_by_step/model_switching.md | 4 +- .../step_by_step/quick_and_dirty_examples.md | 14 +++---- .../src/step_by_step/simple_model_coupling.md | 21 +++++++--- .../downstream_tests.md | 4 +- .../implicit_contracts.md | 6 +-- ...lantsimengine_and_julia_troubleshooting.md | 8 ++-- .../tips_and_workarounds.md | 3 +- .../working_with_data/visualising_outputs.md | 4 +- 19 files changed, 94 insertions(+), 88 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index dd96c4b2c..d969fc87b 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,3 +1,5 @@ +using Pkg +Pkg.develop("PlantSimEngine") using PlantSimEngine using PlantMeteo using DataFrames, CSV @@ -22,25 +24,26 @@ makedocs(; pages=[ "Home" => "index.md", "Introduction" => [ - #"Organization of the documentation" => "./introduction/TODO.md", + #"Organization of the documentation ?" "Why PlantSimEngine ?" => "./introduction/why_plantsimengine.md", "Why Julia ?" => "./introduction/why_julia.md", - #"Overview" => "design.md" TODO, "Feature list ? Companion packages ? TODO" + #"Overview ?" + #"Feature list ? Companion packages ?" ], "Prerequisites" => [ - "Key Concepts" => "./prerequisites/key_concepts.md", - #"Setup" => "TODO.md", + "Key Concepts" => "./prerequisites/key_concepts.md", # Key concepts vs terminology ? + #"Setup" ?", "Julia language basics" => "./prerequisites/julia_basics.md", - "Design" =>"./prerequisites/design.md", + "Design" =>"./prerequisites/design.md", # Rework with key concepts ], "Step by step" => [ - #"First Simulation" => "./step_by_step/TODO.md", + #"First Simulation" => ", "Coupling" => "./step_by_step/simple_model_coupling.md", "Model Switching" => "./step_by_step/model_switching.md", "Processes" => "./step_by_step/implement_a_process.md", "Implementing a model" => "./step_by_step/implement_a_model.md", "Parallelization" => "./step_by_step/parallelization.md", - "Advanced coupling" => "./step_by_step/advanced_coupling.md" + "Advanced coupling and hard dependencies" => "./step_by_step/advanced_coupling.md" ], "Execution" => "model_execution.md", "Working with data" => [ @@ -53,7 +56,7 @@ makedocs(; "Multiscale" => [ "Detailed example" => "./multiscale/multiscale.md", "Handling cyclic dependencies" => "./multiscale/multiscale_cyclic.md", - "Multiscale coupling considerations" => "./multiscale/multiscale_coupling.md", + "Multiscale coupling considerations" => "./multiscale/multiscale_coupling.md", # TODO expand upon this "Building a simple plant" => [ "A rudimentary plant simulation" => "./multiscale/multiscale_example_1.md", "Expanding the plant simulation" => "./multiscale/multiscale_example_2.md", @@ -77,17 +80,3 @@ deploydocs(; repo="github.com/VirtualPlantLab/PlantSimEngine.jl.git", devbranch="main" ) - - -using PlantSimEngine, PlantMeteo -using Pkg -Pkg.develop("PlantSimEngine") -# Import the examples defined in the `Examples` sub-module -using PlantSimEngine.Examples - -meteo = Atmosphere(T = 20.0, Wind = 1.0, Rh = 0.65, Ri_PAR_f = 500.0) - -leaf = ModelList(Beer(0.5), status = (LAI = 2.0,)) - -outputs_example = @enter run!(leaf, meteo) -outputs_example[:aPPFD] \ No newline at end of file diff --git a/docs/src/introduction/why_julia.md b/docs/src/introduction/why_julia.md index eb674723c..bbdf11f0e 100644 --- a/docs/src/introduction/why_julia.md +++ b/docs/src/introduction/why_julia.md @@ -1,4 +1,6 @@ -PlantSimEngine is implemented in Julia. It arose from a particular combination of [needs and requirements](TODO), a combination which Julia seemed to fill adequately. +# The choice of using Julia + +PlantSimEngine is implemented in Julia. It arose from a particular combination of [needs and requirements](why_plantsimengine.md), a combination which Julia seemed to fill adequately. Other modelling frameworks, FSPMs and crop models are -often- written in combinations of Java, C++, Python, or Fortran. Given that it isn't the language many researchers (and developers !) are most familiar with, this page provides a short explanation of the reasoning behind that language choice. It might not have been the only possible valid choice, of course. @@ -24,7 +26,7 @@ Julia, with its 'Just-ahead-of-time' compilation model and its flexibility allow ### Flexibility, ease of use -PlantSimEngine was also developed with a few goals in mind, one of them being to make hypothesis testing quite easy. It is currently difficult to validate FSPM, crop model or ecophysiological hypotheses because TODO +PlantSimEngine was also developed with a few goals in mind, one of them being to make hypothesis testing quite easy. It is currently difficult to validate FSPM, crop model or ecophysiological hypotheses TODO Similarly, when developing a full-featured FSPM, there might be a need to test different models for a specific process, or to switch a model for a more complex one. API and language ease of use is as much of a factor as automated model coupling in keeping these changes smooth. diff --git a/docs/src/multiscale/multiscale_example_1.md b/docs/src/multiscale/multiscale_example_1.md index 8f8207b4a..f678b5ac5 100644 --- a/docs/src/multiscale/multiscale_example_1.md +++ b/docs/src/multiscale/multiscale_example_1.md @@ -4,11 +4,11 @@ TODO change Toy To Example ? This section iteratively walks you through building a multi-scale simulation. -The actual plant being simulated, as well as some of the ad hoc processes, mostly have no physical meaning and are very much ad hoc (which is why they aren't part of the TODO examples folder). Similarly, some of the parameter values are pulled out of thin air, and have no ties to research papers or data. +The actual plant being simulated, as well as some of the ad hoc processes, mostly have no physical meaning and are very much ad hoc (which is why most of them aren't standalone in the examples folder). Similarly, some of the parameter values are pulled out of thin air, and have no ties to research papers or data. The main purpose here is to showcase PlantSimEngine's multi-scale features and how to structure your models, not accuracy, realism or performance. -You can find the full script for this simulation in TODO +You can find the full script for this simulation in the [ToyMultiScalePlantModel](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/ToyMultiScalePlantModel/ToyPlantSimulation1.jl) subfolder of the examples folder. ## A basic growing plant @@ -24,7 +24,7 @@ We'll make the assumption the internodes make use of carbon from a common pool. One way of modeling this approach translates into several scales and models : -- Scene scale, for thermal time. The `ToyDegreeDaysCumulModel()` from the examples folder provides thermal time from temperature data *TODO +- Scene scale, for thermal time. The `ToyDegreeDaysCumulModel()` [from the examples folder](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/ToyDegreeDays.jl) provides thermal time from temperature data - Plant scale, where we'll define the carbon pool - Internode scale, which draws from the pool to create new organs - Leaf scale, which captures carbon @@ -92,7 +92,7 @@ end ### Organ creation -This model is a modified version of the `ToyInternodeEmergence()` model found in the examples folder TODO. An internode produces two leaves and a new internode. +This model is a modified version of the `ToyInternodeEmergence()` model found [in the examples folder](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/ToyInternodeEmergence.jl). An internode produces two leaves and a new internode. Let's first define a helper function that iterates across a Multiscale Tree Graph and returns the number of leaves : diff --git a/docs/src/multiscale/multiscale_example_2.md b/docs/src/multiscale/multiscale_example_2.md index 7cb35430a..78f03e530 100644 --- a/docs/src/multiscale/multiscale_example_2.md +++ b/docs/src/multiscale/multiscale_example_2.md @@ -2,6 +2,8 @@ Let's build on the previous example and add some other organ growth, as well as some very mild coupling between the two. +You can find the full script for this simulation in the [ToyMultiScalePlantModel](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/ToyMultiScalePlantModel/ToyPlantSimulation2.jl) subfolder of the examples folder. + ## Adding roots to our plant We'll add a root that extracts water and adds it to the stock. Initial water stocks are low, so root growth is prioritized, then the plant also grows leaves and a new internode like it did before. Roots only grow up to a certain point, and don't branch. diff --git a/docs/src/planned_features.md b/docs/src/planned_features.md index ab39b592f..4f55be33c 100644 --- a/docs/src/planned_features.md +++ b/docs/src/planned_features.md @@ -30,6 +30,4 @@ iteratively build and validate mappings and modellists ? documenting FP errors, more examples for fitting/type conversion/error propagation Improve multiscale dependency API ? -other packages ? - -TODO credits ? \ No newline at end of file +other package planned features ? \ No newline at end of file diff --git a/docs/src/prerequisites/design.md b/docs/src/prerequisites/design.md index 2b43dff28..5bda62c4d 100644 --- a/docs/src/prerequisites/design.md +++ b/docs/src/prerequisites/design.md @@ -237,4 +237,4 @@ DataFrame(leaf) A model can work either independently or in conjunction with other models. For example a stomatal conductance model is often associated with a photosynthesis model, *i.e.* it is called from the photosynthesis model. -`PlantSimEngine.jl` is designed to make model coupling painless for modelers and users. Please see [Model coupling for users](@ref) and [Model coupling for modelers](@ref) for more details. \ No newline at end of file +`PlantSimEngine.jl` is designed to make model coupling painless for modelers and users. Please see [Standard model coupling](@ref) and [Coupling more complex models](@ref) for more details, or Multiscale coupling considerations TODO for multi-scale specific coupling considerations. \ No newline at end of file diff --git a/docs/src/prerequisites/julia_basics.md b/docs/src/prerequisites/julia_basics.md index 87973d07c..521bd0484 100644 --- a/docs/src/prerequisites/julia_basics.md +++ b/docs/src/prerequisites/julia_basics.md @@ -1,6 +1,6 @@ # Getting started with Julia -PlantSimEngine (as well as its related packages) is written in Julia. The reasons why Julia was chosen are briefly discussed here TODO +PlantSimEngine (as well as its related packages) is written in Julia. The reasons why Julia was chosen are briefly discussed here : [The choice of using Julia](@ref). Julia is a language that is gaining traction, but it isn't the most widely used in research and data science. diff --git a/docs/src/prerequisites/key_concepts.md b/docs/src/prerequisites/key_concepts.md index 54daab0a8..4c9459b1e 100644 --- a/docs/src/prerequisites/key_concepts.md +++ b/docs/src/prerequisites/key_concepts.md @@ -12,11 +12,11 @@ You'll find a brief description of some of the main concepts and terminology rel A process in this package defines a biological or physical phenomena. Think of any process happening in a system, such as light interception, photosynthesis, water, carbon and energy fluxes, growth, yield or even electricity produced by solar panels. -TODO link +See [Implementing a new process](@ref) for a brief explanation on how to declare a new process. ## Models -Models are then implemented for a particular process. TODO link +Models are then implemented for a particular process. There may be different models that can be used for the same process ; for instance, there are multiple hypotheses and ways of modeling photosynthesis, with different granularity and accuracy. A simple photosynthesis model might apply a simple formula and apply it to the total leaf surface, a more complex one might calculate interception and light extinction. @@ -44,7 +44,7 @@ TODO image PlantSimEngine creates this DAG under the hood by plugging the right variables in the right models. Users therefore only need to declare models, they do not need write the code to connect them as PlantSimEngine does that work for them. -### "Hard" and "Soft" dependencies +### ["Hard" and "Soft" dependencies](@id hard_dependency_def) Linking models by finding which output variables are used as input of another model handles many of the coupling situations that can occur (with more situations occurring with multi-scale models and variables), but what if two models are interdependent ? If they need to iterate on some computation and pass variables back and forth ? @@ -77,7 +77,7 @@ meteo = Atmosphere(T = 20.0, Wind = 1.0, Rh = 0.65, Ri_PAR_f = 500.0) More details are available from the [package documentation](https://vezy.github.io/PlantMeteo.jl/stable). If you do not wish to make use of this package, you can alternately provide your own data, as long as it respects the [Tables.jl interface](https://tables.juliadata.org/stable/#Implementing-the-Interface-(i.e.-becoming-a-Tables.jl-source)). -If you wish to make use of more fine-grained weather data, it will likely require more advanced model creation and MTG manipulation, and more involved work on the modeling side. TODO +If you wish to make use of more fine-grained weather data, it will likely require more advanced model creation and MTG manipulation, and more involved work on the modeling side.∂ ### Organ/Scale @@ -93,7 +93,9 @@ For example, a model of photosynthesis at the leaf scale can be combined with a When running multi-scale simulations which contain models operating at different organ levels for the plant, extra information needs to be provided by the user to run models. Since some models are reused at different organ levels, it is necessary to indicate which organ level a model operates at. -This is why multi-scale simulations make use of a 'mapping' : the ModelList in the single-scale examples does not have a way to tie models to plant organs,and the more versatile models could be used in various places. The user must also indicate how models operate with other scales, e.g. if an input variable comes from another scale TODO example, it is required to indicate which scale it is mapped from. +This is why multi-scale simulations make use of a 'mapping' : the ModelList in the single-scale examples does not have a way to tie models to plant organs,and the more versatile models could be used in various places. The user must also indicate how models operate with other scales, e.g. if an input variable comes from another scale, it is required to indicate which scale it is mapped from. + +TODO ### Multi-scale Tree Graphs diff --git a/docs/src/step_by_step/advanced_coupling.md b/docs/src/step_by_step/advanced_coupling.md index 1f81439a2..864a26184 100644 --- a/docs/src/step_by_step/advanced_coupling.md +++ b/docs/src/step_by_step/advanced_coupling.md @@ -16,7 +16,7 @@ m = ModelList( ) ``` -When two or more models have a two-way interdependency (rather than variables flowing out only one-way from one model into the next), we describe it as a [hard dependency](TODO). +When two or more models have a two-way interdependency (rather than variables flowing out only one-way from one model into the next), we describe it as a [hard dependency](@ref hard_dependency_def). This kind of interdependency requires a little more work from the user/modeler for PlantSimEngine to be able to automatically create the dependency graph. diff --git a/docs/src/step_by_step/implement_a_model.md b/docs/src/step_by_step/implement_a_model.md index 09aa3bba4..31c13de77 100644 --- a/docs/src/step_by_step/implement_a_model.md +++ b/docs/src/step_by_step/implement_a_model.md @@ -58,7 +58,7 @@ PlantSimEngine.TimeStepDependencyTrait(::Type{<:Beer}) = PlantSimEngine.IsTimeSt And that is all you need to get going, for this simple example with a single parameter and no interdependencies. -The `@process` macro does a few things under the hood described [here](TODO) +The `@process` macro does some boilerplate work described [here](@ref under_the_hood) If you have more than one parameter, then type conversion utility functions might also be interesting to implement. See here TODO If you need to deal with more complex couplings, the hard dependency section will detail @@ -86,7 +86,7 @@ In those files, you'll see that in order to implement a new model you'll need to - the actual model, developed as a method for the process it simulates - some helper functions used by the package and/or the users -If you create your own process, the function will print a short tutorial on how to do all that, adapted to the process you just created (see [Implement a new process](@ref)). +If you create your own process, the function will print a short tutorial on how to do all that, adapted to the process you just created (see [Implementing a new process](@ref)). In this page, we'll just implement a model for a process that already exists: the light interception. This process is defined in `PlantBiophysics.jl`, and also made available as an example model from the `Examples` sub-module. You can access the script from here: [`examples/Beer.jl`](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/Beer.jl). @@ -109,7 +109,7 @@ We declare the light interception process at l.7 using [`@process`](@ref): @process "light_interception" verbose = false ``` -See [Implement a new process](@ref) for more details on how that works and how to use the process. +See [Implementing a new process](@ref) for more details on how that works and how to use the process. ### The structure @@ -226,22 +226,6 @@ end Note that both functions end with an "\_". This is because these functions are internal, they will not be called by the users directly. Users will use [`inputs`](@ref) and [`outputs`](@ref) instead, which call `inputs_` and `outputs_`, but stripping out the default values. -### Dependencies - -If your model explicitly calls another model, you need to tell PlantSimEngine about it. This is called a hard dependency, in opposition to a soft dependency, which is when your model uses a variable from another model, but does not call it explicitly. - -To do so, we can add a method to the `dep` function that tells PlantSimEngine which processes (and models) are needed for the model to run. - -Our example model does not call another model, so we don't need to implement it. But we can look at *e.g.* the implementation for [`Fvcb`](https://github.com/VEZY/PlantBiophysics.jl/blob/d1d5addccbab45688a6c3797e650a640209b8359/src/processes/photosynthesis/FvCB.jl#L83) in `PlantBiophysics.jl` to see how it works: - -```julia -PlantSimEngine.dep(::Fvcb) = (stomatal_conductance=AbstractStomatal_ConductanceModel,) -``` - -Here we say to PlantSimEngine that the `Fvcb` model needs a model of type `AbstractStomatal_ConductanceModel` in the stomatal conductance process. - -You can read more about dependencies in [Model coupling for modelers](@ref) and [Model coupling for users](@ref). - ### The utility functions Before running a simulation, you can do a little bit more for your implementation (optional). @@ -324,4 +308,21 @@ PlantSimEngine.TimeStepDependencyTrait(::Type{<:Beer}) = PlantSimEngine.IsTimeSt !!! note A model is parallelizable over objects if it does not call another model directly inside its code. Similarly, a model is parallelizable over time-steps if it does not get values from other time-steps directly inside its code. In practice, most of the models are parallelizable one way or another, but it is safer to assume they are not. -OK that's it! Now we have a full new model implementation for the light interception process! I hope it was clear and you understood everything. If you think some sections could be improved, you can make a PR on this doc, or open an issue. \ No newline at end of file +OK that's it! Now we have a full new model implementation for the light interception process! I hope it was clear and you understood everything. If you think some sections could be improved, you can make a PR on this doc, or open an issue. + + +### Dependencies + +If your model explicitly calls another model, you need to tell PlantSimEngine about it. This is called a hard dependency, in opposition to a soft dependency, which is when your model uses a variable from another model, but does not call it explicitly. + +To do so, we can add a method to the `dep` function that tells PlantSimEngine which processes (and models) are needed for the model to run. + +Our example model does not call another model, so we don't need to implement it. But we can look at *e.g.* the implementation for [`Fvcb`](https://github.com/VEZY/PlantBiophysics.jl/blob/d1d5addccbab45688a6c3797e650a640209b8359/src/processes/photosynthesis/FvCB.jl#L83) in `PlantBiophysics.jl` to see how it works: + +```julia +PlantSimEngine.dep(::Fvcb) = (stomatal_conductance=AbstractStomatal_ConductanceModel,) +``` + +Here we say to PlantSimEngine that the `Fvcb` model needs a model of type `AbstractStomatal_ConductanceModel` in the stomatal conductance process. + +You can read more about hard dependencies in [Coupling more complex models](@ref). diff --git a/docs/src/step_by_step/implement_a_process.md b/docs/src/step_by_step/implement_a_process.md index 9e055d69e..28cf15dd4 100644 --- a/docs/src/step_by_step/implement_a_process.md +++ b/docs/src/step_by_step/implement_a_process.md @@ -1,4 +1,4 @@ -# Implement a new process +# Implementing a new process ```@setup usepkg using PlantSimEngine @@ -30,11 +30,11 @@ And that's it! Note that the function guides you in the steps you can make after ## Implement a new model for the process -Once process implementation is done, you can write a corresponding model implementation. The corresponding tutorial page can be found [here](TODO) +Once process implementation is done, you can write a corresponding model implementation. A tutorial page showcasing a light interception model implementation can be found [here](@ref model_implementation_page) A full model implementation for this process is available in the example script [ToyAssimGrowthModel.jl](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/ToyAssimGrowthModel.jl). -## Under the hood +## [Under the hood](@id under_the_hood) The `@process` macro is just a shorthand reducing boilerplate. diff --git a/docs/src/step_by_step/model_switching.md b/docs/src/step_by_step/model_switching.md index 7d07bd232..ca595552a 100644 --- a/docs/src/step_by_step/model_switching.md +++ b/docs/src/step_by_step/model_switching.md @@ -70,7 +70,7 @@ output_initial = run!(models, meteo_day) Now what if we want to switch the model that computes growth ? We can do this by simply replacing the model in the `ModelList`, and PlantSimEngine will automatically update the dependency graph, and adapt the simulation to the new model. -Let's switch `ToyRUEGrowthModel` by `ToyAssimGrowthModel`: +Let's switch `ToyRUEGrowthModel` with `ToyAssimGrowthModel`: ```@example usepkg models2 = ModelList( @@ -97,5 +97,5 @@ And that's it! We can switch between models without changing the code, and witho This was a very standard but easy example. Sometimes other models will require to add other models to the `ModelList`. For example `ToyAssimGrowthModel` could have required a maintenance respiration model. In this case `PlantSimEngine` will tell you that this kind of model is required for the simulation. !!! note - In our example we replaced a soft-dependency model, but the same principle applies to hard-dependency models. Hard and Soft dependencies are concepts explained in TODO + In our example we replaced a soft-dependency model, but the same principle applies to hard-dependency models. Hard and Soft dependencies are concepts explained [here](@ref hard_dependency_def) TODO remove this note ? diff --git a/docs/src/step_by_step/quick_and_dirty_examples.md b/docs/src/step_by_step/quick_and_dirty_examples.md index 353fef47c..d4d07bc6b 100644 --- a/docs/src/step_by_step/quick_and_dirty_examples.md +++ b/docs/src/step_by_step/quick_and_dirty_examples.md @@ -2,16 +2,16 @@ This page is meant for people who have set up their environment and just want to copy-paste an example or two, see what the REPL returns and start tinkering. -If you are less comfortable with Julia, or need to set up an environment first, see this section TODO. -If you wish for a more detailed rundown of the examples, you can instead have a look at the step by step section, which will go into more detail. TODO +If you are less comfortable with Julia, or need to set up an environment first, see this page : [Getting started with Julia](@ref). +If you wish for a more detailed rundown of the examples, you can instead have a look at the [step by step][#step_by_step] section, which will go into more detail. -These examples are all for single-scale simulations. For multi-scale modelling and examples, refer to this section TODO +These examples are all for single-scale simulations. For multi-scale modelling tutorials and examples, refer to [this section][#multiscale] -You can find the implementaiton for these example models, as well as other toy models in TODO +You can find the implementation for these example models, as well as other toy models [in the examples folder](https://github.com/VirtualPlantLab/PlantSimEngine.jl/tree/main/examples). ## Example with a single light interception model and a single weather timestep -```@setup usepkg +```julia using PlantSimEngine, PlantMeteo using PlantSimEngine.Examples meteo = Atmosphere(T = 20.0, Wind = 1.0, Rh = 0.65, Ri_PAR_f = 500.0) @@ -23,7 +23,7 @@ out = run!(leaf, meteo) The weather data in this example contains data over 365 days, meaning the simulation will have as many timesteps. -```@setup usepkg +```julia using PlantSimEngine using PlantMeteo, CSV @@ -43,7 +43,7 @@ outputs_coupled = run!(models, meteo_day) ## Coupling the light interception model with a Leaf Area Index model -```@setup usepkg +```julia using PlantSimEngine using PlantMeteo, CSV diff --git a/docs/src/step_by_step/simple_model_coupling.md b/docs/src/step_by_step/simple_model_coupling.md index 7095ea2aa..c07245ddf 100644 --- a/docs/src/step_by_step/simple_model_coupling.md +++ b/docs/src/step_by_step/simple_model_coupling.md @@ -1,5 +1,17 @@ -# Simple Model coupling +# Standard model coupling +```@setup usepkg +using PlantSimEngine +using PlantSimEngine.Examples +meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) +models = ModelList( + ToyLAIModel(), + Beer(0.5), + ToyRUEGrowthModel(0.2), + status=(TT_cu=cumsum(meteo_day.TT),), +) +nothing +``` ## ModelList The `ModelList` is a container that holds a list of models, their parameter values, and the status of the variables associated to them. @@ -90,7 +102,7 @@ And there you have it. The light interception model made its computations using Of course, one can keep adding models. Here's an example ModelList with another model, `ToyRUEGrowthModel`, which computes the carbon biomass increment caused by photosynthesis. -```@example usepkg +```julia models = ModelList( ToyLAIModel(), Beer(0.5), @@ -99,7 +111,4 @@ models = ModelList( ) nothing # hide -``` - -!!! note - You'll notice a warning returned by `run!` here. If you read its content, you'll see it says that `ToyRUEGrowthModel` does not allow for parallel computations over time-steps. This is because it uses values from the previous time-steps in its computations. By default, `run!` makes the simulations in parallel, so to avoid the warning, you must explicitly tell it to use a sequential execution instead. To do so, you can use the `executor=SequentialEx()` keyword argument. +``` \ No newline at end of file diff --git a/docs/src/troubleshooting_and_testing/downstream_tests.md b/docs/src/troubleshooting_and_testing/downstream_tests.md index 5b3ac00c5..69df26d7e 100644 --- a/docs/src/troubleshooting_and_testing/downstream_tests.md +++ b/docs/src/troubleshooting_and_testing/downstream_tests.md @@ -1,8 +1,8 @@ # Automated tests : downstream dependency checking -PlantSimEngine is open sourced on Github [](TODO), and so are its other companion packages, PlantGeom, PlantMeteo, PlantBioPhysics, MultiScaleTreeGraph, and XPalm. +PlantSimEngine is [open sourced on Github](https://github.com/VirtualPlantLab/PlantSimEngine.jl), and so are its other companion packages, [PlantGeom.jl](https://github.com/VEZY/PlantGeom.jl), [PlantMeteo.jl](https://github.com/VEZY/PlantMeteo.jl), [PlantBioPhysics.jl](https://github.com/VEZY/PlantBioPhysics.jl), [MultiScaleTreeGraph.jl](https://github.com/VEZY/MultiScaleTreeGraph.jl), and [XPalm](https://github.com/PalmStudio/XPalm.jl). -One handy Continuous Integration feature implemented for these packages is automated integration and downstream testing : after changes to a package, its known downstream dependencies are tested to ensure no breaking changes were introduced. For instance, PlantBioPhysics is used in PlantSimEngine, so an integration test ensures that PlantBioPhysics doesn't break in an unforeseen manner after a new PlantSimEngine release. +One handy CI (Continuous Integration) feature implemented for these packages is automated integration and downstream testing : after changes to a package, its known downstream dependencies are tested to ensure no breaking changes were introduced. For instance, PlantBioPhysics is used in PlantSimEngine, so an integration test ensures that PlantBioPhysics doesn't break in an unforeseen manner after a new PlantSimEngine release. This is something you can take advantage of if you wish to develop using PlantSimEngine, by providing us with your package name (or adding it to the CI yml file in a Pull Request) ; we can then add it to the list of downstream packages to test, and generate PR when breaking changes are introduced. diff --git a/docs/src/troubleshooting_and_testing/implicit_contracts.md b/docs/src/troubleshooting_and_testing/implicit_contracts.md index 3035b5705..58867679f 100644 --- a/docs/src/troubleshooting_and_testing/implicit_contracts.md +++ b/docs/src/troubleshooting_and_testing/implicit_contracts.md @@ -10,8 +10,8 @@ In XPalm, weather data for most models is provided daily, meaning biomass calcul Many models are considered to be steady-state over that timeframe, but not all : the leaf pruning model pertubes the plant in a non-steady state fashion, for example. Models that require computations over several iterations to stabilise (often part of hard dependencies) might also have a timestep unrelated to the weather data. -NOTE : TODO -Implicitely, this means any vector variables given as input to the simulation must be consistent with the number of weather timesteps. Providing one weather value but a larger vector variable is an exception : the weather data is replicated over each timestep. (This may be subject to change in the future when support for different timesteps in a single simulation is implemented) +!!! note + Implicitely, this means any vector variables given as input to the simulation must be consistent with the number of weather timesteps. Providing one weather value but a larger vector variable is an exception : the weather data is replicated over each timestep. (This may be subject to change in the future when support for different timesteps in a single simulation is implemented) ## Weather data must be interpolated prior to simulation @@ -55,7 +55,7 @@ Model renaming and duplicating works around this assumption. It may change once This rule avoids potential ambiguity which could then cause both problems in terms of model ordering during the simulation, as well as incorrectly coupling models with the wrong variable. -A workaround for some situations is described here TODO +A workaround for some of the situations where this occurs is described here : [Having a variable simultaneously as input and output of a model](@ref) ## TODO Organs missing in the MTG but declared in the mapping ? diff --git a/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md b/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md index 1a00413af..7f9c2a01a 100644 --- a/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md +++ b/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md @@ -6,6 +6,11 @@ To help people newer to Julia with troubleshooting, here are a few common 'easy- They are listed by 'nature of error', rather than by error message, so you may need to search the page to find your specific error. +If you need more help to decode Julia errors, you can find help on the [Julia Discourse forums](https://discourse.julialang.org). +If you need some advice on the FSPM side, the research community has [its own discourse forum](https://fspm.discourse.group). + +If the issue seems PlantSimEngine-related, or you have questions regarding modeling or have suggestions, you can also [file an issue](https://github.com/VirtualPlantLab/PlantSimEngine.jl/issues) on Github. + ## Tips and workflow Some errors are very specific as to their cause, and the PlantSimEngine errors tend to be explicit about which parameter / variable / organ is causing the error, helping narrow down its origin. @@ -30,9 +35,6 @@ If you wish to search for a specific error in the current page, copy the part of ERROR: MethodError: no method matching ``` -TODO forum, github -TODO - ## Common Julia errors ### NamedTuples with a single value require a comma : diff --git a/docs/src/troubleshooting_and_testing/tips_and_workarounds.md b/docs/src/troubleshooting_and_testing/tips_and_workarounds.md index 7ca33c113..5fb13ae3b 100644 --- a/docs/src/troubleshooting_and_testing/tips_and_workarounds.md +++ b/docs/src/troubleshooting_and_testing/tips_and_workarounds.md @@ -34,8 +34,7 @@ One awkward approach is to rename one of the variables. It is not ideal, of cour In many other situations one can work with what PlantSimEngine already provides. -For example, one model in [XPalm.jl](TODO) handles leaf pruning, affecting biomass. A straightforward implementation would be to have a `leaf_biomass` variable as both input and output. The workaround is to instead output a variable `leaf_biomass_pruning_loss` and to have that as input in the next timestep to compute the new leaf biomass. -TODO example +For example, one model in [XPalm.jl](https://github.com/PalmStudio/XPalm.jl/blob/main/src/plant/phytomer/leaves/leaf_pruning.jl) handles leaf pruning, affecting biomass. A straightforward implementation would be to have a `leaf_biomass` variable as both input and output. The workaround is to instead output a variable `leaf_biomass_pruning_loss` and to have that as input in the next timestep to compute the new leaf biomass. ## Passing in a vector in a mapping status at a specific scale diff --git a/docs/src/working_with_data/visualising_outputs.md b/docs/src/working_with_data/visualising_outputs.md index 760629490..4bab676ae 100644 --- a/docs/src/working_with_data/visualising_outputs.md +++ b/docs/src/working_with_data/visualising_outputs.md @@ -1,3 +1,4 @@ +# Visualizing outputs TODO example environment ? ## Output structure @@ -62,7 +63,8 @@ lines!(ax2, model[:TT_cu], model[:aPPFD], color=:firebrick1) fig ``` -![LAI Growth and light interception](examples/LAI_growth2.png) +TODO +! LAI Growth and light interception ../examples/LAI_growth2.png ## TimeStepTables and DataFrames From f9e2f0eaeb2824d57d46ff8eb0ce39ec8afe8acb Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Wed, 26 Feb 2025 11:20:30 +0100 Subject: [PATCH 062/147] Fixed many jldoctests to conform with API changes, documentation now properly builds locally. A few edits to some doc pages as well (new section in multiscale_coupling), and minor edits. --- docs/make.jl | 8 +-- docs/src/FAQ/translate_a_model.md | 4 +- docs/src/index.md | 8 +-- docs/src/multiscale/multiscale.md | 24 ++------- docs/src/multiscale/multiscale_coupling.md | 54 +++++++++++++++++-- docs/src/prerequisites/design.md | 24 ++++----- .../src/step_by_step/simple_model_coupling.md | 2 + .../working_with_data/visualising_outputs.md | 24 ++++++++- src/checks/dimensions.jl | 2 +- src/component_models/ModelList.jl | 43 ++++----------- src/run.jl | 4 +- 11 files changed, 111 insertions(+), 86 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index d969fc87b..f9e4fd7a8 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,5 +1,5 @@ -using Pkg -Pkg.develop("PlantSimEngine") +#using Pkg +#Pkg.develop("PlantSimEngine") using PlantSimEngine using PlantMeteo using DataFrames, CSV @@ -36,7 +36,7 @@ makedocs(; "Julia language basics" => "./prerequisites/julia_basics.md", "Design" =>"./prerequisites/design.md", # Rework with key concepts ], - "Step by step" => [ + "Step by step - Single Scale simulations" => [ #"First Simulation" => ", "Coupling" => "./step_by_step/simple_model_coupling.md", "Model Switching" => "./step_by_step/model_switching.md", @@ -53,7 +53,7 @@ makedocs(; "Input types" => "./working_with_data/inputs.md", "Visualizing outputs" => "./working_with_data/visualising_outputs.md" ], - "Multiscale" => [ + "Moving to multiscale" => [ "Detailed example" => "./multiscale/multiscale.md", "Handling cyclic dependencies" => "./multiscale/multiscale_cyclic.md", "Multiscale coupling considerations" => "./multiscale/multiscale_coupling.md", # TODO expand upon this diff --git a/docs/src/FAQ/translate_a_model.md b/docs/src/FAQ/translate_a_model.md index 73d834cc8..d5b43fcb2 100644 --- a/docs/src/FAQ/translate_a_model.md +++ b/docs/src/FAQ/translate_a_model.md @@ -153,7 +153,7 @@ m = ModelList( status = (TT_cu = cumsum(meteo_day.TT),), ) -run!(m) +outputs_sim = run!(m) -lines(m[:TT_cu], m[:LAI], color=:green, axis=(ylabel="LAI (m² m⁻²)", xlabel="Days since sowing")) +lines(outputs_sim[:TT_cu], outputs_sim[:LAI], color=:green, axis=(ylabel="LAI (m² m⁻²)", xlabel="Days since sowing")) ``` diff --git a/docs/src/index.md b/docs/src/index.md index bba7e2e5c..28dca6f82 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -108,7 +108,7 @@ Of course you can plot the outputs quite easily: # ] add CairoMakie using CairoMakie -lines(model[:TT_cu], model[:LAI], color=:green, axis=(ylabel="LAI (m² m⁻²)", xlabel="Cumulated growing degree days since sowing (°C)")) +lines(out[:TT_cu], out[:LAI], color=:green, axis=(ylabel="LAI (m² m⁻²)", xlabel="Cumulated growing degree days since sowing (°C)")) ``` ### Model coupling @@ -155,17 +155,17 @@ The `ModelList` couples the models by automatically computing the dependency gra ╰────────────────────────────────────────────────────────────────╯ ``` -We can plot the results by indexing the model with the variable name (e.g. `model2[:LAI]`): +We can plot the results by indexing the outputs with the variable name (e.g. `out2[:LAI]`): ```@example readme using CairoMakie fig = Figure(resolution=(800, 600)) ax = Axis(fig[1, 1], ylabel="LAI (m² m⁻²)") -lines!(ax, model2[:TT_cu], model2[:LAI], color=:mediumseagreen) +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, model2[:TT_cu], model2[:aPPFD], color=:firebrick1) +lines!(ax2, out2[:TT_cu], out2[:aPPFD], color=:firebrick1) fig ``` diff --git a/docs/src/multiscale/multiscale.md b/docs/src/multiscale/multiscale.md index 3c106c17a..a4f6b7caf 100644 --- a/docs/src/multiscale/multiscale.md +++ b/docs/src/multiscale/multiscale.md @@ -195,7 +195,7 @@ outs = Dict( ) ``` -These variables will be available in the `outputs` field of the simulation object, with a value for each time step. +These variables will be available in the output returned by `run!`, with a value for each time step. ### Meteorological data @@ -215,35 +215,19 @@ meteo = Weather( Let's make a simulation using the graph and outputs we just defined: ```@example usepkg -sim = run!(mtg, mapping, meteo, outputs = outs); +outputs_sim = run!(mtg, mapping, meteo, tracked_outputs = outs); nothing # hide ``` -And that's it! - -We can now access the outputs for each scale as a dictionary of vectors of values per variable and scale like this: - -```@example usepkg -outputs(sim); -nothing # hide -``` +And that's it! We can now access the outputs for each scale as a dictionary of vectors of values per variable and scale. Or as a `DataFrame` using the `DataFrames` package: ```@example usepkg using DataFrames -outputs(sim, DataFrame) +outputs(outputs_sim, DataFrame) ``` -The values for the last time-step of the simulation are also available from the statuses: - -```@example usepkg -status(sim); -nothing # hide -``` - -This is a dictionary with the scale as the key and a vector of `Status` as values, one per node of that scale. So, in this example, the `"Leaf"` scale has two nodes, so the value is a vector of two `Status` objects, and the `"Soil"` scale has only one node, so the value is a vector of one `Status` object. - ### Wrapping up In this section, we saw how to define a mapping between models and scales, run a simulation, and access the outputs. diff --git a/docs/src/multiscale/multiscale_coupling.md b/docs/src/multiscale/multiscale_coupling.md index 3b486be61..3edac5911 100644 --- a/docs/src/multiscale/multiscale_coupling.md +++ b/docs/src/multiscale/multiscale_coupling.md @@ -1,13 +1,59 @@ # Handling dependencies in a multiscale context - If a model requires some input variable that is computed at another scale, providing the appropriate mapping will resolve name conflicts and enable proper use of that variable and there will be no extra steps for the user or the modeler. +## Scalar and vector variable mappings - In the case of a hard dependency that operates at a different scale from its parent, the same principle applies and there are also no extra steps on the user-side. +In the detailed example discussed previously (TODO), there were several instances of mapping a variable from one scale to another. Here's a relevant exerpt from the mapping : + +```julia +"Plant" => ( + MultiScaleModel( + model=ToyLAIModel(), + mapping=[ + :TT_cu => "Scene", + ], + ), + ... + MultiScaleModel( + model=ToyCAllocationModel(), + mapping=[ + :carbon_assimilation => ["Leaf"], + :carbon_demand => ["Leaf", "Internode"], + :carbon_allocation => ["Leaf", "Internode"] + ], + ), + ... + ), +``` + +For flexibility reasons, instead of explicitely linking most models from different scales together, one only declares which variables are meant to be taken from another scale (or more accurately, a model at a different scale outputting those variables). This keeps the convenience of switching models while making few changes to the mapping. + +However, PlantSimEngine cannot infer which scales have multiple instances, and which are single-instance, as the scale names are user-defined. + +In the above example, there is only one scene at the "Scene", and one plant at the "Plant" scale, meaning the `TT_cu` variable mapped between the two has a one-to-one scalar-to-scalar correspondance. + +On the other hand, the `carbon_assimilation` variable is computed for **every** leaf, of which there could be hundreds, or thousands, giving a scalar-to-vector correspondance. The carbon assimilation model runs many times every timestep, whereas the carbon allocation model only runs once per timestep. There may be initially be only a single leaf, though, meaning PlantSimEngine cannot currently guess from the initial configuration that there might be multiple leaves created during the simulation. + +Hence the difference in mapping declaration : `TT_cu`is declared as a scalar correspondence : +```julia +:TT_cu => "Scene", +``` +whereas `carbon_assimilation` (and other variables) will be declared as a vector correspondence : +```julia +:carbon_assimilation => ["Leaf"], +``` + +Note that there may be instances where you might wish to write your own model to aggregate a variable from a multi-instance scale. + +## Hard dependencies between models at different scale levels + + If a model requires some input variable that is computed at another scale, then providing the appropriate mapping for that variable will resolve name conflicts and enable that model to run with no further steps for the user or the modeler when the coupling is a 'soft dependency'. + + In the case of a hard dependency that operates at the same scale as its parent, declaring the hard dependency is exactly the same as in single-scale simulations and there are also no new extra steps on the user-side. - On the other hand, modelers need to bear in mind a couple of subtleties when developing models that possess hard dependencies that operate at a different organ level from their parent : + On the other hand, modelers do need to bear in mind a couple of subtleties when developing models that possess hard dependencies that operate at a different organ level from their parent : - The parent model directly handles the call to its hard dependency model(s), meaning they are not explicitely managed by the dependency graph. + The parent model directly handles the call to its hard dependency model(s), meaning they are not explicitely managed by the top-level dependency graph. Therefore only the owning model of that dependency is visible in the graph, and its hard dependency nodes are internal. When the caller (or any downstream model that requires some variables from the hard dependency) operates at the same scale, variables are easily accessible, and no mapping is required. diff --git a/docs/src/prerequisites/design.md b/docs/src/prerequisites/design.md index 5bda62c4d..93fbc544b 100644 --- a/docs/src/prerequisites/design.md +++ b/docs/src/prerequisites/design.md @@ -195,43 +195,37 @@ outputs_example[:aPPFD] ### Outputs TODO -The `status` field of a [`ModelList`](@ref) is used to initialize the variables before simulation and then to keep track of their values during and after the simulation. We can extract the simulation outputs of a model list using the [`status`](@ref) function. +The `status` field of a [`ModelList`](@ref) is used to initialize the variables before simulation and then to keep track of their values during and after the simulation. We can extract outputs of the last timestep of a simulation using the [`status`](@ref) function. -The status is usually stored in a `TimeStepTable` structure from `PlantMeteo.jl`, which is a fast DataFrame-alike structure with each time step being a [`Status`](@ref). It can be also be any `Tables.jl` structure, such as a regular `DataFrame`. The weather is also usually stored in a `TimeStepTable` but with each time step being an `Atmosphere`. +The actual full output data is returned by the `run!` function. Data is usually stored in a `TimeStepTable` structure from `PlantMeteo.jl`, which is a fast DataFrame-alike structure with each time step being a [`Status`](@ref). It can be also be any `Tables.jl` structure, such as a regular `DataFrame`. The weather is also usually stored in a `TimeStepTable` but with each time step being an `Atmosphere`. -Let's look at the status of our previous simulated leaf: +Let's look at the outputs of our previous simulated leaf: ```@setup usepkg -status(leaf) +outputs_example ``` -We can extract the value of one variable using the `status` function, *e.g.* for the intercepted light: +We can extract the value of one variable by indexing into it, *e.g.* for the intercepted light: ```@example usepkg -status(leaf, :aPPFD) +outputs_example[:aPPFD] ``` Or similarly using the dot syntax: ```@example usepkg -leaf.status.aPPFD -``` - -Or much simpler (and recommended), by indexing directly into the model list: - -```@example usepkg -leaf[:aPPFD] +outputs_example.aPPFD ``` Another simple way to get the results is to transform the outputs into a `DataFrame`. Which is very easy because the `TimeStepTable` implements the Tables.jl interface: ```@example usepkg using DataFrames -DataFrame(leaf) +outputs(outputs_example, DataFrame) ``` !!! note - The output from `DataFrame` is adapted to the kind of simulation you did: one row per time-step, and per component models if you simulated several. + The output from this conversion function is adapted to the kind of simulation you did: one row per time-step, and per component models if you simulated several. ## Model coupling diff --git a/docs/src/step_by_step/simple_model_coupling.md b/docs/src/step_by_step/simple_model_coupling.md index c07245ddf..09e800018 100644 --- a/docs/src/step_by_step/simple_model_coupling.md +++ b/docs/src/step_by_step/simple_model_coupling.md @@ -3,6 +3,8 @@ ```@setup usepkg using PlantSimEngine using PlantSimEngine.Examples +using CSV +using DataFrames meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) models = ModelList( ToyLAIModel(), diff --git a/docs/src/working_with_data/visualising_outputs.md b/docs/src/working_with_data/visualising_outputs.md index 4bab676ae..4f6290d9e 100644 --- a/docs/src/working_with_data/visualising_outputs.md +++ b/docs/src/working_with_data/visualising_outputs.md @@ -1,3 +1,25 @@ +```@setup usepkg +# ] add PlantSimEngine, DataFrames, CSV +using PlantSimEngine, PlantMeteo, DataFrames, CSV + +# Include the model definition from the examples folder: +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: +model = 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: +sim_outputs = run!(model, meteo_day) + +``` + # Visualizing outputs TODO example environment ? @@ -76,7 +98,7 @@ Another simple way to get the results is to transform the outputs into a `DataFr ```@example usepkg using DataFrames -outputs(sim_outputs, DataFrames) +PlantSimEngine.outputs(sim_outputs, DataFrame) ``` TODO other examples ? \ No newline at end of file diff --git a/src/checks/dimensions.jl b/src/checks/dimensions.jl index b6da3d148..b8bd22e62 100644 --- a/src/checks/dimensions.jl +++ b/src/checks/dimensions.jl @@ -37,7 +37,7 @@ w = Weather([ PlantSimEngine.check_dimensions(models, w) # output -ERROR: DimensionMismatch: Component status should have the same number of time-steps (2) than weather data (3). +ERROR: DimensionMismatch: Component status has a vector variable : var1 implying multiple timesteps but weather data only provides a single timestep. ``` """ check_dimensions(component, weather) = check_dimensions(DataFormat(weather), component, weather) diff --git a/src/component_models/ModelList.jl b/src/component_models/ModelList.jl index 5769cb4ca..81aa7a1f5 100644 --- a/src/component_models/ModelList.jl +++ b/src/component_models/ModelList.jl @@ -61,7 +61,7 @@ julia> models = ModelList(process1=Process1Model(1.0), process2=Process2Model(), ```jldoctest 1 julia> typeof(models) -ModelList{@NamedTuple{process1::Process1Model, process2::Process2Model, process3::Process3Model}, TimeStepTable{Status{(:var5, :var4, :var6, :var1, :var3, :var2), NTuple{6, Base.RefValue{Float64}}}}, Tuple{}} +ModelList{@NamedTuple{process1::Process1Model, process2::Process2Model, process3::Process3Model}, Status{(:var5, :var4, :var6, :var1, :var3, :var2), NTuple{6, Base.RefValue{Float64}}}} ``` No variables were given as keyword arguments, that means that the status of the ModelList is not @@ -87,11 +87,18 @@ julia> meteo = Atmosphere(T = 22.0, Wind = 0.8333, P = 101.325, Rh = 0.4490995); ``` ```jldoctest 1 -julia> run!(models,meteo) +julia> outputs_sim = run!(models,meteo) +TimeStepTable{Status{(:var5, :var4, :var6, ...}(1 x 6): +╭─────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────╮ +│ Row │ var5 │ var4 │ var6 │ var1 │ var3 │ var2 │ +│ │ Float64 │ Float64 │ Float64 │ Float64 │ Float64 │ Float64 │ +├─────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ 1 │ 36.0139 │ 22.0 │ 58.0139 │ 15.0 │ 5.5 │ 0.3 │ +╰─────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────╯ ``` ```jldoctest 1 -julia> models[:var6] +julia> outputs_sim[:var6] 1-element Vector{Float64}: 58.0138985 ``` @@ -136,36 +143,6 @@ julia> [typeof(models[i][1]) for i in keys(status(models))] Float32 Float32 ``` - -We can also use DataFrame as the status type: - -```jldoctest 1 -julia> using DataFrames; -``` - -```jldoctest 1 -julia> df = DataFrame(:var1 => [13.747, 13.8], :var2 => [1.0, 1.0]); -``` - -```jldoctest 1 -julia> m = ModelList(process1=Process1Model(1.0), process2=Process2Model(), process3=Process3Model(), status=df, init_fun=x -> DataFrame(x)); -``` - -Note that we use `init_fun` to force the status into a `DataFrame`, otherwise it would -be automatically converted into a `TimeStepTable{Status}`. - -```jldoctest 1 -julia> status(m) -2×6 DataFrame - Row │ var5 var4 var6 var1 var3 var2 - │ Float64 Float64 Float64 Float64 Float64 Float64 -─────┼────────────────────────────────────────────────────── - 1 │ -Inf -Inf -Inf 13.747 -Inf 1.0 - 2 │ -Inf -Inf -Inf 13.8 -Inf 1.0 -``` - -Note that computations will be slower using DataFrame, so if performance is an issue, use -TimeStepTable instead (or a NamedTuple as shown in the example). """ struct ModelList{M<:NamedTuple,S} models::M diff --git a/src/run.jl b/src/run.jl index 56c4c90d0..68812d528 100644 --- a/src/run.jl +++ b/src/run.jl @@ -74,13 +74,13 @@ julia> meteo = Atmosphere(T=20.0, Wind=1.0, P=101.3, Rh=0.65, Ri_PAR_f=300.0); Run the simulation: ```jldoctest run -julia> run!(models, meteo); +julia> outputs_sim = run!(models, meteo); ``` Get the results: ```jldoctest run -julia> (models[:var4],models[:var6]) +julia> (outputs_sim[:var4],outputs_sim[:var6]) ([12.0], [41.95]) ``` """ From f24421dbb445b7761a25f120b2712804926d6e24 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Wed, 26 Feb 2025 11:49:35 +0100 Subject: [PATCH 063/147] One failing test on the CI side, might be a setup issue... ? --- test/test-corner-cases.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/test-corner-cases.jl b/test/test-corner-cases.jl index e5b833e3f..eac99a601 100644 --- a/test/test-corner-cases.jl +++ b/test/test-corner-cases.jl @@ -533,8 +533,6 @@ end # Probably very similar to #105 @testset "Issue 111 : Multiscale : outputs not saved when dependency graph only has one depth level" begin - using Pkg - Pkg.develop("PlantSimEngine") using PlantSimEngine using PlantSimEngine.Examples using MultiScaleTreeGraph From 5f108e9cf3f30c1026dbd09999d9e85a794e488a Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Wed, 26 Feb 2025 14:20:06 +0100 Subject: [PATCH 064/147] Deactivating currently unreliable naive MT vs ST check, pending further investigation into sleep() inconsistencies --- test/test-performance.jl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/test-performance.jl b/test/test-performance.jl index 6e9500a99..11b1a676a 100644 --- a/test/test-performance.jl +++ b/test/test-performance.jl @@ -44,10 +44,12 @@ models2 = ModelList(process1=ToySleepModel(), status=(a=vc,)) # Threads sleep/wakeup scheduling overhead causing inconsistencies ? # In any case, sometimes MT beats ST on CI runners, and the mac runner seems to return puzzling false positives - # Deactivating it on mac for non - if !Sys.isapple() - @test abs(nthr * med_time_mt - med_time_seq) < 0.2 * med_time_seq - end + # Deactivating it for now + # TODO there is a thread discussing unreliability of the sleep() function, need to check it + + #if !Sys.isapple() + # @test abs(nthr * med_time_mt - med_time_seq) < 0.2 * med_time_seq + #end # unsure how to recover outputs in benchmarked expressions to compare them, rerun the functions as a workaround for now @test run!(models1, meteo_day; executor = SequentialEx()) == run!(models2, meteo_day; executor = ThreadedEx()) From db5813eede7585d06e5c3f4352568f14a9e88edd Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Wed, 26 Feb 2025 15:22:24 +0100 Subject: [PATCH 065/147] Change the name of the outputs(out, sink ...) functions to convert_outputs. Since they don't take the modellist/graphsim as param now, it's a more accurate name describing what they do. And it avoids many hazardous conflicts that happened when naming the output of a run! 'outputs' (This was less likely to happen when run! returned the graphsimulation object). The other outputs functions used to filter or return outputs are still named as such. --- README.md | 2 +- docs/src/index.md | 2 +- docs/src/multiscale/multiscale.md | 2 +- docs/src/prerequisites/design.md | 2 +- .../working_with_data/visualising_outputs.md | 2 +- src/PlantSimEngine.jl | 2 +- src/mtg/GraphSimulation.jl | 21 ++++++++-------- test/helper-functions.jl | 6 ++--- test/test-ModelList.jl | 24 +++++++++++++++++++ test/test-corner-cases.jl | 4 ++-- test/test-mtg-dynamic.jl | 2 +- test/test-mtg-multiscale-cyclic-dep.jl | 4 ++-- test/test-mtg-multiscale.jl | 2 +- 13 files changed, 50 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index b12ccf89b..e942ffda4 100644 --- a/README.md +++ b/README.md @@ -287,7 +287,7 @@ We can then extract the outputs in a `DataFrame` and sort them: ```@example readme using DataFrames -df_out = outputs(out, DataFrame) +df_out = convert_outputs(out, DataFrame) sort!(df_out, [:timestep, :node]) ``` diff --git a/docs/src/index.md b/docs/src/index.md index 28dca6f82..6b61c7d9a 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -267,7 +267,7 @@ We can then extract the outputs in a `DataFrame` and sort them: ```@example readme using DataFrames -df_out = outputs(out, DataFrame) +df_out = convert_outputs(out, DataFrame) sort!(df_out, [:timestep, :node]) ``` diff --git a/docs/src/multiscale/multiscale.md b/docs/src/multiscale/multiscale.md index a4f6b7caf..b6ef8d0d5 100644 --- a/docs/src/multiscale/multiscale.md +++ b/docs/src/multiscale/multiscale.md @@ -225,7 +225,7 @@ Or as a `DataFrame` using the `DataFrames` package: ```@example usepkg using DataFrames -outputs(outputs_sim, DataFrame) +convert_outputs(outputs_sim, DataFrame) ``` ### Wrapping up diff --git a/docs/src/prerequisites/design.md b/docs/src/prerequisites/design.md index 93fbc544b..a6333e40e 100644 --- a/docs/src/prerequisites/design.md +++ b/docs/src/prerequisites/design.md @@ -221,7 +221,7 @@ Another simple way to get the results is to transform the outputs into a `DataFr ```@example usepkg using DataFrames -outputs(outputs_example, DataFrame) +convert_outputs(outputs_example, DataFrame) ``` !!! note diff --git a/docs/src/working_with_data/visualising_outputs.md b/docs/src/working_with_data/visualising_outputs.md index 4f6290d9e..9ddda0b01 100644 --- a/docs/src/working_with_data/visualising_outputs.md +++ b/docs/src/working_with_data/visualising_outputs.md @@ -98,7 +98,7 @@ Another simple way to get the results is to transform the outputs into a `DataFr ```@example usepkg using DataFrames -PlantSimEngine.outputs(sim_outputs, DataFrame) +PlantSimEngine.convert_outputs(sim_outputs, DataFrame) ``` TODO other examples ? \ No newline at end of file diff --git a/src/PlantSimEngine.jl b/src/PlantSimEngine.jl index 1c195d7cd..8443f020f 100644 --- a/src/PlantSimEngine.jl +++ b/src/PlantSimEngine.jl @@ -107,7 +107,7 @@ export init_status! export add_organ! export @process, process export to_initialize, is_initialized, init_variables, dep -export inputs, outputs, variables +export inputs, outputs, variables, convert_outputs export run! export fit diff --git a/src/mtg/GraphSimulation.jl b/src/mtg/GraphSimulation.jl index 7ce16ca08..8049d76b3 100644 --- a/src/mtg/GraphSimulation.jl +++ b/src/mtg/GraphSimulation.jl @@ -40,18 +40,19 @@ get_models(g::GraphSimulation) = g.models outputs(g::GraphSimulation) = g.outputs """ - outputs(sim::GraphSimulation, sink) + convert_outputs(sim_outputs::Dict{String,O} where O, sink; refvectors=false, no_value=nothing) + convert_outputs(sim_outputs::TimeStepTable{T} where T, sink) -Get the outputs from a simulation made on a plant graph. +Convert the outputs returned by a simulation made on a plant graph into another format. # Details -The first method returns a vector of `NamedTuple`, the second formats it -sing the sink function, for exemple a `DataFrame`. +The first method operates on the outputs of a multiscale simulation, the second one on those of a typical single-scale simulation. +The sink function determines the format used, for exemple a `DataFrame`. # Arguments -- `sim::GraphSimulation`: the simulation object, typically returned by `run!`. +- `sim_outputs : the outputs of a prior simulation, typically returned by `run!`. - `sink`: a sink compatible with the Tables.jl interface (*e.g.* a `DataFrame`) - `refvectors`: if `false` (default), the function will remove the RefVector values, otherwise it will keep them - `no_value`: the value to replace `nothing` values. Default is `nothing`. Usually used to replace `nothing` values @@ -76,7 +77,7 @@ mtg = import_mtg_example(); ``` ```@example -sim = run!(mtg, mapping, meteo, outputs = Dict( +out = run!(mtg, mapping, meteo, tracked_outputs = Dict( "Leaf" => (:carbon_assimilation, :carbon_demand, :soil_water_content, :carbon_allocation), "Internode" => (:carbon_allocation,), "Plant" => (:carbon_allocation,), @@ -85,10 +86,10 @@ sim = run!(mtg, mapping, meteo, outputs = Dict( ``` ```@example -outputs(sim, DataFrames) +convert_outputs(out, DataFrames) ``` """ -function outputs(outs::Dict{String,O} where O, sink; refvectors=false, no_value=nothing) +function convert_outputs(outs::Dict{String,O} where O, sink; refvectors=false, no_value=nothing) @assert Tables.istable(sink) "The sink argument must be compatible with the Tables.jl interface (`Tables.istable(sink)` must return `true`, *e.g.* `DataFrame`)" @@ -156,11 +157,11 @@ function outputs(outs::Dict{String,O} where O, sink; refvectors=false, no_value= end function outputs(outs::Dict{String, O} where O, key::Symbol) - Tables.columns(outputs(outs, Vector{NamedTuple}))[key] + Tables.columns(convert_outputs(outs, Vector{NamedTuple}))[key] end function outputs(outs::Dict{String, O} where O, i::T) where {T<:Integer} - Tables.columns(outputs(outs, Vector{NamedTuple}))[i] + Tables.columns(convert_outputs(outs, Vector{NamedTuple}))[i] end # ModelLists now return outputs as a TimeStepTable{Status}, conversion is straightforward diff --git a/test/helper-functions.jl b/test/helper-functions.jl index e61228e23..ff598825e 100644 --- a/test/helper-functions.jl +++ b/test/helper-functions.jl @@ -1,7 +1,7 @@ # Simple helper functions that can be used in various tests here and there function compare_outputs_modellist_mapping(filtered_outputs, graphsim) - outputs_df = outputs(graphsim.outputs, DataFrame) + outputs_df = convert_outputs(graphsim.outputs, DataFrame) outputs_df_outputs_only = select(outputs_df, Not([:timestep, :organ, :node])) models_df = DataFrame(filtered_outputs) @@ -13,10 +13,10 @@ end # doesn't check for mtg equality function compare_outputs_graphsim(graphsim, graphsim2) - outputs_df = outputs(graphsim.outputs, DataFrame) + outputs_df = convert_outputs(graphsim.outputs, DataFrame) outputs_df_sorted = outputs_df[:, sortperm(names(outputs_df))] - outputs2_df = outputs(graphsim2.outputs, DataFrame) + outputs2_df = convert_outputs(graphsim2.outputs, DataFrame) outputs2_df_sorted = outputs2_df[:, sortperm(names(outputs2_df))] return outputs_df_sorted == outputs2_df_sorted end diff --git a/test/test-ModelList.jl b/test/test-ModelList.jl index 417f12364..d5a60f898 100644 --- a/test/test-ModelList.jl +++ b/test/test-ModelList.jl @@ -293,4 +293,28 @@ end=# #mtg, mapping, outputs_mapping, nsteps, filtered_outputs_modellist = test_filtered_output_begin(modellists[1], status_tuples[1], outs_vectors[1][1], meteos[1]) #@test test_filtered_output(mtg, mapping, nsteps, outputs_mapping, meteo_day, filtered_outputs_modellist) +end + + +PlantSimEngine.@process "modellist_cycle" verbose = false + +struct Reeb{T} <: AbstractModellist_CycleModel + k::T +end + +function PlantSimEngine.run!(::Reeb, models, status, meteo, constants, extra=nothing) + status.LAI = + status.aPPFD + 0.4*k +end + +function PlantSimEngine.inputs_(::Reeb) + (aPPFD=-Inf,) +end + +function PlantSimEngine.outputs_(::Reeb) + (LAI=-Inf,) +end + +@testset "ModelList simple cyclic dependency detection" begin + @test_throws "Cyclic" m = ModelList(Beer(0.5), Reeb(0.5)) end \ No newline at end of file diff --git a/test/test-corner-cases.jl b/test/test-corner-cases.jl index eac99a601..893dc9383 100644 --- a/test/test-corner-cases.jl +++ b/test/test-corner-cases.jl @@ -522,7 +522,7 @@ end ) vars = Dict{String,Any}("Leaf" => (:var1,)) out = run!(mtg, m, Atmosphere(T=20.0, Wind=1.0, Rh=0.65), tracked_outputs=vars, executor=SequentialEx()) - df = outputs(out, DataFrame) + df = convert_outputs(out, DataFrame) @test DataFrames.nrow(df) == 2 end @@ -555,7 +555,7 @@ end sim = run!(mtg, mapping, meteo; tracked_outputs=outs) using DataFrames - df = outputs(sim, DataFrame) + df = convert_outputs(sim, DataFrame) @test DataFrames.nrow(df) == PlantSimEngine.get_nsteps(meteo) end diff --git a/test/test-mtg-dynamic.jl b/test/test-mtg-dynamic.jl index 41f38e0d6..11c30ee93 100644 --- a/test/test-mtg-dynamic.jl +++ b/test/test-mtg-dynamic.jl @@ -82,7 +82,7 @@ out = run!(sim,meteo) @test st["Internode"][1].TT_cu_emergence == 0.0 @test st["Internode"][end].TT_cu_emergence == 25.0 - out_df = outputs(out, DataFrame) + out_df = convert_outputs(out, DataFrame) @test unique(out_df[:, :organ]) |> sort == ["Internode", "Leaf", "Plant", "Soil"] @test filter(row -> row.organ == "Internode", out_df)[:, :TT_cu_emergence] == [0.0, 0.0, 0.0, 0.0, 25.0] @test filter(row -> row.organ == "Leaf", out_df)[:, :carbon_demand] == [0.5, 0.5, 0.75, 0.75, 0.75] diff --git a/test/test-mtg-multiscale-cyclic-dep.jl b/test/test-mtg-multiscale-cyclic-dep.jl index c6bbe9dac..6d9e99ffb 100644 --- a/test/test-mtg-multiscale-cyclic-dep.jl +++ b/test/test-mtg-multiscale-cyclic-dep.jl @@ -224,7 +224,7 @@ end # To update the reference: ref_path = joinpath(pkgdir(PlantSimEngine), "test/references/ref_output_simulation.csv") - # CSV.write(ref_path, sort(outputs(out, DataFrame, no_value=missing), [:timestep, :node]), transform=(col, val) -> something(val, missing)) + # CSV.write(ref_path, sort(convert_outputs(out, DataFrame, no_value=missing), [:timestep, :node]), transform=(col, val) -> something(val, missing)) ref_df = CSV.read(ref_path, DataFrame) - @test isequal(sort(outputs(out, DataFrame, no_value=missing), [:timestep, :node]), ref_df) + @test isequal(sort(convert_outputs(out, DataFrame, no_value=missing), [:timestep, :node]), ref_df) end \ No newline at end of file diff --git a/test/test-mtg-multiscale.jl b/test/test-mtg-multiscale.jl index 1854b1da2..0bbf0d537 100644 --- a/test/test-mtg-multiscale.jl +++ b/test/test-mtg-multiscale.jl @@ -697,7 +697,7 @@ end @test sim.outputs["Plant"][:carbon_allocation][1][1][1] === sim.outputs["Internode"][:carbon_allocation][1][1] # Testing the outputs if transformed into a DataFrame: - outs = outputs(out, DataFrame) + outs = convert_outputs(out, DataFrame) @test isa(outs, DataFrame) @test size(outs) == (12, 7) From 5c2331fb843453ad9b9b957a10c4d1665770aec4 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Wed, 26 Feb 2025 16:02:27 +0100 Subject: [PATCH 066/147] Split XPalm downstream benchmark into setup, run and outputs conversion phases --- test/downstream/test-all-benchmarks.jl | 8 +++++++- test/downstream/test-xpalm.jl | 18 +++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/test/downstream/test-all-benchmarks.jl b/test/downstream/test-all-benchmarks.jl index 1bbfc5155..db9436fde 100644 --- a/test/downstream/test-all-benchmarks.jl +++ b/test/downstream/test-all-benchmarks.jl @@ -38,7 +38,13 @@ suite[suite_name]["PBP_multiple_timesteps_ST"] = @benchmarkable benchmark_plantb # "XPalm benchmark" include("test-xpalm.jl") -suite[suite_name]["XPalm"] = @benchmarkable xpalm_default_param_run() seconds = 120 +suite[suite_name]["XPalm_setup"] = @benchmarkable xpalm_default_param_create() seconds = 120 + +palm, models, out_vars, meteo = xpalm_default_param_create() +sim_outputs = xpalm_default_param_run(palm, models, out_vars, meteo) + +suite[suite_name]["XPalm_run"] = @benchmarkable xpalm_default_param_run($palm, $models, $out_vars, $meteo) seconds = 120 +suite[suite_name]["XPalm_convert_outputs"] = @benchmarkable xpalm_default_param_convert_outputs($sim_outputs) seconds = 120 tune!(suite) results = run(suite, verbose=true) diff --git a/test/downstream/test-xpalm.jl b/test/downstream/test-xpalm.jl index e03fc0059..c2708374d 100644 --- a/test/downstream/test-xpalm.jl +++ b/test/downstream/test-xpalm.jl @@ -14,7 +14,7 @@ using Dates using XPalm using BenchmarkTools -function xpalm_default_param_run() +function xpalm_default_param_create() meteo = CSV.read("../XPalm.jl/0-data/Meteo_Nigeria_PR.txt", DataFrame) meteo.duration = [Dates.Day(i[1:1]) for i in meteo.duration] m = Weather(meteo) @@ -32,10 +32,22 @@ function xpalm_default_param_run() ) # Example 1: Run the model with the default parameters (but output as a DataFrame): - #df = xpalm(m; vars=out_vars, sink=DataFrame) - df = XPalm.xpalm(m, DataFrame; vars=out_vars) + palm = Palm(initiation_age=0, parameters=default_parameters()) + models = model_mapping(palm) + return palm, models, out_vars, meteo end +function xpalm_default_param_run(palm, models, meteo, out_vars) + sim_outputs = PlantSimEngine.run!(palm.mtg, models, meteo, tracked_outputs=out_vars, executor=PlantSimEngine.SequentialEx(), check=false) + return sim_outputs +end + +function xpalm_default_param_convert_outputs(sim_outputs) + df = PlantSimEngine.convert_outputs(out, DataFrame, no_value=missing) + return df +end + + #=@testset "XPalm simple test" begin # default number of seconds is 5 b_XP = @benchmark xpalm_default_param_run() seconds = 120 From 877dbe6c147061c886aca2da4e93a627ed6bbf4b Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Wed, 26 Feb 2025 16:14:31 +0100 Subject: [PATCH 067/147] Oversight, forgot to rename one outputs function --- src/mtg/GraphSimulation.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mtg/GraphSimulation.jl b/src/mtg/GraphSimulation.jl index 8049d76b3..9f243aeb3 100644 --- a/src/mtg/GraphSimulation.jl +++ b/src/mtg/GraphSimulation.jl @@ -165,7 +165,7 @@ function outputs(outs::Dict{String, O} where O, i::T) where {T<:Integer} end # ModelLists now return outputs as a TimeStepTable{Status}, conversion is straightforward -function outputs(out::TimeStepTable{T} where T, sink) +function convert_outputs(out::TimeStepTable{T} where T, sink) @assert Tables.istable(sink) "The sink argument must be compatible with the Tables.jl interface (`Tables.istable(sink)` must return `true`, *e.g.* `DataFrame`)" return sink(out) end \ No newline at end of file From 9f519f5ad29fbeb457020a315781ca582c216090 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Wed, 26 Feb 2025 17:38:51 +0100 Subject: [PATCH 068/147] Split API into two pages, minor edits --- docs/make.jl | 4 +++- docs/src/API/API_private.md | 20 ++++++++++++++++++++ docs/src/{API.md => API/API_public.md} | 13 ++----------- docs/src/step_by_step/implement_a_model.md | 2 +- 4 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 docs/src/API/API_private.md rename docs/src/{API.md => API/API_public.md} (64%) diff --git a/docs/make.jl b/docs/make.jl index f9e4fd7a8..ff9653f0d 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -69,7 +69,9 @@ makedocs(; "Tips and Workarounds" => "./troubleshooting_and_testing/tips_and_workarounds.md", ], - "API" => "API.md", + "API" => [ + "Public API" => "./API/API_public.md", + "Internal API" => "./API/API_private.md",], "Credits" => "credits.md", "Planned features" => "planned_features.md", #"developer section ?" diff --git a/docs/src/API/API_private.md b/docs/src/API/API_private.md new file mode 100644 index 000000000..0d0f9fae7 --- /dev/null +++ b/docs/src/API/API_private.md @@ -0,0 +1,20 @@ +# API - internal functions +## Un-exported + +Private functions, types or constants from `PlantSimEngine`. These are not exported, so you need to use `PlantSimEngine.` to access them (*e.g.* `PlantSimEngine.DataFormat`). Most of them are developer code, but some may be useful for tinkerers, or to have greater control over some simulation parameters (future versions of this documentation might break those categories into separate pages for clarity). + +## Index + +```@index +Modules = [PlantSimEngine] +Public = false +Private = true +``` + +## API documentation + +```@autodocs +Modules = [PlantSimEngine] +Public = false +Private = true +``` diff --git a/docs/src/API.md b/docs/src/API/API_public.md similarity index 64% rename from docs/src/API.md rename to docs/src/API/API_public.md index 26ffca2bf..7716289c6 100644 --- a/docs/src/API.md +++ b/docs/src/API/API_public.md @@ -1,9 +1,10 @@ -# API +# Public API ## Index ```@index Modules = [PlantSimEngine] +Private = false ``` ## API documentation @@ -13,16 +14,6 @@ Modules = [PlantSimEngine] Private = false ``` -## Un-exported - -Private functions, types or constants from `PlantSimEngine`. These are not exported, so you need to use `PlantSimEngine.` to access them (*e.g.* `PlantSimEngine.DataFormat`). - -```@autodocs -Modules = [PlantSimEngine] -Public = false -Private = true -``` - ## Example models PlantSimEngine provides example processes and models to users. They are available from a sub-module called `Examples`. To get access to these models, you can simply use this sub-module: diff --git a/docs/src/step_by_step/implement_a_model.md b/docs/src/step_by_step/implement_a_model.md index 31c13de77..a44916b1e 100644 --- a/docs/src/step_by_step/implement_a_model.md +++ b/docs/src/step_by_step/implement_a_model.md @@ -150,7 +150,7 @@ end Parameterized types are very useful because they let the user choose the type of the parameters, and potentially dispatch on them. -But why not forcing the type such as the following: +But why not force the type ? Such as in the following example : ```julia struct YourStruct <: AbstractLight_InterceptionModel From 5cd5338acb4e95c7aaa5b0dc4c7b42938d49f256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Vezy?= Date: Fri, 28 Feb 2025 14:30:56 +0100 Subject: [PATCH 069/147] Update make.jl Add doc preview for the pull request --- docs/make.jl | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index ff9653f0d..9030bd4d2 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -19,9 +19,7 @@ makedocs(; edit_link="main", assets=String[], size_threshold=300000 - ), - - pages=[ + ), pages=[ "Home" => "index.md", "Introduction" => [ #"Organization of the documentation ?" @@ -34,16 +32,16 @@ makedocs(; "Key Concepts" => "./prerequisites/key_concepts.md", # Key concepts vs terminology ? #"Setup" ?", "Julia language basics" => "./prerequisites/julia_basics.md", - "Design" =>"./prerequisites/design.md", # Rework with key concepts + "Design" => "./prerequisites/design.md", # Rework with key concepts ], "Step by step - Single Scale simulations" => [ - #"First Simulation" => ", - "Coupling" => "./step_by_step/simple_model_coupling.md", - "Model Switching" => "./step_by_step/model_switching.md", - "Processes" => "./step_by_step/implement_a_process.md", - "Implementing a model" => "./step_by_step/implement_a_model.md", - "Parallelization" => "./step_by_step/parallelization.md", - "Advanced coupling and hard dependencies" => "./step_by_step/advanced_coupling.md" + #"First Simulation" => ", + "Coupling" => "./step_by_step/simple_model_coupling.md", + "Model Switching" => "./step_by_step/model_switching.md", + "Processes" => "./step_by_step/implement_a_process.md", + "Implementing a model" => "./step_by_step/implement_a_model.md", + "Parallelization" => "./step_by_step/parallelization.md", + "Advanced coupling and hard dependencies" => "./step_by_step/advanced_coupling.md" ], "Execution" => "model_execution.md", "Working with data" => [ @@ -61,15 +59,11 @@ makedocs(; "A rudimentary plant simulation" => "./multiscale/multiscale_example_1.md", "Expanding the plant simulation" => "./multiscale/multiscale_example_2.md", ], - ], - - "Troubleshooting and testing" => [ - "Troubleshooting" => "./troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md", - "Automated testing" => "./troubleshooting_and_testing/downstream_tests.md", - "Tips and Workarounds" => "./troubleshooting_and_testing/tips_and_workarounds.md", - ], - - "API" => [ + ], "Troubleshooting and testing" => [ + "Troubleshooting" => "./troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md", + "Automated testing" => "./troubleshooting_and_testing/downstream_tests.md", + "Tips and Workarounds" => "./troubleshooting_and_testing/tips_and_workarounds.md", + ], "API" => [ "Public API" => "./API/API_public.md", "Internal API" => "./API/API_private.md",], "Credits" => "credits.md", @@ -80,5 +74,6 @@ makedocs(; deploydocs(; repo="github.com/VirtualPlantLab/PlantSimEngine.jl.git", - devbranch="main" + devbranch="main", + push_preview=true, # Visit https://VirtualPlantLab.github.io/PlantSimEngine.jl/previews/PR128 to visualize the preview of the PR #128 ) From 36f209d03a8c7a1460d3556da1c837d62430b19e Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Fri, 28 Feb 2025 17:19:07 +0100 Subject: [PATCH 070/147] Documentation : add a third part to the Toy Plant multiscale example fixing implementation issues in the second part --- docs/make.jl | 1 + docs/src/FAQ/translate_a_model.md | 2 - docs/src/multiscale/multiscale_example_1.md | 6 +- docs/src/multiscale/multiscale_example_2.md | 6 +- docs/src/multiscale/multiscale_example_3.md | 182 +++++++++++++ docs/src/working_with_data/inputs.md | 13 +- .../ToyPlantSimulation3.jl | 254 ++++++++++++++++++ 7 files changed, 456 insertions(+), 8 deletions(-) create mode 100644 docs/src/multiscale/multiscale_example_3.md create mode 100644 examples/ToyMultiScalePlantTutorial/ToyPlantSimulation3.jl diff --git a/docs/make.jl b/docs/make.jl index 9030bd4d2..76810b92d 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -58,6 +58,7 @@ makedocs(; "Building a simple plant" => [ "A rudimentary plant simulation" => "./multiscale/multiscale_example_1.md", "Expanding the plant simulation" => "./multiscale/multiscale_example_2.md", + "Fixing bugs in the plant simulation"=> "./multiscale/multiscale_example_3.md", # TODO illustrate outputs filtering to find the bug ], ], "Troubleshooting and testing" => [ "Troubleshooting" => "./troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md", diff --git a/docs/src/FAQ/translate_a_model.md b/docs/src/FAQ/translate_a_model.md index d5b43fcb2..92f18fed8 100644 --- a/docs/src/FAQ/translate_a_model.md +++ b/docs/src/FAQ/translate_a_model.md @@ -16,8 +16,6 @@ function lai_toymodel(TT_cu; max_lai=8.0, dd_incslope=500, inc_slope=70, dd_decs end meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) -# Note: meteo_day is defined below if you want to reproduce it, then use this to write it: -# PlantMeteo.write_weather("examples/meteo_day.csv", meteo_day, duration = Dates.Day) ``` If you already have a model, you can easily use `PlantSimEngine` to couple it with other models with minor adjustments. diff --git a/docs/src/multiscale/multiscale_example_1.md b/docs/src/multiscale/multiscale_example_1.md index f678b5ac5..e4c4878ed 100644 --- a/docs/src/multiscale/multiscale_example_1.md +++ b/docs/src/multiscale/multiscale_example_1.md @@ -173,7 +173,7 @@ function PlantSimEngine.run!(m::ToyCustomInternodeEmergence, models, status, met end ``` -## Updated mapping +### Updated mapping We can now define the final mapping for this simulation. @@ -249,14 +249,14 @@ And we're good to go ! outs = run!(mtg, mapping, meteo_day) ``` -And that's it. If you query or display the MTG after simulation, you'll see it expanded and grew multiple internodes and leaves : +If you query or display the MTG after simulation, you'll see it expanded and grew multiple internodes and leaves : ```julia mtg get_n_leaves(mtg) ``` -Feel free to tinker with the parameters and see when things break down, to get a feel for the simulation. +And that's it ! Feel free to tinker with the parameters and see when things break down, to get a feel for the simulation. Of course, this is a very crude and unrealistic simulation, with many dubious assumptions and parameters. But significantly more complex modelling is possible using the same approach : XPalm runs using a few dozen models spread out over nine scales. diff --git a/docs/src/multiscale/multiscale_example_2.md b/docs/src/multiscale/multiscale_example_2.md index 78f03e530..51362ccce 100644 --- a/docs/src/multiscale/multiscale_example_2.md +++ b/docs/src/multiscale/multiscale_example_2.md @@ -220,6 +220,8 @@ meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), outs = run!(mtg, mapping, meteo_day) ``` -And that's it ! We now have a plant with two different growth directions. Roots are added at the beginning, until water is considered abundant enough. +And that's it ! -Of course, there are several design issues with this implementation. It is as utterly unrealistic as the previous one, and doesn't even consume water. \ No newline at end of file +...Or is it ? + +If you inspect the code and output data closely, you may notice some distinctive problems with the way the simulation runs... Some things aren't quite right. If you wish to know more, onwards to the next chapter : TODO \ No newline at end of file diff --git a/docs/src/multiscale/multiscale_example_3.md b/docs/src/multiscale/multiscale_example_3.md new file mode 100644 index 000000000..7efde2d65 --- /dev/null +++ b/docs/src/multiscale/multiscale_example_3.md @@ -0,0 +1,182 @@ +# Fixing bugs in the plant simulation + +There are two major issues hinted at in last chapter's implementation, which we'll discuss and resolve here. + +You can find the full script for this simulation in the [ToyMultiScalePlantModel](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/ToyMultiScalePlantModel/ToyPlantSimulation3.jl) subfolder of the examples folder. + +## Delaying organ maturity + +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. + +This is an implementation decision in PlantSimEngine. By default, new organs are active, and models can affect them as soon as they are created. + +The internode growth also depends on a threshold thermal time value, so doesn't immediately expand within a single timestep. XPalm's organ emission models TODO also + +!!! Note + This may be subject to change in future versions of PlantSimEngine. Also note that the way the dependency graph is structured determines the order in which models run. Meaning that which models are run before or after organ creation might change with new additions and updates to your mapping. Some models might run "one timestep later". + +How do we avoid this extreme instant growth ? We can, of course, add some thermal time constraint. We could arbitrarily tinker with water resources. + +We can otherwise add a simple state machine variable to our root and internodes in the MTG, indicating a newly added organ is immature and cannot grow on the same timestep. Since our root doesn't branch, we can simply keep track of a single state variable. + +In fact, we could change the scale at which the check is made to extend the root, and have another model call this one directly. This enables running this model only for the end root when those occasional timesteps when root growth is possible, instead of at every timestep for every root node. + +You can find several similar patterns in XPalm TODO. + +## Fixing resource computation + +Another problem you may have noticed, is that the water and carbon stock are computed by aggregating photosynthesis over leaves and absorption over roots... But they aren't always properly decremented when consumed ! + +If the end root grows, it outputs a carbon_root_creation_consumed value, but under certain conditions, we might also create other roots and internodes even when there shouldn't be enough carbon left for them. + +Indeed, if both the root and leaf water thresholds are met, and there is enough carbon for a single root or internode but not for both, and the root model runs before the internode model, both will use the carbon_stock variable prior to organ emission. The internode emission model won't account for the root carbon consumption. + +This occurs because carbon_stock is only computed once, and won't update until the next timestep. + +What we can do to avoid that problem in our specific case (for other situations TODO), is to couple the root growth model and the internode emission model, and pass the carbon_root_creation_consumed so that internode emission can take it into account. Or we could have an intermediate model recompute the new stock to pass along to the internode emission model. + +We'll go for the first option. + +TODO previous timestep ? + +### Internode emission adjustments + +The only change required for our internode emission model is to take into account `carbon_root_creation_consumed` as a new input, map that variable from the "Root" scale in our mapping, and compute the adjusted carbon stock. Here's the relevant excerpt in the `run!` function. + +```julia + # take into account that the stock may already be depleted + carbon_stock_updated_after_roots = status.carbon_stock - status.carbon_root_creation_consumed + + # if not enough carbon, no organ creation + if carbon_stock_updated_after_roots < m.carbon_internode_creation_cost + return nothing + end +``` + +### Root growth decision model with a hard dependency + +Our root growth decision model inherits some of the responsibility from last chapter's root growth model, so inputs, parameters and condition checks will be similar. We'll let the root growth model keep the length check and only focus on resources. + +```julia +PlantSimEngine.@process "root_growth_decision" verbose = false + +struct ToyRootGrowthDecisionModel <: AbstractRoot_Growth_DecisionModel + water_threshold::Float64 + carbon_root_creation_cost::Float64 +end + +PlantSimEngine.inputs_(::ToyRootGrowthDecisionModel) = +(water_stock=0.0,carbon_stock=0.0) + +PlantSimEngine.outputs_(::ToyRootGrowthDecisionModel) = NamedTuple() + +PlantSimEngine.dep(::ToyRootGrowthDecisionModel) = (root_growth=AbstractRoot_GrowthModel=>["Root"],) + +function PlantSimEngine.run!(m::ToyRootGrowthDecisionModel, models, status, meteo, constants=nothing, extra=nothing) + + if status.water_stock < m.water_threshold && status.carbon_stock > m.carbon_root_creation_cost + status_Root= extra_args.statuses["Root"][1] + PlantSimEngine.run!(extra.models["Root"].root_growth, models, status_Root, meteo, constants, extra) + end +end +``` + +Note the hard dependency declaration, and the direct call to the root growth `run!` function. The root growth model will output the `carbon_root_creation_consumed` computation, but it'll still be exposed to downstream models despite the root growth model being an 'hidden' model since it's a hard dependency. + +### Root growth + +This version is a simplifed version of last chapter's. + +```julia +PlantSimEngine.@process "root_growth" verbose = false + +struct ToyRootGrowthModel <: AbstractRoot_GrowthModel + root_max_len::Int +end + +PlantSimEngine.inputs_(::ToyRootGrowthModel) = NamedTuple() +PlantSimEngine.outputs_(::ToyRootGrowthModel) = (carbon_root_creation_consumed=0.0,) + +function PlantSimEngine.run!(m::ToyRootGrowthModel, models, status, meteo, constants=nothing, extra=nothing) + status.carbon_root_creation_consumed = 0.0 + + root_end = get_root_end_node(status.node) + + if length(root_end) != 1 + throw(AssertionError("Couldn't find MTG leaf node with symbol \"Root\"")) + end + + root_len = get_roots_count(root_end[1]) + if root_len < m.root_max_len + st = add_organ!(root_end[1], extra, "<", "Root", 2, index=1) + status.carbon_root_creation_consumed = m.carbon_root_creation_cost + end +end +``` + +TODO state machine + +### Mapping adjustments + +The new mapping only has straightforward changes. Some models cease to be multi-scale, others require new variables to be mapped for them. `carbon_root_creation_consumed` ceases to be a vector mapping and is a scalar variable. + +```julia +mapping = Dict( +"Scene" => ToyDegreeDaysCumulModel(), +"Plant" => ( + MultiScaleModel( + model=ToyStockComputationModel(), + mapping=[ + :carbon_captured=>["Leaf"], + :water_absorbed=>["Root"], + :carbon_root_creation_consumed=>"Root", + :carbon_organ_creation_consumed=>["Internode"] + + ], + ), + MultiScaleModel( + model=ToyRootGrowthDecisionModel(10.0, 50.0), + ), + Status(water_stock = 0.0, carbon_stock = 0.0) + ), +"Internode" => ( + MultiScaleModel( + model=ToyCustomInternodeEmergence(),#TT_emergence=20.0), + mapping=[:TT_cu => "Scene", + :water_stock=>"Plant", + :carbon_stock=>"Plant", + :carbon_root_creation_consumed=>"Root"], + ), + Status(carbon_organ_creation_consumed=0.0), + ), +"Root" => (ToyRootGrowthModel(10), + ToyWaterAbsorptionModel(), + Status(carbon_root_creation_consumed=0.0, root_water_assimilation=1.0), + ), +"Leaf" => ( ToyLeafCarbonCaptureModel(),), +) +``` + +We can now run our simulation as we did previously... or can we ? + +```julia +ERROR: Cyclic dependency detected for process resource_stock_computation: resource_stock_computation for organ Plant depends on root_growth from organ Root, which depends on the first one. This is not allowed, you may need to develop a new process that does the whole computation by itself. +``` + +Ah, it looks like our additional usage of the root carbon cost creates a cyclic dependency. + +### Breaking the dependency cycle + +Fortunately, the logic here is quite straightforward. We can't be computing our current timestep's resource stock with `carbon_root_creation_consumed`, and then updating it right after root creation again using a new value of `carbon_root_creation_consumed`. + +The solution is hopefully quite intuitive : when we compute resource stocks, we should be computing it using the previous timestep's values. Then root creation happens (or doesn't), and the computed `carbon_root_creation_consumed` corresponds to the current timestep value. We could also do the same for water to be consistent. + +## Final words + +The full script for this simulation can be found [here](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/ToyMultiScalePlantModel/ToyPlantSimulation3.jl), in the ToyMultiScalePlantModel subfolder of the examples folder. + +We now have a plant with two different growth directions. Roots are added at the beginning, until water is considered abundant enough. + +Of course, there are still several design issues with this implementation. It is as utterly unrealistic as the previous one, and doesn't even consume water. Some condition checking is a little ad hoc and could be made more robust. More sanity checks could be added, and the model and variable names could definitely be made more clear. + +But once again, this example is only made to illustrate what is possible with this framework, and doesn't strive for ecophysiological consistency. And the approach can be made increasingly more complex by refining models and simulation parameters, and feeding in new information about your plant, and ramp up to realistic, production-ready and predictive simulations. \ No newline at end of file diff --git a/docs/src/working_with_data/inputs.md b/docs/src/working_with_data/inputs.md index 87716770e..4e5b4d004 100644 --- a/docs/src/working_with_data/inputs.md +++ b/docs/src/working_with_data/inputs.md @@ -30,4 +30,15 @@ To do so, you need to implement the following methods for your structure that de - (Optionnally) `PlantMeteo.row_from_parent(row, i)`: return row `i` from the parent table, *e.g.* the row `i` from the DataFrame. This is only needed if you want high performance, the default implementation calls `Tables.rows(parent(row))[i]`. !!! compat - `PlantMeteo.rownumber` is temporary. It soon will be replaced by `DataAPI.rownumber` instead, which will be also used by *e.g.* DataFrames.jl. See [this Pull Request](https://github.com/JuliaData/DataAPI.jl/issues/60). \ No newline at end of file + `PlantMeteo.rownumber` is temporary. It soon will be replaced by `DataAPI.rownumber` instead, which will be also used by *e.g.* DataFrames.jl. See [this Pull Request](https://github.com/JuliaData/DataAPI.jl/issues/60). + +## Working with weather data + +Here's a quick example showcasing how to export the example weather data to your own file : + +```julia +meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) +PlantMeteo.write_weather("examples/meteo_day.csv", meteo_day, duration = Dates.Day) +``` + +If you wish to filter weather data, reshape it, adjust it, write it, you'll find some more examples in PlantMeteo's [API reference](https://palmstudio.github.io/PlantMeteo.jl/stable/API/). \ No newline at end of file diff --git a/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation3.jl b/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation3.jl new file mode 100644 index 000000000..0c28899e8 --- /dev/null +++ b/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation3.jl @@ -0,0 +1,254 @@ +########################################### +# Toy plant model with an updated decision model for organ growth +# Physiologically and physically completely meaningless +# (no dimension for units, arbitrary values, stores water and carbon in abstract stocks, +# arbitrary max leaf count and root length, constant and non-coupled photosynthesis and water absorption, ...) +# But it should illustrate the basics of simulating a growing multiscale plant with PlantSimEngine's model approach +########################################### + +function get_root_end_node(node::MultiScaleTreeGraph.Node) + root = MultiScaleTreeGraph.get_root(node) + return MultiScaleTreeGraph.traverse(root, x->x, symbol="Root", filter_fun = MultiScaleTreeGraph.isleaf) +end + +function get_roots_count(node::MultiScaleTreeGraph.Node) + root = MultiScaleTreeGraph.get_root(node) + return length(MultiScaleTreeGraph.traverse(root, x->x, symbol="Root")) +end + +function get_n_leaves(node::MultiScaleTreeGraph.Node) + root = MultiScaleTreeGraph.get_root(node) + nleaves = length(MultiScaleTreeGraph.traverse(root, x->1, symbol="Leaf")) + return nleaves +end + +PlantSimEngine.@process "organ_emergence" verbose = false + +struct ToyCustomInternodeEmergence <: AbstractOrgan_EmergenceModel + TT_emergence::Float64 + carbon_internode_creation_cost::Float64 + leaf_surface_area::Float64 + leaves_max_surface_area::Float64 + water_leaf_threshold::Float64 +end + +ToyCustomInternodeEmergence(;TT_emergence=300.0, carbon_internode_creation_cost=200.0, leaf_surface_area=3.0,leaves_max_surface_area=100.0, +water_leaf_threshold=30.0) = ToyCustomInternodeEmergence(TT_emergence, carbon_internode_creation_cost, leaf_surface_area, leaves_max_surface_area, water_leaf_threshold) + +PlantSimEngine.inputs_(m::ToyCustomInternodeEmergence) = (TT_cu=0.0,water_stock=0.0, carbon_stock=0.0, carbon_root_creation_consumed=0.0) +PlantSimEngine.outputs_(m::ToyCustomInternodeEmergence) = (TT_cu_emergence=0.0, carbon_organ_creation_consumed=0.0) + +function PlantSimEngine.run!(m::ToyCustomInternodeEmergence, models, status, meteo, constants=nothing, sim_object=nothing) + + leaves_surface_area = m.leaf_surface_area * get_n_leaves(status.node) + status.carbon_organ_creation_consumed = 0.0 + + if leaves_surface_area > m.leaves_max_surface_area + return nothing + end + + # if water levels are low, prioritise roots + if status.water_stock < m.water_leaf_threshold + return nothing + end + + # take into account that the stock may already be depleted + carbon_stock_updated_after_roots = status.carbon_stock - status.carbon_root_creation_consumed + + # if not enough carbon, no organ creation + if carbon_stock_updated_after_roots < m.carbon_internode_creation_cost + return nothing + end + + if length(MultiScaleTreeGraph.children(status.node)) == 2 && + 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) + add_organ!(status_new_internode.node, sim_object, "+", "Leaf", 2, index=1) + + status_new_internode.TT_cu_emergence = m.TT_emergence - status.TT_cu + status.carbon_organ_creation_consumed = m.carbon_internode_creation_cost + end + + return nothing +end + +############################ +# Naive water absorption model +# Absorbs precipitation water depending on quantity of roots +############################ +PlantSimEngine.@process "water_absorption" verbose = false + +struct ToyWaterAbsorptionModel <: AbstractWater_AbsorptionModel +end + +PlantSimEngine.inputs_(::ToyWaterAbsorptionModel) = (root_water_assimilation=1.0,) +PlantSimEngine.outputs_(::ToyWaterAbsorptionModel) = (water_absorbed=0.0,) + +function PlantSimEngine.run!(m::ToyWaterAbsorptionModel, models, status, meteo, constants=nothing, extra=nothing) + #root_end = get_root_end_node(status.node) + #root_len = root_end[:Root_len] + status.water_absorbed = meteo.Precipitations * status.root_water_assimilation #* root_len +end + +PlantSimEngine.TimeStepDependencyTrait(::Type{<:ToyWaterAbsorptionModel}) = PlantSimEngine.IsTimeStepIndependent() +PlantSimEngine.ObjectDependencyTrait(::Type{<:ToyWaterAbsorptionModel}) = PlantSimEngine.IsObjectIndependent() + + +########################## +### Root growth : when water stocks are low, expand root +########################## + +PlantSimEngine.@process "root_growth" verbose = false + +struct ToyRootGrowthModel <: AbstractRoot_GrowthModel + carbon_root_creation_cost + root_max_len::Int +end + +PlantSimEngine.inputs_(::ToyRootGrowthModel) = NamedTuple() +PlantSimEngine.outputs_(::ToyRootGrowthModel) = (carbon_root_creation_consumed=0.0,) + +function PlantSimEngine.run!(m::ToyRootGrowthModel, models, status, meteo, constants=nothing, extra=nothing) + status.carbon_root_creation_consumed = 0.0 + + root_end = get_root_end_node(status.node) + + if length(root_end) != 1 + throw(AssertionError("Couldn't find MTG leaf node with symbol \"Root\"")) + end + + root_len = get_roots_count(root_end[1]) + if root_len < m.root_max_len + st = add_organ!(root_end[1], extra, "<", "Root", 2, index=1) + status.carbon_root_creation_consumed = m.carbon_root_creation_cost + end +end + +########################## +### Decision model controlling the root growth model +########################## +PlantSimEngine.@process "root_growth_decision" verbose = false + +struct ToyRootGrowthDecisionModel <: AbstractRoot_Growth_DecisionModel + water_threshold::Float64 + carbon_root_creation_cost::Float64 +end + +PlantSimEngine.inputs_(::ToyRootGrowthDecisionModel) = +(water_stock=0.0,carbon_stock=0.0) + +PlantSimEngine.outputs_(::ToyRootGrowthDecisionModel) = NamedTuple() + +PlantSimEngine.dep(::ToyRootGrowthDecisionModel) = (root_growth=AbstractRoot_GrowthModel=>["Root"],) + +function PlantSimEngine.run!(m::ToyRootGrowthDecisionModel, models, status, meteo, constants=nothing, extra=nothing) + + if status.water_stock < m.water_threshold && status.carbon_stock > m.carbon_root_creation_cost + status_Root= extra.statuses["Root"][1] + PlantSimEngine.run!(extra.models["Root"].root_growth, models, status_Root, meteo, constants, extra) + end +end + + +########################## +### Model accumulating carbon and water resources +########################## + +PlantSimEngine.@process "resource_stock_computation" verbose = false + +struct ToyStockComputationModel <: AbstractResource_Stock_ComputationModel +end + +PlantSimEngine.inputs_(::ToyStockComputationModel) = +(water_absorbed=0.0,carbon_captured=0.0,carbon_organ_creation_consumed=0.0,carbon_root_creation_consumed=0.0) + +PlantSimEngine.outputs_(::ToyStockComputationModel) = (water_stock=-Inf,carbon_stock=-Inf) + +function PlantSimEngine.run!(m::ToyStockComputationModel, models, status, meteo, constants=nothing, extra=nothing) + status.water_stock += sum(status.water_absorbed) + status.carbon_stock += sum(status.carbon_captured) - sum(status.carbon_organ_creation_consumed) - sum(status.carbon_root_creation_consumed) +end + + +######################## +## Leaf model capturing some arbitrary carbon quantity +######################## + +PlantSimEngine.@process "leaf_carbon_capture" verbose = false + +struct ToyLeafCarbonCaptureModel<: AbstractLeaf_Carbon_CaptureModel end + +function PlantSimEngine.inputs_(::ToyLeafCarbonCaptureModel) + NamedTuple()#(TT_cu=-Inf) +end + +function PlantSimEngine.outputs_(::ToyLeafCarbonCaptureModel) + (carbon_captured=0.0,) +end + +function PlantSimEngine.run!(::ToyLeafCarbonCaptureModel, models, status, meteo, constants, extra) + # very crude approximation with LAI of 1 and constant PPFD + status.carbon_captured = 200.0 *(1.0 - exp(-0.2)) +end + + +mapping = Dict( +"Scene" => ToyDegreeDaysCumulModel(), +"Plant" => ( + MultiScaleModel( + model=ToyStockComputationModel(), + mapping=[ + :carbon_captured=>["Leaf"], + :water_absorbed=>["Root"], + PreviousTimeStep(:carbon_root_creation_consumed)=>"Root", + PreviousTimeStep(:carbon_organ_creation_consumed)=>["Internode"], + ], + ), + ToyRootGrowthDecisionModel(10.0, 50.0), + Status(water_stock = 0.0, carbon_stock = 0.0) + ), +"Internode" => ( + MultiScaleModel( + model=ToyCustomInternodeEmergence(),#TT_emergence=20.0), + mapping=[:TT_cu => "Scene", + :water_stock=>"Plant", + :carbon_stock=>"Plant", + :carbon_root_creation_consumed=>"Root"], + ), + Status(carbon_organ_creation_consumed=0.0), + ), +"Root" => (ToyRootGrowthModel(50.0,10), + ToyWaterAbsorptionModel(), + Status(carbon_root_creation_consumed=0.0, root_water_assimilation=1.0), + ), +"Leaf" => ( ToyLeafCarbonCaptureModel(),), +) + + mtg = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Scene", 1, 0)) +#MultiScaleTreeGraph.Node(mtg, MultiScaleTreeGraph.NodeMTG("/", "Soil", 1, 1)) + plant = MultiScaleTreeGraph.Node(mtg, MultiScaleTreeGraph.NodeMTG("+", "Plant", 1, 1)) + + internode1 = MultiScaleTreeGraph.Node(plant, MultiScaleTreeGraph.NodeMTG("/", "Internode", 1, 2)) + MultiScaleTreeGraph.Node(internode1, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) + MultiScaleTreeGraph.Node(internode1, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) + + internode2 = MultiScaleTreeGraph.Node(internode1, MultiScaleTreeGraph.NodeMTG("<", "Internode", 1, 2)) + MultiScaleTreeGraph.Node(internode2, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) + MultiScaleTreeGraph.Node(internode2, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) + + plant_root_start = MultiScaleTreeGraph.Node( + #MultiScaleTreeGraph.new_id(MultiScaleTreeGraph.get_root(plant)), + plant, + MultiScaleTreeGraph.NodeMTG("+", "Root", 1, 3), + #Dict{String, Any}("Root_len"=> 1) + ) + #plant_root_start[:Root_len]=1 + + meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) + + outs = run!(mtg, mapping, meteo_day) + mtg + + + length(MultiScaleTreeGraph.traverse(mtg,x->x, symbol="Leaf")) \ No newline at end of file From eb5a0e1219853e533fe2f8b1fb191deb7833f138 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 3 Mar 2025 13:24:36 +0100 Subject: [PATCH 071/147] Rename mapping kwarg to mapped_variables in mapping declarations, to avoid confusion and improve clarity. (+minor doc edit) --- README.md | 14 +++---- docs/src/index.md | 14 +++---- docs/src/multiscale/multiscale.md | 14 +++---- docs/src/multiscale/multiscale_coupling.md | 6 +-- docs/src/multiscale/multiscale_cyclic.md | 10 ++--- docs/src/multiscale/multiscale_example_1.md | 8 ++-- docs/src/multiscale/multiscale_example_2.md | 6 +-- docs/src/multiscale/multiscale_example_3.md | 4 +- docs/src/prerequisites/key_concepts.md | 14 ++++++- ...lantsimengine_and_julia_troubleshooting.md | 20 +++++----- .../ToyPlantSimulation1.jl | 4 +- .../ToyPlantSimulation2.jl | 6 +-- .../ToyPlantSimulation3.jl | 4 +- src/Abstract_model_structs.jl | 2 +- src/dependencies/hard_dependencies.jl | 2 +- src/doc_templates/mtg-related.jl | 6 +-- src/mtg/MultiScaleModel.jl | 36 +++++++++--------- src/mtg/mapping/getters.jl | 10 ++--- .../model_generation_from_status_vectors.jl | 12 +++--- src/mtg/mapping/reverse_mapping.jl | 4 +- src/mtg/save_results.jl | 6 +-- test/downstream/test-PSE-benchmark.jl | 14 +++---- test/helper-functions.jl | 20 +++++----- test/test-MultiScaleModel.jl | 24 ++++++------ test/test-corner-cases.jl | 12 +++--- test/test-mapping.jl | 20 +++++----- test/test-mtg-dynamic.jl | 14 +++---- test/test-mtg-multiscale-cyclic-dep.jl | 34 ++++++++--------- test/test-mtg-multiscale.jl | 38 +++++++++---------- 29 files changed, 195 insertions(+), 183 deletions(-) diff --git a/README.md b/README.md index e942ffda4..a0b101df9 100644 --- a/README.md +++ b/README.md @@ -205,35 +205,35 @@ mapping = Dict( "Plant" => ( MultiScaleModel( model=ToyLAIModel(), - mapping=[ + mapped_variables=[ :TT_cu => "Scene", ], ), Beer(0.6), MultiScaleModel( model=ToyAssimModel(), - mapping=[:soil_water_content => "Soil"], + mapped_variables=[:soil_water_content => "Soil"], ), MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ :carbon_demand => ["Leaf", "Internode"], :carbon_allocation => ["Leaf", "Internode"] ], ), MultiScaleModel( model=ToyPlantRmModel(), - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], + mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], ), ), "Internode" => ( MultiScaleModel( model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), - mapping=[:TT => "Scene",], + mapped_variables=[:TT => "Scene",], ), MultiScaleModel( model=ToyInternodeEmergence(TT_emergence=20.0), - mapping=[:TT_cu => "Scene"], + mapped_variables=[:TT_cu => "Scene"], ), ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004), Status(carbon_biomass=1.0) @@ -241,7 +241,7 @@ mapping = Dict( "Leaf" => ( MultiScaleModel( model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), - mapping=[:TT => "Scene",], + mapped_variables=[:TT => "Scene",], ), ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), Status(carbon_biomass=1.0) diff --git a/docs/src/index.md b/docs/src/index.md index 6b61c7d9a..83c0335bd 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -182,35 +182,35 @@ mapping = Dict( "Plant" => ( MultiScaleModel( model=ToyLAIModel(), - mapping=[ + mapped_variables=[ :TT_cu => "Scene", ], ), Beer(0.6), MultiScaleModel( model=ToyAssimModel(), - mapping=[:soil_water_content => "Soil"], + mapped_variables=[:soil_water_content => "Soil"], ), MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ :carbon_demand => ["Leaf", "Internode"], :carbon_allocation => ["Leaf", "Internode"] ], ), MultiScaleModel( model=ToyPlantRmModel(), - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], + mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], ), ), "Internode" => ( MultiScaleModel( model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), - mapping=[:TT => "Scene",], + mapped_variables=[:TT => "Scene",], ), MultiScaleModel( model=ToyInternodeEmergence(TT_emergence=20.0), - mapping=[:TT_cu => "Scene"], + mapped_variables=[:TT_cu => "Scene"], ), ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004), Status(carbon_biomass=1.0) @@ -218,7 +218,7 @@ mapping = Dict( "Leaf" => ( MultiScaleModel( model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), - mapping=[:TT => "Scene",], + mapped_variables=[:TT => "Scene",], ), ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), Status(carbon_biomass=1.0) diff --git a/docs/src/multiscale/multiscale.md b/docs/src/multiscale/multiscale.md index b6ef8d0d5..fab474891 100644 --- a/docs/src/multiscale/multiscale.md +++ b/docs/src/multiscale/multiscale.md @@ -69,7 +69,7 @@ mapping = Dict( "Leaf" => ( MultiScaleModel( model=ToyAssimModel(), - mapping=[:soil_water_content => "Soil" => :soil_water_content,], + mapped_variables=[:soil_water_content => "Soil" => :soil_water_content,], ), Status(aPPFD=1300.0), ), @@ -104,14 +104,14 @@ mapping = Dict( "Plant" => ( MultiScaleModel( model=ToyLAIModel(), - mapping=[ + mapped_variables=[ :TT_cu => "Scene", ], ), Beer(0.6), MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ :carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], :carbon_allocation => ["Leaf", "Internode"] @@ -119,13 +119,13 @@ mapping = Dict( ), MultiScaleModel( model=ToyPlantRmModel(), - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], + mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], ), ), "Internode" => ( MultiScaleModel( model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), - mapping=[:TT => "Scene",], + mapped_variables=[:TT => "Scene",], ), ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004), Status(carbon_biomass=1.0), @@ -133,11 +133,11 @@ mapping = Dict( "Leaf" => ( MultiScaleModel( model=ToyAssimModel(), - mapping=[:soil_water_content => "Soil", :aPPFD => "Plant"], + mapped_variables=[:soil_water_content => "Soil", :aPPFD => "Plant"], ), MultiScaleModel( model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), - mapping=[:TT => "Scene",], + mapped_variables=[:TT => "Scene",], ), ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), Status(carbon_biomass=0.5), diff --git a/docs/src/multiscale/multiscale_coupling.md b/docs/src/multiscale/multiscale_coupling.md index 3edac5911..396fd16cd 100644 --- a/docs/src/multiscale/multiscale_coupling.md +++ b/docs/src/multiscale/multiscale_coupling.md @@ -9,14 +9,14 @@ In the detailed example discussed previously (TODO), there were several instance "Plant" => ( MultiScaleModel( model=ToyLAIModel(), - mapping=[ + mapped_variables=[ :TT_cu => "Scene", ], ), ... MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ :carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], :carbon_allocation => ["Leaf", "Internode"] @@ -86,7 +86,7 @@ mapping = Dict( "Male" => MultiScaleModel( model=XPalm.InitiationAgeFromPlantAge(), - mapping=[:plant_age => "Plant",], + mapped_variables=[:plant_age => "Plant",], ), ... XPalm.MaleFinalPotentialBiomass( diff --git a/docs/src/multiscale/multiscale_cyclic.md b/docs/src/multiscale/multiscale_cyclic.md index 5bdb3bcc4..a4243f6da 100644 --- a/docs/src/multiscale/multiscale_cyclic.md +++ b/docs/src/multiscale/multiscale_cyclic.md @@ -14,14 +14,14 @@ For example the following mapping will raise an error: "Plant" => ( MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ :carbon_demand => ["Leaf", "Internode"], :carbon_allocation => ["Leaf", "Internode"] ], ), MultiScaleModel( model=ToyPlantRmModel(), - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], + mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], ), Status(total_surface=0.001, aPPFD=1300.0, soil_water_content=0.6), ), @@ -83,7 +83,7 @@ We can fix our previous mapping by computing the organs respiration using the ca ), MultiScaleModel( model=ToyPlantRmModel(), - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], + mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], ), Status(total_surface=0.001, aPPFD=1300.0, soil_water_content=0.6, carbon_assimilation=5.0), ), @@ -91,7 +91,7 @@ We can fix our previous mapping by computing the organs respiration using the ca ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), MultiScaleModel( model=ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004), - mapping=[PreviousTimeStep(:carbon_biomass),], #! this is where we break the cyclic dependency (first break) + mapped_variables=[PreviousTimeStep(:carbon_biomass),], #! this is where we break the cyclic dependency (first break) ), Status(TT=10.0, carbon_biomass=1.0), ), @@ -99,7 +99,7 @@ We can fix our previous mapping by computing the organs respiration using the ca ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), MultiScaleModel( model=ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), - mapping=[PreviousTimeStep(:carbon_biomass),], #! this is where we break the cyclic dependency (second break) + mapped_variables=[PreviousTimeStep(:carbon_biomass),], #! this is where we break the cyclic dependency (second break) ), ToyCBiomassModel(1.2), Status(TT=10.0), diff --git a/docs/src/multiscale/multiscale_example_1.md b/docs/src/multiscale/multiscale_example_1.md index e4c4878ed..2676968ae 100644 --- a/docs/src/multiscale/multiscale_example_1.md +++ b/docs/src/multiscale/multiscale_example_1.md @@ -182,7 +182,7 @@ The organ creation model at the "Internode" scale needs the carbon stock from th The resource storing model at the "Plant" scale needs the carbon captured by **every** leaf, and the carbon consumed by **every** internode that created new organs this timestep. This requires mapping vector variables : ```julia - mapping=[ + mapped_variables=[ :carbon_captured=>["Leaf"], :carbon_organ_creation_consumed=>["Internode"] ], @@ -190,7 +190,7 @@ The resource storing model at the "Plant" scale needs the carbon captured by **e as opposed to the single-valued carbon stock mapped variable : ```julia - mapping=[:TT_cu => "Scene", + mapped_variables=[:TT_cu => "Scene", PreviousTimeStep(:carbon_stock)=>"Plant"], ``` @@ -202,7 +202,7 @@ mapping = Dict( "Plant" => ( MultiScaleModel( model=ToyStockComputationModel(), - mapping=[ + mapped_variables=[ :carbon_captured=>["Leaf"], :carbon_organ_creation_consumed=>["Internode"] ], @@ -212,7 +212,7 @@ mapping = Dict( "Internode" => ( MultiScaleModel( model=ToyCustomInternodeEmergence(),#TT_emergence=20.0), - mapping=[:TT_cu => "Scene", + mapped_variables=[:TT_cu => "Scene", PreviousTimeStep(:carbon_stock)=>"Plant"], ), Status(carbon_organ_creation_consumed=0.0), diff --git a/docs/src/multiscale/multiscale_example_2.md b/docs/src/multiscale/multiscale_example_2.md index 51362ccce..46b4a1df0 100644 --- a/docs/src/multiscale/multiscale_example_2.md +++ b/docs/src/multiscale/multiscale_example_2.md @@ -163,7 +163,7 @@ mapping = Dict( "Plant" => ( MultiScaleModel( model=ToyStockComputationModel(), - mapping=[ + mapped_variables=[ :carbon_captured=>["Leaf"], :water_absorbed=>["Root"], :carbon_root_creation_consumed=>["Root"], @@ -176,7 +176,7 @@ mapping = Dict( "Internode" => ( MultiScaleModel( model=ToyCustomInternodeEmergence(),#TT_emergence=20.0), - mapping=[:TT_cu => "Scene", + mapped_variables=[:TT_cu => "Scene", PreviousTimeStep(:water_stock)=>"Plant", PreviousTimeStep(:carbon_stock)=>"Plant"], ), @@ -184,7 +184,7 @@ mapping = Dict( ), "Root" => ( MultiScaleModel( model=ToyRootGrowthModel(10.0, 50.0, 10), - mapping=[PreviousTimeStep(:carbon_stock)=>"Plant", + mapped_variables=[PreviousTimeStep(:carbon_stock)=>"Plant", PreviousTimeStep(:water_stock)=>"Plant"], ), ToyWaterAbsorptionModel(), diff --git a/docs/src/multiscale/multiscale_example_3.md b/docs/src/multiscale/multiscale_example_3.md index 7efde2d65..9d9471351 100644 --- a/docs/src/multiscale/multiscale_example_3.md +++ b/docs/src/multiscale/multiscale_example_3.md @@ -126,7 +126,7 @@ mapping = Dict( "Plant" => ( MultiScaleModel( model=ToyStockComputationModel(), - mapping=[ + mapped_variables=[ :carbon_captured=>["Leaf"], :water_absorbed=>["Root"], :carbon_root_creation_consumed=>"Root", @@ -142,7 +142,7 @@ mapping = Dict( "Internode" => ( MultiScaleModel( model=ToyCustomInternodeEmergence(),#TT_emergence=20.0), - mapping=[:TT_cu => "Scene", + mapped_variables=[:TT_cu => "Scene", :water_stock=>"Plant", :carbon_stock=>"Plant", :carbon_root_creation_consumed=>"Root"], diff --git a/docs/src/prerequisites/key_concepts.md b/docs/src/prerequisites/key_concepts.md index 4c9459b1e..c5cd0e041 100644 --- a/docs/src/prerequisites/key_concepts.md +++ b/docs/src/prerequisites/key_concepts.md @@ -8,6 +8,11 @@ You'll find a brief description of some of the main concepts and terminology rel ## PlantSimEngine terminology +This section provides a general description of the concepts and terminology used in PlantSimEngine. For a more implementation-guided description of the design and some of the terms presented here, see [First Simulation]TODO + +!!! Note + Some terminology unfortunately has different meanings in different contexts. This is particularly true of the terms organ, scale and symbol, which have a different meaning for Multi-scale tree graphs than the rest of PlantSimEngine. Make sure to double-check this section, and relevant examples if you encounter issues relating to these terms. + ### Processes A process in this package defines a biological or physical phenomena. Think of any process happening in a system, such as light interception, photosynthesis, water, carbon and energy fluxes, growth, yield or even electricity produced by solar panels. @@ -64,7 +69,9 @@ Note that hard dependencies can also have their own hard dependencies, and some To run a simulation, we usually need the climatic/meteorological conditions measured close to the object or component. -Users are strongly encouraged to use [`PlantMeteo.jl`](https://github.com/PalmStudio/PlantMeteo.jl), the companion package that helps manage such data, with default pre-computations and structures for efficient computations. The most basic data structure from this package is a type called [`Atmosphere`](https://palmstudio.github.io/PlantMeteo.jl/stable/#PlantMeteo.Atmosphere), which defines steady-state atmospheric conditions, *i.e.* the conditions are considered at equilibrium. Another structure is available to define different consecutive time-steps: [`TimeStepTable`](https://palmstudio.github.io/PlantMeteo.jl/stable/#PlantMeteo.TimeStepTable). +Users are strongly encouraged to use [`PlantMeteo.jl`](https://github.com/PalmStudio/PlantMeteo.jl), the companion package that helps manage such data, with default pre-computations and structures for efficient computations. We will make constant use of it throughout the documentation, and recommend working with it. + +The most basic data structure from this package is a type called [`Atmosphere`](https://palmstudio.github.io/PlantMeteo.jl/stable/#PlantMeteo.Atmosphere), which defines steady-state atmospheric conditions, *i.e.* the conditions are considered at equilibrium. Another structure is available to define different consecutive time-steps: [`TimeStepTable`](https://palmstudio.github.io/PlantMeteo.jl/stable/#PlantMeteo.TimeStepTable). The mandatory variables to provide for an [`Atmosphere`](https://palmstudio.github.io/PlantMeteo.jl/stable/#PlantMeteo.Atmosphere) are: `T` (air temperature in °C), `Rh` (relative humidity, 0-1) and `Wind` (the wind speed, m s⁻¹). @@ -97,6 +104,11 @@ This is why multi-scale simulations make use of a 'mapping' : the ModelList in t TODO +!!! Note + When you encounter the terms "Single-scale simulations", or "ModelList simulations", they will refer to simulations that are "not multi-scale". A multi-scale simulation makes use of a mapping between different organ/scale levels. A single-scale simulation has no such mapping, and uses the simpler ModelList interface. + + You can implement a mapping that only makes use of a single scale level, of course, making it a "single-scale multi-scale simulation", but unless otherwise specified, single-scale, and the whole section dedicated to single-scale simulations, refer to simulations with ModelList objects, and no mapping. + ### Multi-scale Tree Graphs Multi-scale Tree Graphs (MTG) are a data structure used to represent plants. A more detailed introduction to the format and its attributes can be found [in the MultiScaleTreeGraph.jl package documentation](https://vezy.github.io/MultiScaleTreeGraph.jl/stable/the_mtg/mtg_concept/). diff --git a/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md b/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md index 7f9c2a01a..f335f9af2 100644 --- a/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md +++ b/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md @@ -92,12 +92,12 @@ The syntax for an empty NamedTuple is `NamedTuple()`. If instead one types `()` ### Forgetting a kwarg when declaring a MultiScaleModel -A MultiScaleModel requires two kwargs, model and mapping : +A MultiScaleModel requires two kwargs, model and mapped_variables : ```julia models = MultiScaleModel( model=ToyLAIModel(), - mapping=[:TT_cu => "Scene",], + mapped_variables=[:TT_cu => "Scene",], ) ``` @@ -106,19 +106,19 @@ Forgetting 'model=' : ```julia models = MultiScaleModel( ToyLAIModel(), - mapping=[:TT_cu => "Scene",], + mapped_variables=[:TT_cu => "Scene",], ) -ERROR: MethodError: no method matching MultiScaleModel(::ToyLAIModel; mapping::Vector{Pair{Symbol, String}}) +ERROR: MethodError: no method matching MultiScaleModel(::ToyLAIModel; mapped_variables::Vector{Pair{Symbol, String}}) The type `MultiScaleModel` exists, but no method is defined for this combination of argument types when trying to construct it. Closest candidates are: - MultiScaleModel(::T, ::Any) where T<:AbstractModel got unsupported keyword argument "mapping" + MultiScaleModel(::T, ::Any) where T<:AbstractModel got unsupported keyword argument "mapped_variables" @ PlantSimEngine PlantSimEngine/src/mtg/MultiScaleModel.jl:188 - MultiScaleModel(; model, mapping) + MultiScaleModel(; model, mapped_variables) @ PlantSimEngine PlantSimEngine/src/mtg/MultiScaleModel.jl:191 ``` -Forgetting 'mapping=' : +Forgetting 'mapped_variables=' : ```julia models = MultiScaleModel( model=ToyLAIModel(), @@ -144,7 +144,7 @@ A possible cause for this error is that a variable was declared instead of a sym mapping = Dict("Scale" => MultiScaleModel( model = ToyModel(), - mapping = [should_be_symbol => "Other_Scale"] # should_be_symbol is a variable, likely not found in the current module + mapped_variables = [should_be_symbol => "Other_Scale"] # should_be_symbol is a variable, likely not found in the current module ), ... ), @@ -155,7 +155,7 @@ Here's the correct version : mapping = Dict("Scale" => MultiScaleModel( model = ToyModel(), - mapping = [:should_be_symbol => "Other_Scale"] # should_be_symbol is now a symbol + mapped_variables=[:should_be_symbol => "Other_Scale"] # should_be_symbol is now a symbol ), ... ), @@ -289,7 +289,7 @@ If there is a need to collect variables at two different scales, and one scale i "E2" => ( MultiScaleModel( model = HardDepSameScaleEchelle2Model(), - mapping = [:c => "E1" => :c, :e3 => "E3" => :e3, :f3 => "E3" => :f3,], + mapped_variables=[:c => "E1" => :c, :e3 => "E3" => :e3, :f3 => "E3" => :f3,], ), ), # No E3 in the mapping ! diff --git a/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation1.jl b/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation1.jl index 37bd2a2c5..f872b9ec9 100644 --- a/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation1.jl +++ b/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation1.jl @@ -101,7 +101,7 @@ mapping = Dict( "Plant" => ( MultiScaleModel( model=ToyStockComputationModel(), - mapping=[ + mapped_variables=[ :carbon_captured=>["Leaf"], :carbon_organ_creation_consumed=>["Internode"] ], @@ -111,7 +111,7 @@ mapping = Dict( "Internode" => ( MultiScaleModel( model=ToyCustomInternodeEmergence(),#TT_emergence=20.0), - mapping=[:TT_cu => "Scene", + mapped_variables=[:TT_cu => "Scene", PreviousTimeStep(:carbon_stock)=>"Plant"], ), Status(carbon_organ_creation_consumed=0.0), diff --git a/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation2.jl b/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation2.jl index c717ebe78..d8d67f2b0 100644 --- a/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation2.jl +++ b/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation2.jl @@ -181,7 +181,7 @@ mapping = Dict( "Plant" => ( MultiScaleModel( model=ToyStockComputationModel(), - mapping=[ + mapped_variables=[ :carbon_captured=>["Leaf"], :water_absorbed=>["Root"], :carbon_root_creation_consumed=>["Root"], @@ -194,7 +194,7 @@ mapping = Dict( "Internode" => ( MultiScaleModel( model=ToyCustomInternodeEmergence(),#TT_emergence=20.0), - mapping=[:TT_cu => "Scene", + mapped_variables=[:TT_cu => "Scene", PreviousTimeStep(:water_stock)=>"Plant", PreviousTimeStep(:carbon_stock)=>"Plant"], ), @@ -202,7 +202,7 @@ mapping = Dict( ), "Root" => ( MultiScaleModel( model=ToyRootGrowthModel(10.0, 50.0, 10), - mapping=[PreviousTimeStep(:carbon_stock)=>"Plant", + mapped_variables=[PreviousTimeStep(:carbon_stock)=>"Plant", PreviousTimeStep(:water_stock)=>"Plant"], ), ToyWaterAbsorptionModel(), diff --git a/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation3.jl b/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation3.jl index 0c28899e8..1699f1f24 100644 --- a/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation3.jl +++ b/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation3.jl @@ -198,7 +198,7 @@ mapping = Dict( "Plant" => ( MultiScaleModel( model=ToyStockComputationModel(), - mapping=[ + mapped_variables=[ :carbon_captured=>["Leaf"], :water_absorbed=>["Root"], PreviousTimeStep(:carbon_root_creation_consumed)=>"Root", @@ -211,7 +211,7 @@ mapping = Dict( "Internode" => ( MultiScaleModel( model=ToyCustomInternodeEmergence(),#TT_emergence=20.0), - mapping=[:TT_cu => "Scene", + mapped_variables=[:TT_cu => "Scene", :water_stock=>"Plant", :carbon_stock=>"Plant", :carbon_root_creation_consumed=>"Root"], diff --git a/src/Abstract_model_structs.jl b/src/Abstract_model_structs.jl index 4547b8512..9cef7b087 100644 --- a/src/Abstract_model_structs.jl +++ b/src/Abstract_model_structs.jl @@ -25,4 +25,4 @@ model_(m::AbstractModel) = m get_models(m::AbstractModel) = [model_(m)] # Get the models of an AbstractModel # Note: it is returning a vector of models, because in this case the user provided a single model instead of a vector of. get_status(m::AbstractModel) = nothing -get_mapping(m::AbstractModel) = Pair{Symbol,String}[] \ No newline at end of file +get_mapped_variables(m::AbstractModel) = Pair{Symbol,String}[] \ No newline at end of file diff --git a/src/dependencies/hard_dependencies.jl b/src/dependencies/hard_dependencies.jl index 4728b1028..0d1fa5b3c 100644 --- a/src/dependencies/hard_dependencies.jl +++ b/src/dependencies/hard_dependencies.jl @@ -113,7 +113,7 @@ end # When we use a mapping (multiscale), we return the set of soft-dependencies (we put the hard-dependencies as their children): function hard_dependencies(mapping::Dict{String,T}; verbose::Bool=true) where {T} - full_vars_mapping = Dict(first(mod) => Dict(get_mapping(last(mod))) for mod in mapping) + full_vars_mapping = Dict(first(mod) => Dict(get_mapped_variables(last(mod))) for mod in mapping) soft_dep_graphs = Dict{String,Any}() not_found = Dict{Symbol,DataType}() diff --git a/src/doc_templates/mtg-related.jl b/src/doc_templates/mtg-related.jl index 238abfc36..c5c3666dc 100644 --- a/src/doc_templates/mtg-related.jl +++ b/src/doc_templates/mtg-related.jl @@ -34,7 +34,7 @@ mapping = Dict( \ "Plant" => ( \ MultiScaleModel( \ model=ToyCAllocationModel(), \ - mapping=[ \ + mapped_variables=[ \ :carbon_assimilation => ["Leaf"], \ :carbon_demand => ["Leaf", "Internode"], \ :carbon_allocation => ["Leaf", "Internode"] \ @@ -42,7 +42,7 @@ mapping = Dict( \ ), MultiScaleModel( \ model=ToyPlantRmModel(), \ - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],] \ + mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],] \ ), \ ),\ "Internode" => ( \ @@ -53,7 +53,7 @@ mapping = Dict( \ "Leaf" => ( \ MultiScaleModel( \ model=ToyAssimModel(), \ - mapping=[:soil_water_content => "Soil",], \ + mapped_variables=[:soil_water_content => "Soil",], \ ), \ ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), \ ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), \ diff --git a/src/mtg/MultiScaleModel.jl b/src/mtg/MultiScaleModel.jl index 0182e32eb..3243afc6a 100644 --- a/src/mtg/MultiScaleModel.jl +++ b/src/mtg/MultiScaleModel.jl @@ -1,5 +1,5 @@ """ - MultiScaleModel(model, mapping) + MultiScaleModel(model, mapped_variables) A structure to make a model multi-scale. It defines a mapping between the variables of a model and the nodes symbols from which the values are taken from. @@ -7,9 +7,9 @@ model and the nodes symbols from which the values are taken from. # Arguments - `model<:AbstractModel`: the model to make multi-scale -- `mapping<:Vector{Pair{Symbol,Union{AbstractString,Vector{AbstractString}}}}`: a vector of pairs of symbols and strings or vectors of strings +- `mapped_variables<:Vector{Pair{Symbol,Union{AbstractString,Vector{AbstractString}}}}`: a vector of pairs of symbols and strings or vectors of strings -The mapping can be of the form: +The mapped_variables argument can be of the form: 1. `[:variable_name => "Plant"]`: We take one value from the Plant node 2. `[:variable_name => ["Leaf"]]`: We take a vector of values from the Leaf nodes @@ -74,25 +74,25 @@ We can make it multi-scale by defining a mapping between the variables of the mo For example, if the `carbon_allocation` comes from the `Leaf` and `Internode` nodes, we can define the mapping as follows: ```jldoctest mylabel -julia> mapping = [:carbon_allocation => ["Leaf", "Internode"]] +julia> mapped_variables=[:carbon_allocation => ["Leaf", "Internode"]] 1-element Vector{Pair{Symbol, Vector{String}}}: :carbon_allocation => ["Leaf", "Internode"] ``` -The mapping is a vector of pairs of symbols and strings or vectors of strings. In this case, we have only one pair to define the mapping +The mapped_variables argument is a vector of pairs of symbols and strings or vectors of strings. In this case, we have only one pair to define the mapping between the `carbon_allocation` variable and the `Leaf` and `Internode` nodes. -We can now make the model multi-scale by passing the model and the mapping to the `MultiScaleModel` constructor : +We can now make the model multi-scale by passing the model and the mapped variables to the `MultiScaleModel` constructor : ```jldoctest mylabel -julia> multiscale_model = PlantSimEngine.MultiScaleModel(model, mapping) +julia> multiscale_model = PlantSimEngine.MultiScaleModel(model, mapped_variables) MultiScaleModel{ToyCAllocationModel, Vector{Pair{Union{Symbol, PreviousTimeStep}, Union{Pair{String, Symbol}, Vector{Pair{String, Symbol}}}}}}(ToyCAllocationModel(), Pair{Union{Symbol, PreviousTimeStep}, Union{Pair{String, Symbol}, Vector{Pair{String, Symbol}}}}[:carbon_allocation => ["Leaf" => :carbon_allocation, "Internode" => :carbon_allocation]]) ``` -We can access the mapping and the model: +We can access the mapped variables and the model: ```jldoctest mylabel -julia> PlantSimEngine.mapping_(multiscale_model) +julia> PlantSimEngine.mapped_variables_(multiscale_model) 1-element Vector{Pair{Union{Symbol, PreviousTimeStep}, Union{Pair{String, Symbol}, Vector{Pair{String, Symbol}}}}}: :carbon_allocation => ["Leaf" => :carbon_allocation, "Internode" => :carbon_allocation] ``` @@ -104,12 +104,12 @@ ToyCAllocationModel() """ struct MultiScaleModel{T<:AbstractModel,V<:AbstractVector{Pair{A,Union{Pair{S,Symbol},Vector{Pair{S,Symbol}}}}} where {A<:Union{Symbol,PreviousTimeStep},S<:AbstractString}} model::T - mapping::V + mapped_variables::V - function MultiScaleModel{T}(model::T, mapping) where {T<:AbstractModel} + function MultiScaleModel{T}(model::T, mapped_variables) where {T<:AbstractModel} # Check that the variables in the mapping are variables of the model: model_variables = keys(variables(model)) - for i in mapping + for i in mapped_variables # If the var is a PreviousTimeStep, we take the variable name, else take the first element of the pair: var = isa(i, PreviousTimeStep) ? i.variable : first(i) @@ -133,7 +133,7 @@ struct MultiScaleModel{T<:AbstractModel,V<:AbstractVector{Pair{A,Union{Pair{S,Sy process_ = process(model) unfolded_mapping = Pair{Union{Symbol,PreviousTimeStep},Union{Pair{String,Symbol},Vector{Pair{String,Symbol}}}}[] - for i in mapping + for i in mapped_variables push!(unfolded_mapping, _get_var(isa(i, PreviousTimeStep) ? i : Pair(i.first, i.second), process_)) # Note: We are using Pair(i.first, i.second) to make sure the Pair is specialized enough, because sometimes the vector in the mapping made the Pair not specialized enough e.g. [:v1 => "S" => :v2,:v3 => "S"] makes the pairs `Pair{Symbol, Any}`. end @@ -185,16 +185,16 @@ end -function MultiScaleModel(model::T, mapping) where {T<:AbstractModel} - MultiScaleModel{T}(model, mapping) +function MultiScaleModel(model::T, mapped_variables) where {T<:AbstractModel} + MultiScaleModel{T}(model, mapped_variables) end -MultiScaleModel(; model, mapping) = MultiScaleModel(model, mapping) +MultiScaleModel(; model, mapped_variables) = MultiScaleModel(model, mapped_variables) -mapping_(m::MultiScaleModel) = m.mapping +mapped_variables_(m::MultiScaleModel) = m.mapped_variables model_(m::MultiScaleModel) = m.model inputs_(m::MultiScaleModel) = inputs_(m.model) outputs_(m::MultiScaleModel) = outputs_(m.model) get_models(m::MultiScaleModel) = [model_(m)] # Get the models of a MultiScaleModel: # Note: it is returning a vector of models, because in this case the user provided a single MultiScaleModel instead of a vector of. get_status(m::MultiScaleModel) = nothing -get_mapping(m::MultiScaleModel{T,S}) where {T,S} = mapping_(m) \ No newline at end of file +get_mapped_variables(m::MultiScaleModel{T,S}) where {T,S} = mapped_variables_(m) \ No newline at end of file diff --git a/src/mtg/mapping/getters.jl b/src/mtg/mapping/getters.jl index dc4c4fdb9..4c46a7dbd 100644 --- a/src/mtg/mapping/getters.jl +++ b/src/mtg/mapping/getters.jl @@ -26,7 +26,7 @@ If we just give a MultiScaleModel, we get its model as a one-element vector: ```jldoctest mylabel julia> models = MultiScaleModel( \ model=ToyCAllocationModel(), \ - mapping=[ \ + mapped_variables=[ \ :carbon_assimilation => ["Leaf"], \ :carbon_demand => ["Leaf", "Internode"], \ :carbon_allocation => ["Leaf", "Internode"] \ @@ -46,7 +46,7 @@ If we give a tuple of models, we get each model in a vector: julia> models2 = ( \ MultiScaleModel( \ model=ToyAssimModel(), \ - mapping=[:soil_water_content => "Soil",], \ + mapped_variables=[:soil_water_content => "Soil",], \ ), \ ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), \ Status(aPPFD=1300.0, TT=10.0), \ @@ -90,7 +90,7 @@ function get_status(m) end """ - get_mapping(m) + get_mapped_variables(m) Get the mapping of a dictionary of model mapping. @@ -104,8 +104,8 @@ Returns a vector of pairs of symbols and strings or vectors of strings See [`get_models`](@ref) for examples. """ -function get_mapping(m) - mod_mapping = [mapping_(i) for i in m if isa(i, MultiScaleModel)] +function get_mapped_variables(m) + mod_mapping = [mapped_variables_(i) for i in m if isa(i, MultiScaleModel)] if length(mod_mapping) == 0 return Pair{Symbol,String}[] end diff --git a/src/mtg/mapping/model_generation_from_status_vectors.jl b/src/mtg/mapping/model_generation_from_status_vectors.jl index 05e8e4db1..ccf83f42b 100644 --- a/src/mtg/mapping/model_generation_from_status_vectors.jl +++ b/src/mtg/mapping/model_generation_from_status_vectors.jl @@ -86,7 +86,7 @@ function replace_mapping_status_vectors_with_generated_models(mapping_with_vecto HelperNextTimestepModel(), MultiScaleModel( model=HelperCurrentTimestepModel(), - mapping=[PreviousTimeStep(:next_timestep),], + mapped_variables=[PreviousTimeStep(:next_timestep),], ), mapping[organ], ) else @@ -94,7 +94,7 @@ function replace_mapping_status_vectors_with_generated_models(mapping_with_vecto HelperNextTimestepModel(), MultiScaleModel( model=HelperCurrentTimestepModel(), - mapping=[PreviousTimeStep(:next_timestep),], + mapped_variables=[PreviousTimeStep(:next_timestep),], ), mapping[organ]..., ) end @@ -165,7 +165,7 @@ function generate_model_from_status_vector_variable(mapping, timestep_scale, sta # if :current_timestep is not in the same scale if timestep_scale != organ - model_add_decl = "generated_models_534f1c161f91bb346feba1a84a55e8251f5ad446 = (generated_models_534f1c161f91bb346feba1a84a55e8251f5ad446..., MultiScaleModel(model=$model_name($value_534f1c161f91bb346feba1a84a55e8251f5ad446), mapping=[:current_timestep=>\"$timestep_scale\"],),)" + model_add_decl = "generated_models_534f1c161f91bb346feba1a84a55e8251f5ad446 = (generated_models_534f1c161f91bb346feba1a84a55e8251f5ad446..., MultiScaleModel(model=$model_name($value_534f1c161f91bb346feba1a84a55e8251f5ad446), mapped_variables=[:current_timestep=>\"$timestep_scale\"],),)" end eval(Meta.parse(model_add_decl)) @@ -198,7 +198,7 @@ function modellist_to_mapping(modellist_original::ModelList, modellist_status; n models..., MultiScaleModel( model=HelperCurrentTimestepModel(), - mapping=[PreviousTimeStep(:next_timestep),], + mapped_variables=[PreviousTimeStep(:next_timestep),], ), Status((current_timestep=1,next_timestep=1,)) ), @@ -208,7 +208,7 @@ function modellist_to_mapping(modellist_original::ModelList, modellist_status; n models..., MultiScaleModel( model=HelperCurrentTimestepModel(), - mapping=[PreviousTimeStep(:next_timestep),], + mapped_variables=[PreviousTimeStep(:next_timestep),], ), Status((modellist_status..., current_timestep=1,next_timestep=1,)) ), @@ -226,7 +226,7 @@ function modellist_to_mapping(modellist_original::ModelList, modellist_status; n HelperNextTimestepModel(), MultiScaleModel( model=HelperCurrentTimestepModel(), - mapping=[PreviousTimeStep(:next_timestep),], + mapped_variables=[PreviousTimeStep(:next_timestep),], ), new_status, ), diff --git a/src/mtg/mapping/reverse_mapping.jl b/src/mtg/mapping/reverse_mapping.jl index 3c36804d7..059b42947 100644 --- a/src/mtg/mapping/reverse_mapping.jl +++ b/src/mtg/mapping/reverse_mapping.jl @@ -34,7 +34,7 @@ julia> mapping = Dict( \ "Plant" => \ MultiScaleModel( \ model=ToyCAllocationModel(), \ - mapping=[ \ + mapped_variables=[ \ :carbon_assimilation => ["Leaf"], \ :carbon_demand => ["Leaf", "Internode"], \ :carbon_allocation => ["Leaf", "Internode"] \ @@ -44,7 +44,7 @@ julia> mapping = Dict( \ "Leaf" => ( \ MultiScaleModel( \ model=ToyAssimModel(), \ - mapping=[:soil_water_content => "Soil",], \ + mapped_variables=[:soil_water_content => "Soil",], \ ), \ ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), \ Status(aPPFD=1300.0, TT=10.0), \ diff --git a/src/mtg/save_results.jl b/src/mtg/save_results.jl index 29f5e50ae..57f9de02e 100644 --- a/src/mtg/save_results.jl +++ b/src/mtg/save_results.jl @@ -38,7 +38,7 @@ julia> mapping = Dict( \ "Plant" => ( \ MultiScaleModel( \ model=ToyCAllocationModel(), \ - mapping=[ \ + mapped_variables=[ \ :carbon_assimilation => ["Leaf"], \ :carbon_demand => ["Leaf", "Internode"], \ :carbon_allocation => ["Leaf", "Internode"] \ @@ -46,7 +46,7 @@ julia> mapping = Dict( \ ), MultiScaleModel( \ model=ToyPlantRmModel(), \ - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],] \ + mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],] \ ), \ ),\ "Internode" => ( \ @@ -57,7 +57,7 @@ julia> mapping = Dict( \ "Leaf" => ( \ MultiScaleModel( \ model=ToyAssimModel(), \ - mapping=[:soil_water_content => "Soil",], \ + mapped_variables=[:soil_water_content => "Soil",], \ ), \ ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), \ ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), \ diff --git a/test/downstream/test-PSE-benchmark.jl b/test/downstream/test-PSE-benchmark.jl index bf93d7288..3a7131d3b 100644 --- a/test/downstream/test-PSE-benchmark.jl +++ b/test/downstream/test-PSE-benchmark.jl @@ -64,14 +64,14 @@ function do_benchmark_on_heavier_mtg() "Plant" => ( MultiScaleModel( model=ToyLAIModel(), - mapping=[ + mapped_variables=[ :TT_cu => "Scene", ], ), PlantSimEngine.Examples.Beer(0.6), MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ :carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], :carbon_allocation => ["Leaf", "Internode"] @@ -79,17 +79,17 @@ function do_benchmark_on_heavier_mtg() ), MultiScaleModel( model=ToyPlantRmModel(), - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], + mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], ), ), "Internode" => ( MultiScaleModel( model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), - mapping=[:TT => "Scene",], + mapped_variables=[:TT => "Scene",], ), MultiScaleModel( model=ToyInternodeCrazyEmergence(TT_emergence=1.0), - mapping=[:TT_cu => "Scene"], + mapped_variables=[:TT_cu => "Scene"], ), ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004), Status(carbon_biomass=1.0) @@ -97,11 +97,11 @@ function do_benchmark_on_heavier_mtg() "Leaf" => ( MultiScaleModel( model=ToyAssimModel(), - mapping=[:soil_water_content => "Soil", :aPPFD => "Plant"], + mapped_variables=[:soil_water_content => "Soil", :aPPFD => "Plant"], ), MultiScaleModel( model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), - mapping=[:TT => "Scene",], + mapped_variables=[:TT => "Scene",], ), ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), Status(carbon_biomass=1.0) diff --git a/test/helper-functions.jl b/test/helper-functions.jl index ff598825e..c9499f3c5 100644 --- a/test/helper-functions.jl +++ b/test/helper-functions.jl @@ -177,33 +177,33 @@ function get_simple_mapping_bank() "Plant" => ( MultiScaleModel( model=ToyLAIModel(), - mapping=[:TT_cu => "Scene",],), + mapped_variables=[:TT_cu => "Scene",],), Beer(0.6), MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ :carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], :carbon_allocation => ["Leaf", "Internode"]],), MultiScaleModel( model=ToyPlantRmModel(), - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],],),), + mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],],),), "Internode" => ( MultiScaleModel( model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), - mapping=[:TT => "Scene",],), + mapped_variables=[:TT => "Scene",],), MultiScaleModel( model=ToyInternodeEmergence(TT_emergence=20.0), - mapping=[:TT_cu => "Scene"],), + mapped_variables=[:TT_cu => "Scene"],), ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004), Status(carbon_biomass=1.0)), "Leaf" => ( MultiScaleModel( model=ToyAssimModel(), - mapping=[:soil_water_content => "Soil", :aPPFD => "Plant"],), + mapped_variables=[:soil_water_content => "Soil", :aPPFD => "Plant"],), MultiScaleModel( model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), - mapping=[:TT => "Scene",],), + mapped_variables=[:TT => "Scene",],), ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), Status(carbon_biomass=1.0)), "Soil" => (ToySoilWaterModel(),),), @@ -217,7 +217,7 @@ function get_simple_mapping_bank() "Plant" => ( MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ # inputs :carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], @@ -225,7 +225,7 @@ function get_simple_mapping_bank() :carbon_allocation => ["Leaf", "Internode"]],), MultiScaleModel( model=ToyPlantRmModel(), - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],],),), + mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],],),), "Internode" => ( ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004), @@ -233,7 +233,7 @@ function get_simple_mapping_bank() "Leaf" => ( MultiScaleModel( model=ToyAssimModel(), - mapping=[:soil_water_content => "Soil",], + mapped_variables=[:soil_water_content => "Soil",], # Notice we provide "Soil", not ["Soil"], so a single value is expected here ), ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), diff --git a/test/test-MultiScaleModel.jl b/test/test-MultiScaleModel.jl index ca02a07dd..673dac327 100644 --- a/test/test-MultiScaleModel.jl +++ b/test/test-MultiScaleModel.jl @@ -18,65 +18,65 @@ end; @testset "MultiScaleModel: case 1" begin models = MultiScaleModel( model=ToyLAIModel(), - mapping=[:TT_cu => "Scene",], + mapped_variables=[:TT_cu => "Scene",], ) @test models.model == ToyLAIModel() - @test models.mapping == [:TT_cu => "Scene" => :TT_cu] + @test models.mapped_variables == [:TT_cu => "Scene" => :TT_cu] end; @testset "MultiScaleModel: case 2" begin models = MultiScaleModel( model=ToyLAIModel(), - mapping=[:TT_cu => ["Plant"],], + mapped_variables=[:TT_cu => ["Plant"],], ) @test models.model == ToyLAIModel() - @test models.mapping == [:TT_cu => ["Plant" => :TT_cu]] + @test models.mapped_variables == [:TT_cu => ["Plant" => :TT_cu]] models = MultiScaleModel( model=ToyLAIModel(), - mapping=[:TT_cu => ["Leaf", "Internode"],], + mapped_variables=[:TT_cu => ["Leaf", "Internode"],], ) @test models.model == ToyLAIModel() - @test models.mapping == [:TT_cu => ["Leaf" => :TT_cu, "Internode" => :TT_cu]] + @test models.mapped_variables == [:TT_cu => ["Leaf" => :TT_cu, "Internode" => :TT_cu]] end; @testset "MultiScaleModel: case 2, several variables with different format" begin models = MultiScaleModel( model=ToyCAllocationModel(), - mapping=[:carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], :Rm => "Plant" => :Rm_plant], + mapped_variables=[:carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], :Rm => "Plant" => :Rm_plant], ) @test models.model == ToyCAllocationModel() - @test models.mapping == [:carbon_assimilation => ["Leaf" => :carbon_assimilation], :carbon_demand => ["Leaf" => :carbon_demand, "Internode" => :carbon_demand], :Rm => "Plant" => :Rm_plant] + @test models.mapped_variables == [:carbon_assimilation => ["Leaf" => :carbon_assimilation], :carbon_demand => ["Leaf" => :carbon_demand, "Internode" => :carbon_demand], :Rm => "Plant" => :Rm_plant] end; @testset "MultiScaleModel: case with PreviousTimeStep => ..." begin models = MultiScaleModel( model=ToyLAIfromLeafAreaModel(1.0), - mapping=[ + mapped_variables=[ PreviousTimeStep(:plant_surfaces) => "Plant" => :surface, ], ) @test models.model == ToyLAIfromLeafAreaModel(1.0) - @test models.mapping == [PreviousTimeStep(:plant_surfaces, :LAI_Dynamic) => ("Plant" => :surface)] + @test models.mapped_variables == [PreviousTimeStep(:plant_surfaces, :LAI_Dynamic) => ("Plant" => :surface)] end; @testset "MultiScaleModel: several types of mapping" begin models = MultiScaleModel( model=ToyLightPartitioningModel(), - mapping=[ + mapped_variables=[ :aPPFD_larger_scale => "Scene" => :aPPFD, :total_surface => "Scene" ], ) @test models.model == ToyLightPartitioningModel() - @test models.mapping == [:aPPFD_larger_scale => ("Scene" => :aPPFD), :total_surface => ("Scene" => :total_surface)] + @test models.mapped_variables == [:aPPFD_larger_scale => ("Scene" => :aPPFD), :total_surface => ("Scene" => :total_surface)] end \ No newline at end of file diff --git a/test/test-corner-cases.jl b/test/test-corner-cases.jl index 893dc9383..9bb8dbb60 100644 --- a/test/test-corner-cases.jl +++ b/test/test-corner-cases.jl @@ -176,11 +176,11 @@ end Msg3LvlScaleAmontModel(), MultiScaleModel( model=Msg3LvlScaleAvalModel(), - mapping=[:e3 => "E3" => :e3, :b2 => "E2" => :b2, :g2 => "E2" => :g2], + mapped_variables=[:e3 => "E3" => :e3, :b2 => "E2" => :b2, :g2 => "E2" => :g2], ), MultiScaleModel( model=Msg3LvlScaleEchelle1Model(), - mapping=[:e2 => "E2" => :e2, :f2 => "E2" => :f2,], + mapped_variables=[:e2 => "E2" => :e2, :f2 => "E2" => :f2,], ), Status(a=1.0,)# y = 1.0, z = 1.0) ), "E2" => ( @@ -188,14 +188,14 @@ end Msg3LvlScaleAval2Model(), MultiScaleModel( model=Msg3LvlScaleEchelle2Model(), - mapping=[:c => "E1" => :c, :e3 => "E3" => :e3, :f3 => "E3" => :f3,], + mapped_variables=[:c => "E1" => :c, :e3 => "E3" => :e3, :f3 => "E3" => :f3,], ), Status(a2=1.0, i2=1.0,) ), "E3" => ( MultiScaleModel( model=Msg3LvlScaleEchelle3Model(), - mapping=[:c => "E1" => :c,], + mapped_variables=[:c => "E1" => :c,], ), ), ) @@ -336,7 +336,7 @@ end "E1" => (HardDepSameScaleEchelle1Model(), MultiScaleModel( model=HardDepSameScaleEchelle1bisModel(), - mapping=[:e3 => "E3" => :e3], + mapped_variables=[:e3 => "E3" => :e3], ), Status(a=1.0),), "E3" => ( @@ -463,7 +463,7 @@ end "E3" => ( MultiScaleModel( model=SingleModelScale3(), - mapping=[:out1 => "E1" => :out1, :out2 => "E2" => :out2,], + mapped_variables=[:out1 => "E1" => :out1, :out2 => "E2" => :out2,], ), Status(in=1.0, in3=1.0,), ), diff --git a/test/test-mapping.jl b/test/test-mapping.jl index 19234333d..3c6ffc8d5 100755 --- a/test/test-mapping.jl +++ b/test/test-mapping.jl @@ -2,7 +2,7 @@ mapping = Dict( "Plant" => ( MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ # inputs :carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], @@ -12,7 +12,7 @@ mapping = Dict( ), MultiScaleModel( model=ToyPlantRmModel(), - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], + mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], ), ), "Internode" => ( @@ -23,7 +23,7 @@ mapping = Dict( "Leaf" => ( MultiScaleModel( model=ToyAssimModel(), - mapping=[:soil_water_content => "Soil",], + mapped_variables=[:soil_water_content => "Soil",], # Notice we provide "Soil", not ["Soil"], so a single value is expected here ), ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), @@ -111,7 +111,7 @@ function modellist_to_mapping_manual(modellist_original::ModelList, modellist_st PlantSimEngine.HelperNextTimestepModel(), MultiScaleModel( model=PlantSimEngine.HelperCurrentTimestepModel(), - mapping=[PreviousTimeStep(:next_timestep),], + mapped_variables=[PreviousTimeStep(:next_timestep),], ), Status(current_timestep=1,next_timestep=1) ), @@ -230,7 +230,7 @@ end "Plant" => ( MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ # inputs :carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], @@ -240,7 +240,7 @@ end ), MultiScaleModel( model=ToyPlantRmModel(), - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], + mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], ), ), "Internode" => ( @@ -251,7 +251,7 @@ end "Leaf" => ( MultiScaleModel( model=ToyAssimModel(), - mapping=[:soil_water_content => "Soil",], + mapped_variables=[:soil_water_content => "Soil",], # Notice we provide "Soil", not ["Soil"], so a single value is expected here ), ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), @@ -286,7 +286,7 @@ mapping_without_vectors = PlantSimEngine.replace_mapping_status_vectors_with_gen mapping_with_two_vectors = Dict("Plant" => ( MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ # inputs :carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], @@ -296,7 +296,7 @@ mapping_without_vectors = PlantSimEngine.replace_mapping_status_vectors_with_gen ), MultiScaleModel( model=ToyPlantRmModel(), - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], + mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], ), ), "Internode" => ( @@ -307,7 +307,7 @@ mapping_without_vectors = PlantSimEngine.replace_mapping_status_vectors_with_gen "Leaf" => ( MultiScaleModel( model=ToyAssimModel(), - mapping=[:soil_water_content => "Soil",], + mapped_variables=[:soil_water_content => "Soil",], ), ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), diff --git a/test/test-mtg-dynamic.jl b/test/test-mtg-dynamic.jl index 11c30ee93..b767c391e 100644 --- a/test/test-mtg-dynamic.jl +++ b/test/test-mtg-dynamic.jl @@ -15,14 +15,14 @@ mapping = Dict( "Plant" => ( MultiScaleModel( model=ToyLAIModel(), - mapping=[ + mapped_variables=[ :TT_cu => "Scene", ], ), Beer(0.6), MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ :carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], :carbon_allocation => ["Leaf", "Internode"] @@ -30,17 +30,17 @@ mapping = Dict( ), MultiScaleModel( model=ToyPlantRmModel(), - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], + mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], ), ), "Internode" => ( MultiScaleModel( model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), - mapping=[:TT => "Scene",], + mapped_variables=[:TT => "Scene",], ), MultiScaleModel( model=ToyInternodeEmergence(TT_emergence=20.0), - mapping=[:TT_cu => "Scene"], + mapped_variables=[:TT_cu => "Scene"], ), ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004), Status(carbon_biomass=1.0) @@ -48,11 +48,11 @@ mapping = Dict( "Leaf" => ( MultiScaleModel( model=ToyAssimModel(), - mapping=[:soil_water_content => "Soil", :aPPFD => "Plant"], + mapped_variables=[:soil_water_content => "Soil", :aPPFD => "Plant"], ), MultiScaleModel( model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), - mapping=[:TT => "Scene",], + mapped_variables=[:TT => "Scene",], ), ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), Status(carbon_biomass=1.0) diff --git a/test/test-mtg-multiscale-cyclic-dep.jl b/test/test-mtg-multiscale-cyclic-dep.jl index 6d9e99ffb..134941e86 100644 --- a/test/test-mtg-multiscale-cyclic-dep.jl +++ b/test/test-mtg-multiscale-cyclic-dep.jl @@ -19,14 +19,14 @@ out_vars = Dict( "Plant" => ( MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ :carbon_demand => ["Leaf", "Internode"], :carbon_allocation => ["Leaf", "Internode"] ], ), MultiScaleModel( model=ToyPlantRmModel(), - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], + mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], ), Status(total_surface=0.001, aPPFD=1300.0, soil_water_content=0.6), ), @@ -61,14 +61,14 @@ end "Plant" => ( MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ :carbon_demand => ["Leaf", "Internode"], :carbon_allocation => ["Leaf", "Internode"] ], ), MultiScaleModel( model=ToyPlantRmModel(), - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], + mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], ), Status(total_surface=0.001, aPPFD=1300.0, soil_water_content=0.6, carbon_assimilation=5.0), ), @@ -76,7 +76,7 @@ end ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), MultiScaleModel( model=ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004), - mapping=[PreviousTimeStep(:carbon_biomass),], #! this is where we break the cyclic dependency (first break) + mapped_variables=[PreviousTimeStep(:carbon_biomass),], #! this is where we break the cyclic dependency (first break) ), Status(TT=10.0, carbon_biomass=1.0), ), @@ -84,7 +84,7 @@ end ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), MultiScaleModel( model=ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), - mapping=[PreviousTimeStep(:carbon_biomass),], #! this is where we break the cyclic dependency (second break) + mapped_variables=[PreviousTimeStep(:carbon_biomass),], #! this is where we break the cyclic dependency (second break) ), ToyCBiomassModel(1.2), Status(TT=10.0), @@ -121,7 +121,7 @@ end ToyDegreeDaysCumulModel(), MultiScaleModel( model=ToyLAIfromLeafAreaModel(1.0), - mapping=[ + mapped_variables=[ :plant_surfaces => ["Plant" => :surface], ], ), @@ -130,7 +130,7 @@ end "Plant" => ( MultiScaleModel( model=ToyPlantLeafSurfaceModel(), - mapping=[PreviousTimeStep(:leaf_surfaces) => ["Leaf" => :surface],], + mapped_variables=[PreviousTimeStep(:leaf_surfaces) => ["Leaf" => :surface],], #! We use PreviousTimeStep to break the cyclic dependency between the LAI and the leaf surface # that is computed as one of the latest sub-models. Now the LAI used for light interception # will be the one from the previous time-step, and at the end of the time-step we will update @@ -138,41 +138,41 @@ end ), MultiScaleModel( model=ToyLightPartitioningModel(), - mapping=[ + mapped_variables=[ :aPPFD_larger_scale => "Scene" => :aPPFD, :total_surface => "Scene" ], ), MultiScaleModel( model=ToyAssimModel(), - mapping=[ + mapped_variables=[ :soil_water_content => "Soil", ], ), MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ :carbon_demand => ["Leaf", "Internode"], :carbon_allocation => ["Leaf", "Internode"] ], ), MultiScaleModel( model=ToyPlantRmModel(), - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], + mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], ), ), "Internode" => ( MultiScaleModel( model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), - mapping=[:TT => "Scene",], + mapped_variables=[:TT => "Scene",], ), MultiScaleModel( model=ToyInternodeEmergence(TT_emergence=20.0), - mapping=[:TT_cu => "Scene"], + mapped_variables=[:TT_cu => "Scene"], ), MultiScaleModel( model=ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004), - mapping=[PreviousTimeStep(:carbon_biomass),], #! this is where we break the cyclic dependency (first break) + mapped_variables=[PreviousTimeStep(:carbon_biomass),], #! this is where we break the cyclic dependency (first break) ), ToyCBiomassModel(1.1), Status(carbon_biomass=0.0) @@ -180,11 +180,11 @@ end "Leaf" => ( MultiScaleModel( model=ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), - mapping=[:TT => "Scene",], + mapped_variables=[:TT => "Scene",], ), MultiScaleModel( model=ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), - mapping=[PreviousTimeStep(:carbon_biomass),], #! this is where we break the cyclic dependency (first break) + mapped_variables=[PreviousTimeStep(:carbon_biomass),], #! this is where we break the cyclic dependency (first break) ), ToyCBiomassModel(1.2), ToyLeafSurfaceModel(0.1), diff --git a/test/test-mtg-multiscale.jl b/test/test-mtg-multiscale.jl index 0bbf0d537..d1113f46b 100644 --- a/test/test-mtg-multiscale.jl +++ b/test/test-mtg-multiscale.jl @@ -35,7 +35,7 @@ mapping_1 = Dict( "Plant" => ( MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ # inputs :carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], @@ -45,7 +45,7 @@ mapping_1 = Dict( ), MultiScaleModel( model=ToyPlantRmModel(), - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], + mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], ), ), "Internode" => ( @@ -56,7 +56,7 @@ mapping_1 = Dict( "Leaf" => ( MultiScaleModel( model=ToyAssimModel(), - mapping=[:soil_water_content => "Soil",], + mapped_variables=[:soil_water_content => "Soil",], # Notice we provide "Soil", not ["Soil"], so a single value is expected here ), ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), @@ -224,7 +224,7 @@ end "Plant" => MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ # inputs :carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], @@ -236,7 +236,7 @@ end "Leaf" => ( MultiScaleModel( model=ToyAssimModel(), - mapping=[:soil_water_content => "Soil",], + mapped_variables=[:soil_water_content => "Soil",], # Notice we provide "Soil", not ["Soil"], so a single value is expected here ), ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), @@ -265,7 +265,7 @@ end "Plant" => MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ # inputs :carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], @@ -277,7 +277,7 @@ end "Leaf" => ( MultiScaleModel( model=ToyAssimModel(), - mapping=[:soil_water_content => "Soil",], + mapped_variables=[:soil_water_content => "Soil",], # Notice we provide "Soil", not ["Soil"], so a single value is expected here ), ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), @@ -306,7 +306,7 @@ end "Plant" => MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ # inputs :carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], @@ -318,7 +318,7 @@ end "Leaf" => ( MultiScaleModel( model=ToyAssimModel(), - mapping=[:soil_water_content => "Soil",], + mapped_variables=[:soil_water_content => "Soil",], # Notice we provide "Soil", not ["Soil"], so a single value is expected here ), ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), @@ -337,7 +337,7 @@ end "Plant" => MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ # inputs :carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], @@ -349,7 +349,7 @@ end "Leaf" => ( MultiScaleModel( model=ToyAssimModel(), - mapping=[:soil_water_content => "Soil" => :var3,], + mapped_variables=[:soil_water_content => "Soil" => :var3,], # Notice we provide "Soil", not ["Soil"], so a single value is expected here ), ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), @@ -407,7 +407,7 @@ end "Plant" => MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ # inputs :carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], @@ -419,7 +419,7 @@ end "Leaf" => ( MultiScaleModel( model=ToyAssimModel(), - mapping=[:soil_water_content => "Soil",], + mapped_variables=[:soil_water_content => "Soil",], # Notice we provide "Soil", not ["Soil"], so a single value is expected here ), ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), @@ -573,7 +573,7 @@ end "Plant" => ( MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ # inputs :carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], @@ -583,7 +583,7 @@ end ), MultiScaleModel( model=ToyPlantRmModel(), - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], + mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], ), ), "Internode" => ( @@ -594,7 +594,7 @@ end "Leaf" => ( MultiScaleModel( model=ToyAssimModel(), - mapping=[:soil_water_content => "Soil",], + mapped_variables=[:soil_water_content => "Soil",], # Notice we provide "Soil", not ["Soil"], so a single value is expected here ), ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), @@ -631,7 +631,7 @@ end "Plant" => ( MultiScaleModel( model=ToyCAllocationModel(), - mapping=[ + mapped_variables=[ # inputs :carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], @@ -641,7 +641,7 @@ end ), MultiScaleModel( model=ToyPlantRmModel(), - mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], + mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],], ), ), "Internode" => ( @@ -652,7 +652,7 @@ end "Leaf" => ( MultiScaleModel( model=ToyAssimModel(), - mapping=[:soil_water_content => "Soil",], + mapped_variables=[:soil_water_content => "Soil",], # Notice we provide "Soil", not ["Soil"], so a single value is expected here ), ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), From c2f73b49b6a6111134d44ad2baf541bbdc9730a2 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 3 Mar 2025 15:54:19 +0100 Subject: [PATCH 072/147] Slightly awkward change to enable proper type promotion in the modellist outputs (current state had broken uncertainty propagation). --- src/component_models/ModelList.jl | 4 ++++ src/mtg/save_results.jl | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/component_models/ModelList.jl b/src/component_models/ModelList.jl index 81aa7a1f5..e9f02abd3 100644 --- a/src/component_models/ModelList.jl +++ b/src/component_models/ModelList.jl @@ -147,6 +147,7 @@ julia> [typeof(models[i][1]) for i in keys(status(models))] struct ModelList{M<:NamedTuple,S} models::M status::S + type_promotion::Union{Nothing, Dict} end #=function ModelList(models::M, status::Status) where {M<:NamedTuple{names,T} where {names,T<:NTuple{N,<:AbstractModel} where {N}}} @@ -189,6 +190,7 @@ function ModelList( model_list = ModelList( mods, ts_kwargs, + type_promotion ) variables_check && !is_initialized(model_list) @@ -311,6 +313,7 @@ function Base.copy(m::T) where {T<:ModelList} ModelList( m.models, deepcopy(m.status), + deepcopy(m.type_promotion) ) end @@ -318,6 +321,7 @@ function Base.copy(m::T, status) where {T<:ModelList} ModelList( m.models, status, + deepcopy(m.type_promotion) ) end diff --git a/src/mtg/save_results.jl b/src/mtg/save_results.jl index 57f9de02e..a024e1468 100644 --- a/src/mtg/save_results.jl +++ b/src/mtg/save_results.jl @@ -230,7 +230,10 @@ function pre_allocate_outputs(m::ModelList, outs, nsteps; type_promotion=nothing # NOTE : init_variables recreates a DependencyGraph, it's not great # TODO : copy ? - out_vars_all = merge(init_variables(m; verbose=false)...) + out_vars_pre_type_promotion = merge(init_variables(m; verbose=false)...) + + # bit hacky, could be cleaned up + out_vars_all = convert_vars(out_vars_pre_type_promotion, m.type_promotion) out_keys_requested = Symbol[] if !isnothing(outs) From 3d17f299b974b8958acf533f93c7d92e2a7cf1d9 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Tue, 4 Mar 2025 10:59:45 +0100 Subject: [PATCH 073/147] Several documentation changes : two new pages, one deleted, many smaller edits --- docs/src/documentation_improvement.md | 7 ++ docs/src/multiscale/single_to_multiscale.md | 0 docs/src/planned_features.md | 67 ++++++++++------ docs/src/prerequisites/julia_basics.md | 1 + docs/src/prerequisites/key_concepts.md | 15 +++- .../detailed_first_example.md} | 80 +++++++++---------- docs/src/step_by_step/implement_a_process.md | 15 +++- .../step_by_step/quick_and_dirty_examples.md | 2 +- .../downstream_tests.md | 4 +- .../implicit_contracts.md | 18 ++++- .../tips_and_workarounds.md | 2 + 11 files changed, 132 insertions(+), 79 deletions(-) create mode 100644 docs/src/documentation_improvement.md create mode 100644 docs/src/multiscale/single_to_multiscale.md rename docs/src/{prerequisites/design.md => step_by_step/detailed_first_example.md} (52%) diff --git a/docs/src/documentation_improvement.md b/docs/src/documentation_improvement.md new file mode 100644 index 000000000..1ebfe8e97 --- /dev/null +++ b/docs/src/documentation_improvement.md @@ -0,0 +1,7 @@ +# Help improve our documentation ! + +One goal for PlantSimEngine is to ensure testing ecophysiological hypotheses, or building plant simulations is as easy as can be for a wider range of people than previous frameworks. + +Good documentation is essential for that purpose. + +If parts of the documentation are unclear to you, you are very welcome to send a PR, an email, or a message (either on Github or on Discourse : TODO) so that we can improve upon it. \ No newline at end of file diff --git a/docs/src/multiscale/single_to_multiscale.md b/docs/src/multiscale/single_to_multiscale.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/src/planned_features.md b/docs/src/planned_features.md index 4f55be33c..f82dcba37 100644 --- a/docs/src/planned_features.md +++ b/docs/src/planned_features.md @@ -1,33 +1,54 @@ -Roadmap +# Roadmap -more examples ? -reworked mapping API, and other API changes +## Planned major features -Handling varying timesteps -CI/downstream -multi-species +## Varying timesteps -better tracking of memory usage and type stability +Currently, all models are required to make use of the same timestep. Some physiological phenomenae within a plant tend to run on an hourly basis, others are slower. Weather data is often provided daily. Enabling different timesteps depending on the model is on the roadmap. -meteo required variables checking +## Multi-plant/Multi-species simulations -avoid printing the whole shebang by default +A goal for PlantSimEngine down the line is to be able to simulate complex scenes with data comprising several plants, possibly of different species, for agroforestry purposes. -Better dependency graph visualisation -Improving user errors -MTG couple of new features #106, other bugs -other bugs -cyclic management for modellists -dependency graph traversal +Its current state doesn't enable practical declaration of several plant species, or multiple plants relying on similar subsets of models with partially different models or parameters. -run! unrolling -Improved parallelisation ? +## Minor features -state machine checker -graph fuzzing +- Implement a trait or a prepass that checks whether weather data is needed, and if so, if it is properly provided to a simulation +- Better dependency graph visualization and information printing -iteratively build and validate mappings and modellists ? -documenting FP errors, more examples for fitting/type conversion/error propagation -Improve multiscale dependency API ? +## Minor planned improvements and QOL features -other package planned features ? \ No newline at end of file +- A reworked and more consistent mapping API, and multiscale dependency declaration +- Improved user errors +- More examples +- Better dependency graph traversal functions +- Ensure cyclic dependency checking and PreviousTimestep is active for ModelLists + +## Improvements on the testing side + +- Better tracking of memory usage and type stability +- Working CI/Downstream tests +- state machine checker, validating output invariants +- graph fuzzing for improved corner-case testing + +## Possible features (likely not a priority) + +- API enabling iterative builds and validation of mappings and modellists +- Improved parallelisation +- Reintroduce multi-object parallelisation in single-scale + +## Other minor points + +- Documenting floating-point accumulation errors +- More examples for fitting/type conversion/error propagation +- MTG couple of new features #106 +- Other minor bugs +- Unrolling the run! function + +## Other + +The full list of issues can be found [here](https://github.com/VirtualPlantLab/PlantSimEngine.jl/issues) + +TODO +Detail other related packages' roadmaps ? \ No newline at end of file diff --git a/docs/src/prerequisites/julia_basics.md b/docs/src/prerequisites/julia_basics.md index 521bd0484..91d1af283 100644 --- a/docs/src/prerequisites/julia_basics.md +++ b/docs/src/prerequisites/julia_basics.md @@ -14,6 +14,7 @@ If you wish to compare Julia to a specific language, [this page](https://docs.ju You can also find a few cheatsheets [here](https://palmstudio.github.io/Biophysics_database_palm/cheatsheets/) as well as a [short introductory notebook](https://palmstudio.github.io/Biophysics_database_palm/basic_syntax/) along with [install instructions](https://palmstudio.github.io/Biophysics_database_palm/installation/) +## Troubleshooting ### Installing Julia diff --git a/docs/src/prerequisites/key_concepts.md b/docs/src/prerequisites/key_concepts.md index c5cd0e041..0018485ad 100644 --- a/docs/src/prerequisites/key_concepts.md +++ b/docs/src/prerequisites/key_concepts.md @@ -11,7 +11,7 @@ You'll find a brief description of some of the main concepts and terminology rel This section provides a general description of the concepts and terminology used in PlantSimEngine. For a more implementation-guided description of the design and some of the terms presented here, see [First Simulation]TODO !!! Note - Some terminology unfortunately has different meanings in different contexts. This is particularly true of the terms organ, scale and symbol, which have a different meaning for Multi-scale tree graphs than the rest of PlantSimEngine. Make sure to double-check this section, and relevant examples if you encounter issues relating to these terms. + Some terminology unfortunately has different meanings in different contexts. This is particularly true of the terms organ, scale and symbol, which have a different meaning for [Multi-scale tree graphs](TODO) than the rest of PlantSimEngine(TODO). Make sure to double-check this section, and relevant examples if you encounter issues relating to these terms. ### Processes @@ -25,8 +25,7 @@ Models are then implemented for a particular process. There may be different models that can be used for the same process ; for instance, there are multiple hypotheses and ways of modeling photosynthesis, with different granularity and accuracy. A simple photosynthesis model might apply a simple formula and apply it to the total leaf surface, a more complex one might calculate interception and light extinction. -The companion package PlantBiophysics.jl provides the [`Beer`](https://vezy.github.io/PlantBiophysics.jl/stable/functions/#PlantBiophysics.Beer) structure for the implementation of the Beer-Lambert law of light extinction. The process of `light_interception` and the `Beer` model are provided as an example -script in this package too at [`examples/Beer.jl`](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/master/examples/Beer.jl). +The companion package PlantBiophysics.jl provides the [`Beer`](https://vezy.github.io/PlantBiophysics.jl/stable/functions/#PlantBiophysics.Beer) structure for the implementation of the Beer-Lambert law of light extinction. The process of `light_interception` and the `Beer` model are provided as an example script in this package too at [`examples/Beer.jl`](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/master/examples/Beer.jl). Models can also be used for ad hoc computations that aren't directly tied to a specific literature-defined physiological process. In PlantSimEngine, everything is a model. There are many instances where a custom model might be practical to aggregate some computations or handle other information. To illustrate, XPalm, the Oil Palm model has a few models that handle the state of different organs, and a mdoel to handle leaf pruning, which you can find [here](https://github.com/PalmStudio/XPalm.jl/blob/main/src/plant/phytomer/leaves/leaf_pruning.jl). @@ -92,6 +91,16 @@ Plants have different organs with distinct physiological properties and processe PlantSimEngine documentation tends to use the terms "organ" and "scale" mostly interchangeably. "Scale" is a bit more general and accurate, since some models might not operate at a specific organ level, but (for example) at the scene level, so a "Scene" scale might be present in the MTG, and in the user-provided data. +When working with multi-scale data, the scale will often need to be specified to map variables, or to indicate at what scale level models work out. You will see some code resembling this : + +```julia +"Root" => (RootGrowthModel(), OrganAgeModel()), +"Leaf" => (LightInterceptionModel(), OrganAgeModel()), +"Plant" => (TotalBiomassModel(),), +``` + +This example excerpt links specific models to a specific scale. Note that one model is reused at two different scales, and note that "Plant" isn't an actual organ, hence the preferred usage of the term "scale". + ### Multiscale modeling Multi-scale modeling is the process of simulating a system at multiple levels of detail simultaneously. For example, some models can run at the organ scale while others run at the plot scale. Each model can access variables at its scale and other scales if needed, allowing for a more comprehensive system representation. It can also help identify emergent properties that are not apparent at a single level of detail. diff --git a/docs/src/prerequisites/design.md b/docs/src/step_by_step/detailed_first_example.md similarity index 52% rename from docs/src/prerequisites/design.md rename to docs/src/step_by_step/detailed_first_example.md index a6333e40e..0a14d72cb 100644 --- a/docs/src/prerequisites/design.md +++ b/docs/src/step_by_step/detailed_first_example.md @@ -1,13 +1,16 @@ -# Package design +# Detailed walkthrough of a simple simulation + +This section walks you through the ins and outs of a basic simulation, mostly aimed at people who have less experience programming, to showcase the various concepts presented earlier and requirements for a simulation in context. + +The full example discussed in this page can be found [further down](@ref Example simulation). -`PlantSimEngine.jl` is designed to ease the process of modelling and simulation of plants, soil and atmosphere, or really any system (*e.g.* agroforestry system, agrivoltaics...). `PlantSimEngine.jl` aims at being the backbone tool for developing Functional-Structural Plant Models (FSPM) and crop models without the hassle of performance and other computer-science considerations. ```@setup usepkg using PlantSimEngine, PlantMeteo using PlantSimEngine.Examples meteo = Atmosphere(T = 20.0, Wind = 1.0, Rh = 0.65, Ri_PAR_f = 500.0) leaf = ModelList(Beer(0.5), status = (LAI = 2.0,)) -run!(leaf, meteo) +out_sim = run!(leaf, meteo) ``` ## Definitions @@ -16,27 +19,14 @@ run!(leaf, meteo) A process in this package defines a biological or physical phenomena. Think of any process happening in a system, such as light interception, photosynthesis, water, carbon and energy fluxes, growth, yield or even electricity produced by solar panels. -A process is "declared", meaning we just define a process using [`@process`](@ref), and then we implement models for its simulation. Declaring a process generates some boilerplate code for its simulation: - -- an abstract type for the process -- a method for the `process` function, that is used internally - -For example, the `light_interception` process is declared using: - -```julia -@process light_interception -``` - -Which would generate a tutorial to help the user implement a model for the process. - -The abstract process type is then used as a supertype of all models implementations for the process, and is named `AbstractProcess`, *e.g.* `AbstractLight_InterceptionModel`. +A process is "declared", meaning we define a process, and then implement models for its simulation. In this example, we will make use of a process that was already defined, and for which there already is a model implementation. ### Models (ModelList) A process is simulated using a particular implementation, or **a model**. Each model is implemented using a structure that lists the parameters of the model. For example, PlantBiophysics provides the [`Beer`](https://vezy.github.io/PlantBiophysics.jl/stable/functions/#PlantBiophysics.Beer) structure for the implementation of the Beer-Lambert law of light extinction. The process of `light_interception` and the `Beer` model are provided as an example script in this package too at [`examples/Beer.jl`](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/master/examples/Beer.jl). -Models can use three types of entries: +Models can use several types of entries: - Parameters - Meteorological information @@ -44,11 +34,14 @@ Models can use three types of entries: - Constants - Extras -Parameters are constant values that are used by the model to compute its outputs. Meteorological information are values that are provided by the user and are used as inputs to the model. It is defined for one time-step, and `PlantSimEngine.jl` takes care of applying the model to each time-steps given by the user. Variables are either used or computed by the model and can optionally be initialized before the simulation. Constants are constant values, usually common between models, *e.g.* the universal gas constant. And extras are just extra values that can be used by a model, it is for example used to pass the current node of the Multi-Scale Tree Graph to be able to *e.g.* retrieve children or ancestors values. +- Parameters are constant values that are used by the model to compute its outputs, and are exclusive to that model. +- Meteorological information contains values that are provided by the user and are used as inputs to the model. It is defined for one time-step, and `PlantSimEngine.jl` takes care of applying the model to each time-steps given by the user. +- Variables are either used or computed by the model and can optionally be initialized before the simulation. They can be part of multiple models, computed by one and then used as an input by another. They can also be a global simulation output, or be provided at the start of a simulation by the user. +- Constants are constant values, usually common between models, *e.g.* the universal gas constant. And extras are just extra values that can be used by a model. -Users can choose which model is used to simulate a process using the [`ModelList`](@ref) structure. `ModelList` is also used to store the values of the parameters, and to initialize variables. +Users declare a set of models used for simulation, as well as the necessary parameters for each model, and whatever variables need to be initialized. This is done using a [`ModelList`](@ref) structure. -For example let's instantiate a [`ModelList`](@ref) with the Beer-Lambert model of light extinction. The model is implemented with the [`Beer`](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/master/examples/Beer.jl) structure and has only one parameter: the extinction coefficient (`k`). +For example let's instantiate a [`ModelList`](@ref) with a single model : the Beer-Lambert model of light extinction, used to simulate the light interception process. The model is implemented with the [`Beer`](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/master/examples/Beer.jl) structure and only has one parameter: the extinction coefficient (`k`). Importing the package: @@ -62,17 +55,17 @@ Import the examples defined in the `Examples` sub-module (`light_interception` a using PlantSimEngine.Examples ``` -And then making a [`ModelList`](@ref) with the `Beer` model: +And then declare a [`ModelList`](@ref) with the `Beer` model: ```@example usepkg -ModelList(Beer(0.5)) +m = ModelList(Beer(0.5)) ``` What happened here? We provided an instance of the `Beer` model to a [`ModelList`](@ref) to simulate the light interception process. ## Parameters -A parameter is a constant value that is used by a model to compute its outputs. For example, the Beer-Lambert model uses the extinction coefficient (`k`) to compute the light extinction. The Beer-Lambert model is implemented with the `Beer` structure, which has only one field: `k`. We can see that using `fieldnames` on the model structure: +A parameter is a value constant for a simulation that is internal to a model and used for its computations. For example, the Beer-Lambert model uses the extinction coefficient (`k`) to compute the light extinction. The `Beer` structure in the Beer-Lambert model implementation, only has one field: `k`. We can see that using `fieldnames` on the model structure: ```@example usepkg fieldnames(Beer) @@ -80,23 +73,23 @@ fieldnames(Beer) ## Variables (inputs, outputs) -Variables are either input or outputs (*i.e.* computed) of models. Variables and their values are stored in the [`ModelList`](@ref) structure, and are initialized automatically or manually. +Variables are either inputs or outputs (*i.e.* computed) of models. Variables and their values are stored in the [`ModelList`](@ref) structure, and are initialized automatically or manually. For example, the `Beer` model needs the leaf area index (`LAI`, m² m⁻²) to run. -We can see which variables are needed as inputs using [`inputs`](@ref): +We can see which variables are passed in as inputs using [`inputs`](@ref): ```@example usepkg inputs(Beer(0.5)) ``` -and the outputs of the model using [`outputs`](@ref): +and which are computed outputs of the model using [`outputs`](@ref): ```@example usepkg outputs(Beer(0.5)) ``` -If we instantiate a [`ModelList`](@ref) with the Beer-Lambert model, we can see that the `:status` field has two variables: `LAI` and `PPFD`. The first is an input, the second an output (*i.e.* it is computed by the model). +The [`ModelList`](@ref) structure will keep track of every variable's current state when running the simulation, storing them in a field called `status`. We can inspect that field with the `status` function and see that in our example it has two variables: `LAI` and `PPFD`. The first is an input, the second an output (*i.e.* it is computed by the model). ```@example usepkg m = ModelList(Beer(0.5)) @@ -116,12 +109,12 @@ Their values are uninitialized though (hence the warnings): (m[:LAI], m[:aPPFD]) ``` -Uninitialized variables are initialized to the value given in the `inputs` or `outputs` methods, which is usually equal to `typemin()`, *e.g.* `-Inf` for `Float64`. +Uninitialized variables are initialized to the value given in the `inputs` or `outputs` methods in the model's implementation code, which is usually equal to `typemin()`, *e.g.* `-Inf` for `Float64`. !!! tip - Prefer using `to_initialize` rather than `inputs` to check which variables should be initialized. `inputs` returns the variables that are needed by the model to run, but `to_initialize` returns the variables that are needed by the model to run and that are not initialized in the `ModelList`. Also `to_initialize` is more clever when coupling models (see below). + Prefer using `to_initialize` rather than `inputs` to check which variables should be initialized. `inputs` returns every variable that is needed by the model to run, but in multi-model simulations, some of them may already be computed by other models and not require initialization. `to_initialize` returns **only** the variables that are needed by the model to run and that are not initialized in the `ModelList`. -We can initialize the variables by providing their values to the status at instantiation: +We can initialize the required variables by providing their starting values to the status when declaring the `ModelList`: ```@example usepkg m = ModelList(Beer(0.5), status = (LAI = 2.0,)) @@ -141,7 +134,7 @@ We can check if a component is correctly initialized using [`is_initialized`](@r is_initialized(m) ``` -Some variables are inputs of models, but outputs of other models. When we couple models, `PlantSimEngine.jl` is clever and only requests the variables that are not computed by other models. +Some variables are inputs of models, but outputs of other models. When we couple models, `to_initialize` only requests the variables that are not computed by other models. ## Climate forcing @@ -156,13 +149,15 @@ using PlantMeteo meteo = Atmosphere(T = 20.0, Wind = 1.0, Rh = 0.65, Ri_PAR_f = 500.0) ``` +This `meteo` variable will therefore provide a single weather timeframe that can be used in a simulation. + More details are available from the [package documentation](https://vezy.github.io/PlantMeteo.jl/stable). ## Simulation ### Simulation of processes -Making a simulation is rather simple, we simply call the [`run!`](@ref) method on the `ModelList`. If some meteorological data is required for models to be simulated over several timesteps, that can be passed in as a parameter as well. +To run a simulation, you can call the [`run!`](@ref) method on the `ModelList`. If some meteorological data is required for models to be simulated over several timesteps, that can be passed in as an optional argument as well. Here is an example: @@ -172,7 +167,7 @@ run!(model_list, meteo) The first argument is the model list (see [`ModelList`](@ref)), and the second defines the micro-climatic conditions. -The `ModelList` should be initialized for the given process before calling the function. See [Variables (inputs, outputs)](@ref) for more details. +The `ModelList` should already be initialized for the given process before calling the function. Refer to the earlier section [Variables (inputs, outputs)](@ref) for more details. ### Example simulation @@ -194,12 +189,14 @@ outputs_example[:aPPFD] ``` ### Outputs -TODO -The `status` field of a [`ModelList`](@ref) is used to initialize the variables before simulation and then to keep track of their values during and after the simulation. We can extract outputs of the last timestep of a simulation using the [`status`](@ref) function. -The actual full output data is returned by the `run!` function. Data is usually stored in a `TimeStepTable` structure from `PlantMeteo.jl`, which is a fast DataFrame-alike structure with each time step being a [`Status`](@ref). It can be also be any `Tables.jl` structure, such as a regular `DataFrame`. The weather is also usually stored in a `TimeStepTable` but with each time step being an `Atmosphere`. +The `status` field of a [`ModelList`](@ref) is used to initialize the variables before simulation and then to keep track of their values during and after the simulation. We can extract outputs of the very last timestep of a simulation using the [`status`](@ref) function. -Let's look at the outputs of our previous simulated leaf: +The actual full output data is returned by the `run!` function. Data is usually stored in a `TimeStepTable` structure from `PlantMeteo.jl`, which is a fast DataFrame-like structure with each time step being a [`Status`](@ref). It can be also be any `Tables.jl` structure, such as a regular `DataFrame`. The weather is also usually stored in a `TimeStepTable` but with each time step being an `Atmosphere`. + +In our example, the simulation was only provided one weather timestep, so the outputs returned by `run!` and the ModelList's `status` field are identical. + +Let's look at the outputs structure of our previous simulated leaf: ```@setup usepkg outputs_example @@ -217,16 +214,15 @@ Or similarly using the dot syntax: outputs_example.aPPFD ``` -Another simple way to get the results is to transform the outputs into a `DataFrame`. Which is very easy because the `TimeStepTable` implements the Tables.jl interface: +You can then print the outputs, convert them to another format, or visualize them, using other Julia packages. +TODO +Another convenient way to get the results is to transform the outputs into a `DataFrame`. Which is very easy because the `TimeStepTable` implements the Tables.jl interface: ```@example usepkg using DataFrames convert_outputs(outputs_example, DataFrame) ``` -!!! note - The output from this conversion function is adapted to the kind of simulation you did: one row per time-step, and per component models if you simulated several. - ## Model coupling A model can work either independently or in conjunction with other models. For example a stomatal conductance model is often associated with a photosynthesis model, *i.e.* it is called from the photosynthesis model. diff --git a/docs/src/step_by_step/implement_a_process.md b/docs/src/step_by_step/implement_a_process.md index 28cf15dd4..74142ce19 100644 --- a/docs/src/step_by_step/implement_a_process.md +++ b/docs/src/step_by_step/implement_a_process.md @@ -8,11 +8,20 @@ PlantSimEngine.@process growth ## Introduction -`PlantSimEngine.jl` was designed to make the implementation of new processes and models easy and fast. Let's learn about how to implement a new process with a simple example: implementing a growth model. +A process in this package defines a biological or physical phenomena. Think of any process happening in a system, such as light interception, photosynthesis, water, carbon and energy fluxes, growth, yield or even electricity produced by solar panels. + +`PlantSimEngine.jl` was designed to make the implementation of new processes and models easy and fast. The next section showcases how to implement a new process with a simple example: implementing a growth model. ## Implement a process -To implement a new process, we need to define an abstract structure that will help us associate the models to this process. We also need to generate some boilerplate code, such as a method for the `process` function. Fortunately, PlantSimEngine provides a macro to generate all that at once: [`@process`](@ref). This macro takes only one argument: the name of the process. +A process is "declared", meaning we define a process, and then implement models for its simulation. Declaring a process generates some boilerplate code for its simulation: + +- an abstract type for the process +- a method for the `process` function, that is used internally + +The abstract process type is then used as a supertype of all models implementations for the process, and is named `AbstractProcess`, *e.g.* `AbstractLight_InterceptionModel`. + +Fortunately, PlantSimEngine provides a macro to generate all that at once: [`@process`](@ref). This macro takes only one argument: the name of the process. For example, the photosynthesis process in [PlantBiophysics.jl](https://github.com/VEZY/PlantBiophysics.jl) is declared using just this tiny line of code: @@ -26,7 +35,7 @@ If we want to simulate the growth of a plant, we could add a new process called @process "growth" ``` -And that's it! Note that the function guides you in the steps you can make after creating a process. Let's break it up here. +And that's it! Note that the function guides you in the steps you can make after creating a process. ## Implement a new model for the process diff --git a/docs/src/step_by_step/quick_and_dirty_examples.md b/docs/src/step_by_step/quick_and_dirty_examples.md index d4d07bc6b..eaa340752 100644 --- a/docs/src/step_by_step/quick_and_dirty_examples.md +++ b/docs/src/step_by_step/quick_and_dirty_examples.md @@ -40,7 +40,7 @@ models = ModelList( outputs_coupled = run!(models, meteo_day) ``` -## Coupling the light interception model with a Leaf Area Index model +## Coupling the light interception and Leaf Area Index models with a biomass increment model ```julia diff --git a/docs/src/troubleshooting_and_testing/downstream_tests.md b/docs/src/troubleshooting_and_testing/downstream_tests.md index 69df26d7e..04b8dd02a 100644 --- a/docs/src/troubleshooting_and_testing/downstream_tests.md +++ b/docs/src/troubleshooting_and_testing/downstream_tests.md @@ -4,6 +4,4 @@ PlantSimEngine is [open sourced on Github](https://github.com/VirtualPlantLab/Pl One handy CI (Continuous Integration) feature implemented for these packages is automated integration and downstream testing : after changes to a package, its known downstream dependencies are tested to ensure no breaking changes were introduced. For instance, PlantBioPhysics is used in PlantSimEngine, so an integration test ensures that PlantBioPhysics doesn't break in an unforeseen manner after a new PlantSimEngine release. -This is something you can take advantage of if you wish to develop using PlantSimEngine, by providing us with your package name (or adding it to the CI yml file in a Pull Request) ; we can then add it to the list of downstream packages to test, and generate PR when breaking changes are introduced. - -## Help improve our documentation ! \ No newline at end of file +This is something you can take advantage of if you wish to develop using PlantSimEngine, by providing us with your package name (or adding it to the CI yml file in a Pull Request) ; we can then add it to the list of downstream packages to test, and generate PR when breaking changes are introduced. \ No newline at end of file diff --git a/docs/src/troubleshooting_and_testing/implicit_contracts.md b/docs/src/troubleshooting_and_testing/implicit_contracts.md index 58867679f..2b26e69cc 100644 --- a/docs/src/troubleshooting_and_testing/implicit_contracts.md +++ b/docs/src/troubleshooting_and_testing/implicit_contracts.md @@ -10,7 +10,7 @@ In XPalm, weather data for most models is provided daily, meaning biomass calcul Many models are considered to be steady-state over that timeframe, but not all : the leaf pruning model pertubes the plant in a non-steady state fashion, for example. Models that require computations over several iterations to stabilise (often part of hard dependencies) might also have a timestep unrelated to the weather data. -!!! note +!!! Note Implicitely, this means any vector variables given as input to the simulation must be consistent with the number of weather timesteps. Providing one weather value but a larger vector variable is an exception : the weather data is replicated over each timestep. (This may be subject to change in the future when support for different timesteps in a single simulation is implemented) ## Weather data must be interpolated prior to simulation @@ -43,11 +43,11 @@ There may be new parallelisation features for multi-plant simulations further do The final dependency graph is comprised only of soft dependency nodes, and is guaranteed to contain no cycles. Hard dependencies are handled internally by their soft dependency ancestor. To avoid any ambiguity in terms of processing order, only one soft dependency node can 'own' a hard dependency And similarly, nested hard dependencies only have a single soft dependency ancestor. -This is not solely an implementation detail of PlantSimEngine's internal mechanisms ; if your simulation requires complex coupling, you might need to carefully consider how to manage your hard dependencies, or insert an extra intermediate model to simplify thigns. +This is not solely an implementation detail of PlantSimEngine's internal mechanisms ; if your simulation requires complex coupling, you might need to carefully consider how to manage your hard dependencies, or insert an extra intermediate model to simplify things. ## A model can only be used once per scale -Similarly, both for graph creation non-ambiguity (and for simulation cohesion), PlantSimEngine currently assumes a model describing a process only occurs once per scale. +Similarly, to avoid depedency graph ambiguity (and for simulation cohesion), PlantSimEngine currently assumes a model describing a process only occurs once per scale. Model renaming and duplicating works around this assumption. It may change once multi-plant/multi-species features are implemented. @@ -64,4 +64,14 @@ TODO ## Diffusion systems ? -## TODO simulation order, node order, etc. \ No newline at end of file +## TODO simulation order, node order, etc. + +## Simulation order instability when adding models + +An important aspect to bear in mind is that PlantSimEngine automatically determines an order in which models are run from the dependency graph it generates by coupling models together. + +This order of simulation depends on the way the models link together. If you replace a model by a new set of models, or pass in new variables that create new links between models, you may change the simulation order. + +When iterating and slowly making a simulation more physiologically realistic and complex, it is therefore fully possible that the order in which two models are run is flipped by a user change. + +This design choice implementation -a concession made for ease of use and flexibility when developing a simulation- means that until your set of models is fully stabilized and you know which variables are `PreviousTimestep` and what order models run in, as you expand and change the set you might see differences of execution of one timestep for some models. It isn't a conceptual problem as most models are steady-state, and simulation order is stable for a given set of models, but it does mean PlantSimEngine will be less conveient for some types of simulation. \ No newline at end of file diff --git a/docs/src/troubleshooting_and_testing/tips_and_workarounds.md b/docs/src/troubleshooting_and_testing/tips_and_workarounds.md index 5fb13ae3b..84729ec5d 100644 --- a/docs/src/troubleshooting_and_testing/tips_and_workarounds.md +++ b/docs/src/troubleshooting_and_testing/tips_and_workarounds.md @@ -36,6 +36,8 @@ In many other situations one can work with what PlantSimEngine already provides. For example, one model in [XPalm.jl](https://github.com/PalmStudio/XPalm.jl/blob/main/src/plant/phytomer/leaves/leaf_pruning.jl) handles leaf pruning, affecting biomass. A straightforward implementation would be to have a `leaf_biomass` variable as both input and output. The workaround is to instead output a variable `leaf_biomass_pruning_loss` and to have that as input in the next timestep to compute the new leaf biomass. +TODO use toy plant as example + ## Passing in a vector in a mapping status at a specific scale You may have noticed that sometimes a vector (1-dimensional array) variable is passed into the `status` component of a `ModelList` in documentation examples (An example here with cumulative thermal time : [Model switching](@ref)). From 1181396246472df3b6e918b48540a7e3bebf6d6f Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Tue, 4 Mar 2025 17:58:05 +0100 Subject: [PATCH 074/147] Documentation : many changes : extra pages for single to multiscale conversion ; some type promotion and utility functions discussion moved to their own page + other edits. --- docs/make.jl | 16 +- docs/src/multiscale/multiscale.md | 8 +- docs/src/multiscale/multiscale_coupling.md | 18 +- docs/src/multiscale/single_to_multiscale.md | 160 ++++++++++++++ docs/src/planned_features.md | 4 +- docs/src/step_by_step/implement_a_model.md | 198 ++++++------------ .../implement_a_model_additional.md | 93 ++++++++ .../tips_and_workarounds.md | 4 +- .../working_with_data/visualising_outputs.md | 2 +- examples/ToySingleToMultiScale.jl | 85 ++++++++ 10 files changed, 424 insertions(+), 164 deletions(-) create mode 100644 docs/src/step_by_step/implement_a_model_additional.md create mode 100644 examples/ToySingleToMultiScale.jl diff --git a/docs/make.jl b/docs/make.jl index 76810b92d..2602b5ba1 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -32,29 +32,30 @@ makedocs(; "Key Concepts" => "./prerequisites/key_concepts.md", # Key concepts vs terminology ? #"Setup" ?", "Julia language basics" => "./prerequisites/julia_basics.md", - "Design" => "./prerequisites/design.md", # Rework with key concepts ], - "Step by step - Single Scale simulations" => [ - #"First Simulation" => ", + "Step by step - Single-scale simulations" => [ + "Detailed first simulation" => "./step_by_step/detailed_first_example.md", "Coupling" => "./step_by_step/simple_model_coupling.md", "Model Switching" => "./step_by_step/model_switching.md", - "Processes" => "./step_by_step/implement_a_process.md", + "Quick examples" => "./step_by_step/quick_and_dirty_examples.md", + "Implementing a process" => "./step_by_step/implement_a_process.md", "Implementing a model" => "./step_by_step/implement_a_model.md", "Parallelization" => "./step_by_step/parallelization.md", - "Advanced coupling and hard dependencies" => "./step_by_step/advanced_coupling.md" + "Advanced coupling and hard dependencies" => "./step_by_step/advanced_coupling.md", + "Implementing a model : additional notes" => "./step_by_step/implement_a_model_additional.md", ], "Execution" => "model_execution.md", "Working with data" => [ - # Quick and dirty examples "Reducing DoF" => "./working_with_data/reducing_dof.md", "Fitting" => "./working_with_data/fitting.md", "Input types" => "./working_with_data/inputs.md", "Visualizing outputs" => "./working_with_data/visualising_outputs.md" ], "Moving to multiscale" => [ + "Converting a simulation to multi-scale" => "./multiscale/single_to_multiscale.md", "Detailed example" => "./multiscale/multiscale.md", "Handling cyclic dependencies" => "./multiscale/multiscale_cyclic.md", - "Multiscale coupling considerations" => "./multiscale/multiscale_coupling.md", # TODO expand upon this + "Multiscale coupling considerations" => "./multiscale/multiscale_coupling.md", "Building a simple plant" => [ "A rudimentary plant simulation" => "./multiscale/multiscale_example_1.md", "Expanding the plant simulation" => "./multiscale/multiscale_example_2.md", @@ -72,6 +73,7 @@ makedocs(; #"developer section ?" ] ) +# move repeated examples listing to a specific page ? deploydocs(; repo="github.com/VirtualPlantLab/PlantSimEngine.jl.git", diff --git a/docs/src/multiscale/multiscale.md b/docs/src/multiscale/multiscale.md index fab474891..f979a80cd 100644 --- a/docs/src/multiscale/multiscale.md +++ b/docs/src/multiscale/multiscale.md @@ -1,12 +1,6 @@ # Multi-scale modeling -## Moving to multi-scale - -PlantSimEngine provides a framework for multi-scale modeling to seamlessly integrate models at different scales, keeping all nice functionalities provided at one scale. A nice feature is that many models do not need to be aware of the scale at which they are running, nor about the scales at which their inputs are computed, or outputs will be given, which means those models can be reused at different scales or in single-scale simulations. - -PlantSimEngine automatically computes the dependency graph between mono and multi-scale models, considering every combination of models at any scale, to determine the order of model execution. This means that the user does not need to worry about the order of model execution and can focus on the model definition and the mapping between models and scales. - -Using PlantSimEngine for multi-scale modeling is relatively easy and mostly follows the same rules as mono-scale models. Let's dive into the details with a short tutorial. +Let's look at a more advanced multi-scale example in detail. This page mostly focuses on the mapping. ## Simple mapping between models and scales diff --git a/docs/src/multiscale/multiscale_coupling.md b/docs/src/multiscale/multiscale_coupling.md index 396fd16cd..085d5844c 100644 --- a/docs/src/multiscale/multiscale_coupling.md +++ b/docs/src/multiscale/multiscale_coupling.md @@ -47,20 +47,20 @@ Note that there may be instances where you might wish to write your own model to ## Hard dependencies between models at different scale levels - If a model requires some input variable that is computed at another scale, then providing the appropriate mapping for that variable will resolve name conflicts and enable that model to run with no further steps for the user or the modeler when the coupling is a 'soft dependency'. +If a model requires some input variable that is computed at another scale, then providing the appropriate mapping for that variable will resolve name conflicts and enable that model to run with no further steps for the user or the modeler when the coupling is a 'soft dependency'. - In the case of a hard dependency that operates at the same scale as its parent, declaring the hard dependency is exactly the same as in single-scale simulations and there are also no new extra steps on the user-side. +In the case of a hard dependency that operates at the same scale as its parent, declaring the hard dependency is exactly the same as in single-scale simulations and there are also no new extra steps on the user-side. - On the other hand, modelers do need to bear in mind a couple of subtleties when developing models that possess hard dependencies that operate at a different organ level from their parent : +On the other hand, modelers do need to bear in mind a couple of subtleties when developing models that possess hard dependencies that operate at a different organ level from their parent : - The parent model directly handles the call to its hard dependency model(s), meaning they are not explicitely managed by the top-level dependency graph. +The parent model directly handles the call to its hard dependency model(s), meaning they are not explicitely managed by the top-level dependency graph. Therefore only the owning model of that dependency is visible in the graph, and its hard dependency nodes are internal. - When the caller (or any downstream model that requires some variables from the hard dependency) operates at the same scale, variables are easily accessible, and no mapping is required. +When the caller (or any downstream model that requires some variables from the hard dependency) operates at the same scale, variables are easily accessible, and no mapping is required. - If an inner model operates at a different scale/organ level, a modeler must declare hard dependencies with their respective organ level, similarly to the way the user provides a mapping. +If an inner model operates at a different scale/organ level, a modeler must declare hard dependencies with their respective organ level, similarly to the way the user provides a mapping. - Conceptually : +Conceptually : ```julia PlantSimEngine.dep(m::ParentModel) = ( @@ -68,7 +68,9 @@ Note that there may be instances where you might wish to write your own model to ) ``` - Here's a concrete example in [XPalm](https://github.com/PalmStudio/XPalm.jl), an oil palm model developed on top of PlantSimEngine. +TODO example discussed in toy plant + +Here's a concrete example in [XPalm](https://github.com/PalmStudio/XPalm.jl), an oil palm model developed on top of PlantSimEngine. Organs are produced at the phytomer scale, but need to run an age model and a biomass model at the reproductive organs' scales. ```julia diff --git a/docs/src/multiscale/single_to_multiscale.md b/docs/src/multiscale/single_to_multiscale.md index e69de29bb..51e7c0bb0 100644 --- a/docs/src/multiscale/single_to_multiscale.md +++ b/docs/src/multiscale/single_to_multiscale.md @@ -0,0 +1,160 @@ +# Moving to multi-scale simulations + +PlantSimEngine provides a framework for multi-scale modeling to integrate and couple models at different scales, retaining functionalities provided in single-scale simulations. ('Multi-scale' and 'single-scale' terminology is defined [here](TODO)) + +`ModelList` structures don't have a concept of a "scale", so are insufficient when it comes to using models which work at different plant organ levels. A similar but slightly different API is provided for multi-scale simulations. + +This section showcases how to take a single-scale `ModelList` simulation, and convert it into an equivalent multi-scale simulation (with only one provided scale in practice). This eases the transition for future full-fledged multi-scale simulation which might have multiple plant organs and operate at several scales. + +There is a more detailed discussion of mappings and scales [here](TODO). You can also find a three-part tutorial implementing an example multi-scale toy plant [here](TODO) + +## Multi-scale considerations + +Declaring and running a multi-scale simulation follows the same general workflow as the single-scale version. + +Multi-scale simulations do have some differences : they require a Multi-scale Tree Graph (MTG) and the ModelList is replaced by a slightly more complex model mapping. + +The model dependency graph will still be computed automatically, meaning users don't need to specify the order of model execution once the extra code to declare the models is written. + +Multi-scale simulations also tend to require more extra ad hoc models to prepare some variables for some models. + +### Multi-scale tree graphs + +A multi-scale simulation is implicitely expected to operate on a plant-like object. Functional-Structural Plant Models are often about simulationg plant growth. + +A multi-scale tree graph (MTG) object see TODO is therefore required to run a multi-scale simulations. It can be a dummy MTG if the simulation doesn't actually affect it, but is nevertheless a required argument to the multi-scale `run` function. + +### Mappings + +Some models are tied to a specific plant organ. + +For instance, a model computing a leaf's surface area depending on its age would operate at the "leaf" scale, and be called **for every leaf** at every timestep. On the other hand, a model computing the plant's total leaf area only needs to be run once per timestep, and can be run at the "Plant" scale. + +When users define which models they use, PlantSimEngine cannot determine in advance which scale level they operate at. This is because the plant organs in an MTG do not have standardized names, and also because some plant organs might not be part of the initial MTG, so parsing it isn't enough to infer what scales are used. + +The user therefore needs to indicate the simulation's different scales and related models. + +A mapping links models to the scale at which they operate, and is implemented as a Julia `Dict`, tying a scale, such as "Leaf" to models operating at that scale, such as "LeafSurfaceAreaModel". + +Multi-scale models can be similar models to the ones found in earlier sections, or, if they need to make use of variables at other scales, may need to be wrapped as part of a `MultiScaleModel` object. Many models are not tied to a particular scale, which means those models can be reused at different scales or in single-scale simulations. + +## Correspondence between single and multi-scale simulations + +A single-scale simulation can be turned into a 'pseudo-multi-scale' simulation by providing a simple multi-scale tree graph, and declaring a mapping linking all models to a unique scale level. + +For example, let's consider the `ModelList` coupling a light interception model, a Leaf Area Index model, and a carbon biomass increment model that was discussed [here](Further coupling) : + +```julia +meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) + +models = ModelList( + ToyLAIModel(), + Beer(0.5), + ToyRUEGrowthModel(0.2), + status=(TT_cu=cumsum(meteo_day.TT),), +) +``` + +Those models all operate on a simplified model of a single plant, without any organ-local information. We can therefore consider them to be working at the 'whole plant' scale. Their variables also operate at that "plant" scale, so there is no need to map any variable to other scales. + +We can therefore convert this into the following mapping : + +```julia +mapping = Dict( +"Plant" => ( + ToyLAIModel(), + Beer(0.5), + ToyRUEGrowthModel(0.2), + Status(TT_cu=cumsum(meteo_day.TT),) + ), +) +``` +Note the slight difference in syntax for the `Status`. This is due to an implementation quirk (sorry). + +None of these models operate on a multi-scale tree graph, either. There is no concept of organ creation or growth. We still need to provide a multi-scale tree graph to a multi-scale simulation, so we can -for now- declare a very simple MTG, with a single node : + +```julia +mtg = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Plant", 0, 0),) +``` + +## Running the multi-scale simulation + +We now have **almost** what we need to run the multiscale simulation. + +This first conversion step can be a starting point for a more elaborate multi-scale simulation. + +The signature of the `run!` function in multi-scale differs slightly from the ModelList version : + +```julia +out_multiscale = run!(mtg, mapping, meteo_day) +``` + +(Some of the optional arguments also change slightly) + +Unfortunately, there is one caveat. Passing in a vector through the `Status` is possible in multi-scale mode, but requires a little more advanced tinkering with the mapping, as it generates a custom model under the hood and the implementation is less user-friendly. + +If you are keen on going down that path, you can find a detailed example here TODO, but we don't recommend it for beginners. + +What we'll do instead, is use a ready-made model to provide the thermal time per timestep as a variable, instead of as a single vector in the `Status`. + +Our pseudo-multiscale first approach will therefore turn into a genuine multi-scale simulation. + +## Adding a second scale + +Let's have a model provide the thermal time to our Leaf Area Index model, instead of initializing it through the `Status`. + +There is a model for this purpose, `ToyDegreeDaysCumulModel`, which can also be found in the examples folder.TODO. + +This model doesn't represent a physiological process of the plant, rather an environmental process affecting its physiology. We could therefore have it operate at a different scale unrelated to the plant, which we'll call "Scene". + +The cumulated thermal time (`:TT_cu`) which was previously provided to the LAI model as a simulation parameter now needs to be mapped from the "Scene" scale level. + +This is done by wrapping our `ToyLAIModel` in a dedicated structure called a `MultiScaleModel`. A `MultiScaleModel` requires two keyword arguments : `model`, indicating the model for which some variables are mapped, and `mapped_variables`, indicating which scale link to which variables, and potentially renaming them. + +There can be different kinds of variable mapping with slightly different syntax, but in our case, only a single scalar value of the TT_cu is passed from the "Scene" to the "Plant" scale. + +This gives us the following declaration : + +```julia +MultiScaleModel( + model=ToyLAIModel(), + mapped_variables=[ + :TT_cu => "Scene", + ], + ), +``` +and the new mapping with two scales : + +```julia +mapping_multiscale = Dict( + "Scene" => ToyDegreeDaysCumulModel(), + "Plant" => ( + MultiScaleModel( + model=ToyLAIModel(), + mapped_variables=[ + :TT_cu => "Scene", + ], + ), + Beer(0.5), + ToyRUEGrowthModel(0.2), + ), +) +``` + +We can then run the multiscale simulation, with a similar dummy MTG : + +```julia +# We didn't use the previous mtg, but it is good practice to avoid unnecessarily mixing data between simulations +mtg_multiscale = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Plant", 0, 0),) +out_multiscale = run!(mtg, mapping_multiscale, meteo_day) +``` + +TODO The output structure, like the mapping, is a Julia Dict structure indexed by scale. + +#We can compare the biomass_increment with the equivalent ModelList output, and check results are identical : +#TODO slight result discrepancy + +```julia +out_dataframe_multiscale = collect(Base.Iterators.flatten(out_multiscale["Plant"][:biomass_increment])) +out_singlescale.biomass_increment +``` \ No newline at end of file diff --git a/docs/src/planned_features.md b/docs/src/planned_features.md index f82dcba37..7b02272a9 100644 --- a/docs/src/planned_features.md +++ b/docs/src/planned_features.md @@ -2,11 +2,11 @@ ## Planned major features -## Varying timesteps +### Varying timesteps Currently, all models are required to make use of the same timestep. Some physiological phenomenae within a plant tend to run on an hourly basis, others are slower. Weather data is often provided daily. Enabling different timesteps depending on the model is on the roadmap. -## Multi-plant/Multi-species simulations +### Multi-plant/Multi-species simulations A goal for PlantSimEngine down the line is to be able to simulate complex scenes with data comprising several plants, possibly of different species, for agroforestry purposes. diff --git a/docs/src/step_by_step/implement_a_model.md b/docs/src/step_by_step/implement_a_model.md index a44916b1e..91a92a6c7 100644 --- a/docs/src/step_by_step/implement_a_model.md +++ b/docs/src/step_by_step/implement_a_model.md @@ -1,4 +1,4 @@ -# [Model implementation in 5 minutes](@id model_implementation_page) +# [Implementing a model](@id model_implementation_page) ```@setup usepkg using PlantSimEngine @@ -8,7 +8,7 @@ struct Beer{T} <: AbstractLight_InterceptionModel end ``` -You'll probably want to move beyond simple usage at some point and implement your own models. +For your own simulations, you might want to move beyond simple usage at some point and implement your own models. In this section, we'll go through the required steps for writing a new model. The detailed version is tailored for people less familiar with programming. ## Quick version @@ -56,54 +56,47 @@ PlantSimEngine.ObjectDependencyTrait(::Type{<:Beer}) = PlantSimEngine.IsObjectIn PlantSimEngine.TimeStepDependencyTrait(::Type{<:Beer}) = PlantSimEngine.IsTimeStepIndependent() ``` -And that is all you need to get going, for this simple example with a single parameter and no interdependencies. +And that is all you need to get going, for this example with a single parameter and no interdependencies. The `@process` macro does some boilerplate work described [here](@ref under_the_hood) If you have more than one parameter, then type conversion utility functions might also be interesting to implement. See here TODO -If you need to deal with more complex couplings, the hard dependency section will detail +If you need to deal with more complex couplings, the hard dependency section will detail TODO ## Detailed version `PlantSimEngine.jl` was designed to make new model implementation very simple. So let's learn about how to implement your own model with a simple example: implementing a new light interception model. -## Inspiration +The model we'll (re)implement is available as an example model from the `Examples` sub-module. You can access the script from here: [`examples/Beer.jl`](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/Beer.jl). It is also available in the `PlantBioPhysics.jl` package. -If you want to implement a new model, the best way to do it is to start from another implementation. +You can import the model and PlantSimEngine's other example models into your environment with `using`: -For a complete example, you can look at the code in [`PlantBiophysics.jl`](https://github.com/VEZY/PlantBiophysics.jl), were you will find *e.g.* a photosynthesis model, with the implementation of the `FvCB` model in this Julia file: [src/photosynthesis/FvCB.jl](https://github.com/VEZY/PlantBiophysics.jl/blob/master/src/processes/photosynthesis/FvCB.jl); an energy balance model with the implementation of the `Monteith` model in [src/energy/Monteith.jl](https://github.com/VEZY/PlantBiophysics.jl/blob/master/src/processes/energy/Monteith.jl); or a stomatal conductance model in [src/conductances/stomatal/medlyn.jl](https://github.com/VEZY/PlantBiophysics.jl/blob/master/src/processes/conductances/stomatal/medlyn.jl). +```julia +# Import the example models defined in the `Examples` sub-module: +using PlantSimEngine.Examples +``` + +## Other examples -`PlantSimEngine` also provide toy models that can be used as a base to better understand how to implement a new model: +`PlantSimEngine`'s other toy models can be found in the [examples](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples) folder. -- The Beer model for light interception in [examples/Beer.jl](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/Beer.jl) -- A toy LAI development in [examples/ToyLAIModel.jl](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/ToyLAIModel.jl) +For other examples, you can look at the code in [`PlantBiophysics.jl`](https://github.com/VEZY/PlantBiophysics.jl), where you will find *e.g.* a photosynthesis model, with the implementation of the `FvCB` model in [src/photosynthesis/FvCB.jl](https://github.com/VEZY/PlantBiophysics.jl/blob/master/src/processes/photosynthesis/FvCB.jl); an energy balance model with the implementation of the `Monteith` model in [src/energy/Monteith.jl](https://github.com/VEZY/PlantBiophysics.jl/blob/master/src/processes/energy/Monteith.jl); or a stomatal conductance model in [src/conductances/stomatal/medlyn.jl](https://github.com/VEZY/PlantBiophysics.jl/blob/master/src/processes/conductances/stomatal/medlyn.jl). ## Requirements -In those files, you'll see that in order to implement a new model you'll need to implement: +If you have a look at example models, you'll see that in order to implement a new model you'll need to implement: - a structure, used to hold the parameter values and to dispatch to the right method - the actual model, developed as a method for the process it simulates - some helper functions used by the package and/or the users -If you create your own process, the function will print a short tutorial on how to do all that, adapted to the process you just created (see [Implementing a new process](@ref)). - -In this page, we'll just implement a model for a process that already exists: the light interception. This process is defined in `PlantBiophysics.jl`, and also made available as an example model from the `Examples` sub-module. You can access the script from here: [`examples/Beer.jl`](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/Beer.jl). - -We can import the model like so: - -```julia -# Import the example models defined in the `Examples` sub-module: -using PlantSimEngine.Examples -``` - -But instead of just using it, we will review the script line by line. +TODO If you create your own process, the function will print a short tutorial on how to do all that, adapted to the process you just created (see [Implementing a new process](@ref)). ## Example: the Beer-Lambert model ### The process -We declare the light interception process at l.7 using [`@process`](@ref): +We start by declaring the light interception process at l.7 using [`@process`](@ref): ```julia @process "light_interception" verbose = false @@ -126,21 +119,25 @@ struct Beer{T} <: AbstractLight_InterceptionModel end ``` -The first line defines the name of the model (`Beer`), which is completely free, except it is good practice to use camel case for the name, *i.e.* using capital letters for the words and no separator `LikeThis`. +The first line defines the name of the model (`Beer`). It is good practice to use camel case for the name, *i.e.* using capital letters for the words and no separator `LikeThis`. + +The `Beer` structure is defined as a subtype of `AbstractLight_InterceptionModel` indicating what kind of process the model simulates. The `AbstractLight_InterceptionModel` type is automatically created when defining the process "light_interception". -We also can see that we define the `Beer` structure as a subtype of `AbstractLight_InterceptionModel`. This step is very important as it tells to the package what kind of process the model simulates. `AbstractLight_InterceptionModel` is automatically created when defining the process "light_interception". +We can therefore infer from the declaration that `Beer` is a model to simulate the light interception process. -In our case, it tells us that `Beer` is a model to simulate the light interception process. +Then come the parameters names, and their types. -Then comes the parameters names, and their types. The type of parameters is given by the user at instantiation in our example. This is done using the `T` notation as follows: +### Parametric types -- we say that our structure `Beer` is a parameterized `struct` by putting `T` in between brackets after the name of the `struct` -- We put `::T` after our parameter name in the `struct`. This way Julia knows that our parameter will be of type `T`. +There is a little Julia specificity here, to enable the user to pass their own types to the simulation. -The `T` is completely free, you can use any other letter or word instead. If you have parameters that you know will be of different types, you can either force their type, or make them parameterizable too, using another letter, *e.g.*: +- `Beer` is a parameterized `struct`, indicated by the `{T}` annotation +- We indicate the `k` parameter is of type `T` by adding `::T` after the name. + +The `T` is an arbitrary letter here. If you have parameters that you know will be of different types, you can either force their type, or make them parameterizable too, using another letter, *e.g.*: ```julia -struct YourStruct{T,S} <: AbstractLight_InterceptionModel +struct CustomModel{T,S} <: AbstractLight_InterceptionModel k::T x::T y::T @@ -148,71 +145,18 @@ struct YourStruct{T,S} <: AbstractLight_InterceptionModel end ``` -Parameterized types are very useful because they let the user choose the type of the parameters, and potentially dispatch on them. - -But why not force the type ? Such as in the following example : - -```julia -struct YourStruct <: AbstractLight_InterceptionModel - k::Float64 - x::Float64 - y::Float64 - z::Int -end -``` - -Well, you can do that. But you'll lose a lot of the magic Julia has to offer this way. +Parameterized types are practical because they let the user choose the type of the parameters, and potentially change them at runtime. For example a user could use the `Particles` type from [MonteCarloMeasurements.jl](https://github.com/baggepinnen/MonteCarloMeasurements.jl) for automatic uncertainty propagation throughout the simulation. We refer you to [this page]TODO for more information on parametric types. -For example a user could use the `Particles` type from [MonteCarloMeasurements.jl](https://github.com/baggepinnen/MonteCarloMeasurements.jl) to make automatic uncertainty propagation, and this is only possible if the type is parameterizable. +### Inputs and outputs -### The method +When implementing a new model, it is necessary to declare what variables will be required, whether provided as an input to our model or computed for every timestep as an output. Input variables will either be initialized by the user in a `Status` object, or provided by another model. Output variables may be global simulation outputs and/or used by other models. -The models are implemented by adding a method for its type to the [`run!`](@ref) function. The exclamation point at the end of the function name is used in Julia to tell users that the function is mutating, *i.e.* it modifies its input. - -The function takes six arguments: - -- the type of your model -- models: a `ModelList` object, which contains all the models of the simulation -- status: a `Status` object, which contains the current values (*i.e.* state) of the variables for **one** time-step (e.g. the value of the plant LAI at time t) -- meteo: (usually) an `Atmosphere` object, or a row of the meteorological data, which contains the current values of the meteorological variables for **one** time-step (*e.g.* the value of the PAR at time t) -- constants: a `Constants` object, or a `NamedTuple`, which contains the values of the constants for the simulation (*e.g.* the value of the Stefan-Boltzmann constant) -- extras: any other object you want to pass to your model. This is for advanced users, and is not used in this example. Note that it is used to pass the `Node` when simulating a MultiScaleTreeGraph. - -Your implementation can use any variables or parameters in these objects. The only thing you have to do is to make sure that the variables you use are defined in the `Status` object, the meteorology, and the `Constants` object. - -The variables you use from the `Status` must be declared as inputs of your model. And the ones you modify must be declared as outputs. We'll that below. - -!!! warning - Models implementations are done for **one** time-step by design. The values of the previous time-step is always available in the `status` (*e.g.* `status.biomass`) as long as the variable is an output of your model. This is because at the end of a time-step, the `Status` object is recycled for the next time-step and so the latest computed values are always available. This is why it is possible to increment a value every time-step using *e.g.* `status.biomass += 1.0`. By design models don't have access to values prior to the one before. If you're not convinced by this approach, ask yourself how the plant knows the value of *e.g.* LAI from 15 days ago. It doesn't. It only knows its current state. Most of the time-sensitive variables really are just an accumulation of values until a threshold anyway. BUt if you really need to use values from the past (*e.g.* 15 time-steps before), you can add a variable to the `Status` object that is uses like a queue (see *e.g.* [DataStructures.jl](https://juliacollections.github.io/DataStructures.jl/stable/)). - -`PlantSimEngine` then automatically deals with every other detail, such as checking that the object is correctly initialized, applying the computations over objects and time-steps. This is nice because as a developer you don't have to deal with those details, and you can just concentrate on your model implementation. - -!!! warning - You need to import all the functions you want to extend, so Julia knows your intention of adding a method to the function from PlantSimEngine, and not defining your own function. To do so, you have to prefix the said functions by the package name, or import them before *e.g.*: `import PlantSimEngine: inputs_, outputs_` - -So let's do it! Here is our own implementation of the light interception for a `ModelList` component models: - -```@example usepkg -function run!(::Beer, models, status, meteo, constants, extras) - status.PPFD = - meteo.Ri_PAR_f * - exp(-models.light_interception.k * status.LAI) * - constants.J_to_umol -end -``` - -The first argument (`::Beer`) means this method will only execute when the function is called with a first argument that is of type `Beer`. This is our way of telling Julia that this method implements the `Beer` model for the light interception process. - -An important thing to note is that the model parameters are available from the `ModelList` that is passed via the `models` argument. Then parameters are found in field called by the process name, and the parameter name. For example, the `k` parameter of the `Beer` model is found in `models.light_interception.k`. - -One last thing to do is to define the inputs and outputs of our model. This is done by adding a method for the [`inputs`](@ref) and [`outputs`](@ref) functions. These functions take the type of the model as argument, and return a `NamedTuple` with the names of the variables as keys, and their default values as values. - -In our case, the `Beer` model has one input and one output: +In our case, the `Beer` model, computing light interception, has one input variable and one output variable: - Inputs: `:LAI`, the leaf area index (m² m⁻²) - Outputs: `:aPPFD`, the photosynthetic photon flux density (μmol m⁻² s⁻¹) -Here is how we communicate that to PlantSimEngine: +We declare these inputs/outputs by adding a method for the [`inputs`](@ref) and [`outputs`](@ref) functions. These functions take the type of the model as argument, and return a `NamedTuple` with the names of the variables as keys, and their default values as values: ```@example usepkg function PlantSimEngine.inputs_(::Beer) @@ -224,70 +168,50 @@ function PlantSimEngine.outputs_(::Beer) end ``` -Note that both functions end with an "\_". This is because these functions are internal, they will not be called by the users directly. Users will use [`inputs`](@ref) and [`outputs`](@ref) instead, which call `inputs_` and `outputs_`, but stripping out the default values. +These functions are internal, and end with an "\_". Users instead use [`inputs`](@ref) and [`outputs`](@ref) to query model variables. -### The utility functions +### The run! method -Before running a simulation, you can do a little bit more for your implementation (optional). - -First, you can add a method for type promotion. It wouldn't make any sense for our example because we have only one parameter. But we can make another example with a new model that would be called `Beer2` that would take two parameters: +When running a simulation with `run!`, each model is run in turn at every timestep, following whatever order was deduced from the ModelList definition and Status. Each model also has its [`run!`](@ref) method for that purpose that update the simulation's current state, with a slightly different signature. The function takes six arguments: ```julia -struct Beer2{T} <: AbstractLight_InterceptionModel - k::T - x::T -end -``` - -To add type promotion to `Beer2` we would do: - -```julia -function Beer2(k,x) - Beer2(promote(k,x)) -end -``` - -This would allow users to instantiate the model parameters using different types of inputs. For example they may use this: - -```julia -Beer2(0.6,2) +function run!(::Beer, models, status, meteo, constants, extras) ``` -You don't see a problem? Well your users won't either. But there's one: `Beer2` is a parametric type, so all fields share the same type `T`. This is the `T` in `Beer2{T}` and then in `k::T` and `x::T`. And this force the user to give all parameters with the same type. - -And in our example above, the user provides `0.6` for `k`, which is a `Float64`, and `2` for `x`, which is an `Int`. ANd if you don't have type promotion, Julia will return an error because both should be either `Float64` or `Int`. That's were the promotion comes in handy, it will convert all your inputs to a common type (when possible). In our example it will convert `2` to `2.0`. - -A second thing also is to help your user with default values for some parameters (if applicable). For example a user will almost never change the value of `k`. So we can provide a default value like so: - -```@example usepkg -Beer() = Beer(0.6) -``` +- the model's type +- models: a `ModelList` object, which contains all the models of the simulation +- status: a `Status` object, which contains the current values (*i.e.* state) of the variables for **one** time-step (e.g. the value of the plant LAI at time t) +- meteo: (usually) an `Atmosphere` object, or a row of the meteorological data, which contains the current values of the meteorological variables for **one** time-step (*e.g.* the value of the PAR at time t) +- constants: a `Constants` object, or a `NamedTuple`, which contains the values of the constants for the simulation (*e.g.* the value of the Stefan-Boltzmann constant, unit-conversion constants...) +- extras: any other object you want to pass to your model, mostly for advanced usage, not detailed here -Now the user can call `Beer` with zero value, and `k` will default to `0.6`. +A typical `run!` function can therefore make use of simulation constants, input/output variables accessible through the `Status` object, or weather data. -Another useful thing is the ability to instantiate your model type with keyword arguments, *i.e.* naming the arguments. You can do it by adding the following method: +Here is the `run!` implementation of the light interception for a `ModelList` component models. Note that the input and output variable are accessed through the `status` argument : ```@example usepkg -Beer(;k) = Beer(k) +function run!(::Beer, models, status, meteo, constants, extras) + status.PPFD = + meteo.Ri_PAR_f * + exp(-models.light_interception.k * status.LAI) * + constants.J_to_umol +end ``` -Did you notice the `;` before the argument? It tells Julia that we want those arguments provided as keywords, so now we can call `Beer` like this: - -```@example usepkg -Beer(k = 0.7) -``` +### Additional notes -This is nice when we have a lot of parameters and some with default values, but again, this is completely optional. +To use this model, users will have to make sure that the variables for that model are defined in the `Status` object, the meteorology, and the `Constants` object. -The last optional thing to implement is a method for the `eltype` function: +!!! Note + `Status` objects contain the current state of the simulation. It is not, by default, possible to make use of earlier variable states, unless a custom model is written for that purpose. -```@example usepkg -Base.eltype(x::Beer{T}) where {T} = T -``` +Model parameters are available from the `ModelList` that is passed via the `models` argument. Index by the process name, then the parameter name. For example, the `k` parameter of the `Beer` model is found in `models.light_interception.k`. -This one helps Julia know the type of the elements in the structure, and make it faster. +TODO +!!! warning + You need to import all the functions you want to extend, so Julia knows your intention of adding a method to the function from PlantSimEngine, and not defining your own function. To do so, you have to prefix the said functions by the package name, or import them before *e.g.*: `import PlantSimEngine: inputs_, outputs_` -### Traits +### Parallelization traits `PlantSimEngine` defines traits to get additional information about the models. At the moment, there are two traits implemented that help the package to know if a model can be run in parallel over space (*i.e.* objects) and/or time (*i.e.* time-steps). diff --git a/docs/src/step_by_step/implement_a_model_additional.md b/docs/src/step_by_step/implement_a_model_additional.md new file mode 100644 index 000000000..c8d894f36 --- /dev/null +++ b/docs/src/step_by_step/implement_a_model_additional.md @@ -0,0 +1,93 @@ +# Model implementation additional notes + +## Parametric types + +In [Implementing a new model](@ref), the Beer model's structure was declared with a parametric type. + +```julia +struct Beer{T} <: AbstractLight_InterceptionModel + k::T +end +``` + +Why not force the type ? Float64 is more accurate than Float32, after all: + +```julia +struct YourStruct <: AbstractLight_InterceptionModel + k::Float64 + x::Float64 + y::Float64 + z::Int +end +``` + +Doing so would lose some flexibility in the way users can make use of your models. For example a user could use the `Particles` type from [MonteCarloMeasurements.jl](https://github.com/baggepinnen/MonteCarloMeasurements.jl) for automatic uncertainty propagation, and this is only possible if the model type is parameterizable. Forcing a `Float64` type would render the model incompatible with `Particles`. + +## Type promotion + +When implementing a new model, you can do a little optional extra work to help future users. + +You can add a method for type promotion. It wouldn't make any sense for the previous `Beer` example because we have only one parameter. But we can make another example with a new model that would be called `Beer2` that would take two parameters: + +```julia +struct Beer2{T} <: AbstractLight_InterceptionModel + k::T + x::T +end +``` + +To add type promotion to `Beer2` we would do: + +```julia +function Beer2(k,x) + Beer2(promote(k,x)) +end +``` + +This would allow users to instantiate the model parameters using different types of inputs. For example users may write the following: + +```julia +Beer2(0.6,2) +``` + +`Beer2` is a parametric type, with all fields sharing the same type `T`. This is the `T` in `Beer2{T}` and then in `k::T` and `x::T`. And this forces the user to give all parameters with the same type. + +And in the example above, providin `0.6` for `k`, which is a `Float64`, and `2` for `x`, which is an `Int`. If you don't have type promotion, Julia will return an error because both should be either `Float64` or `Int`. That's were type promotion comes in handy, as it will convert all your inputs to a common type (when possible). In our case it will convert `2` to `2.0`. + +## Other helper functions and constructors + +### Default parameter values + +You can simplify model usage by helping your user with default values for some parameters (if applicable). For example, in the `Beer` model a user will almost never change the value of `k`. So we can provide a default value like so: + +```@example usepkg +Beer() = Beer(0.6) +``` + +Now the user can call `Beer` with no arguments, and `k` will default to `0.6`. + +### Parameter values as kwargs + +Another useful thing is the ability to instantiate your model type with keyword arguments, *i.e.* naming the arguments. You can do it by adding the following method: + +```@example usepkg +Beer(;k) = Beer(k) +``` + +The `;` syntax indicates that subsequent arguments are provided as keyword arguments, so now we can call `Beer` like this: + +```@example usepkg +Beer(k = 0.7) +``` + +This helps readability when there are a lot of parameters and some have default values, but again. + +### eltype + +The last optional utility function to implement is a method for the `eltype` function: + +```@example usepkg +Base.eltype(x::Beer{T}) where {T} = T +``` + +This one helps Julia know the type of the elements in the structure, and make it faster. \ No newline at end of file diff --git a/docs/src/troubleshooting_and_testing/tips_and_workarounds.md b/docs/src/troubleshooting_and_testing/tips_and_workarounds.md index 84729ec5d..43805b8e9 100644 --- a/docs/src/troubleshooting_and_testing/tips_and_workarounds.md +++ b/docs/src/troubleshooting_and_testing/tips_and_workarounds.md @@ -38,7 +38,7 @@ For example, one model in [XPalm.jl](https://github.com/PalmStudio/XPalm.jl/blob TODO use toy plant as example -## Passing in a vector in a mapping status at a specific scale +## Multiscale : passing in a vector in a mapping status at a specific scale You may have noticed that sometimes a vector (1-dimensional array) variable is passed into the `status` component of a `ModelList` in documentation examples (An example here with cumulative thermal time : [Model switching](@ref)). @@ -52,7 +52,7 @@ Due to, uh, implementation quirks, the way to use this is as follows : Call the function `replace_mapping_status_vectors_with_generated_models(mapping_with_vectors_in_status, timestep_model_organ_level, nsteps)`on your mapping. -It will parse your mapping, generate custom models to store and feed the vector values each timestep, and return the new mapping you can then use for your simulation. It also slips in a couple of internal models that provide the timestep index to these models (so note that symbols `:current_timestep` and `:next_timestep` will be declared for that mapping). You can decide which scale/organ level you want those models to be in via the `timestep_model_organ_level`parameter. `nsteps`is used as a sanity check, and expects you to provide the amount of simulation timesteps. +It will parse your mapping, generate custom models to store and feed the vector values each timestep, and return the new mapping you can then use for your simulation. It also slips in a couple of internal models that provide the timestep index to these models (so note that symbols `:current_timestep` and `:next_timestep` will be declared for that mapping). You can decide which scale/organ level you want those models to be in via the `timestep_model_organ_level`parameter. `nsteps` is used as a sanity check, and expects you to provide the amount of simulation timesteps. !!! note Only subtypes of AbstractVector present in statuses will be affected. In some cases, meteo values might need a small conversion. For instance : diff --git a/docs/src/working_with_data/visualising_outputs.md b/docs/src/working_with_data/visualising_outputs.md index 9ddda0b01..4c11167fc 100644 --- a/docs/src/working_with_data/visualising_outputs.md +++ b/docs/src/working_with_data/visualising_outputs.md @@ -90,7 +90,7 @@ TODO ## TimeStepTables and DataFrames -The output data is usually stored in a `TimeStepTable` structure defined in `PlantMeteo.jl`, which is a fast DataFrame-alike structure with each time step being a [`Status`](@ref). It can be also be any `Tables.jl` structure, such as a regular `DataFrame`. Weather data is also usually stored in a `TimeStepTable` but with each time step being an `Atmosphere`. +The output data is usually stored in a `TimeStepTable` structure defined in `PlantMeteo.jl`, which is a fast DataFrame-like structure with each time step being a [`Status`](@ref). It can be also be any `Tables.jl` structure, such as a regular `DataFrame`. Weather data is also usually stored in a `TimeStepTable` but with each time step being an `Atmosphere`. TODO example extracting specific variables diff --git a/examples/ToySingleToMultiScale.jl b/examples/ToySingleToMultiScale.jl new file mode 100644 index 000000000..a09dd4fcd --- /dev/null +++ b/examples/ToySingleToMultiScale.jl @@ -0,0 +1,85 @@ +############################## +### Example single- to multi-scale conversion +############################## + +# Environment setup +using CSV +using DataFrames +using PlantSimEngine +using PlantMeteo +using PlantSimEngine.Examples +using MultiScaleTreeGraph + +# Weather data for all simulations +meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) + +# Single-scale simulation +models_singlescale = ModelList( + ToyLAIModel(), + Beer(0.5), + ToyRUEGrowthModel(0.2), + status=(TT_cu=cumsum(meteo_day.TT),), +) + +out_singlescale = run!(models_singlescale, meteo_day) + +# Direct translation of the single-scale simulation +mapping_pseudo_multiscale = Dict( +"Plant" => ( + ToyLAIModel(), + Beer(0.5), + ToyRUEGrowthModel(0.2), + Status(TT_cu=cumsum(meteo_day.TT),) + ), +) + +mtg = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Plant", 1, 0),) +#plant = MultiScaleTreeGraph.Node(mtg, MultiScaleTreeGraph.NodeMTG("+", "Plant", 1, 1)) + +# will generate an error as vectors can't be directly passed into a Status in multi-scale simulations +out_pseudo_multiscale = run!(mtg, mapping_pseudo_multiscale, meteo_day) + + +# TODO This seems to have a bug, generates an error +mapping_2 = PlantSimEngine.replace_mapping_status_vectors_with_generated_models(mapping_pseudo_multiscale, "Plant", PlantSimEngine.get_nsteps(meteo_day)) +#=new_status, generated_models = PlantSimEngine.generate_model_from_status_vector_variable(mapping_pseudo_multiscale, "Plant", Status(TT_cu=cumsum(meteo_day.TT)), "Plant", PlantSimEngine.get_nsteps(meteo_day)) +mapping_pseudo_multiscale_adjusted = Dict("Plant" => ( + ToyLAIModel(), + Beer(0.5), + ToyRUEGrowthModel(0.2), generated_models..., + PlantSimEngine.HelperNextTimestepModel(), + MultiScaleModel( + model=PlantSimEngine.HelperCurrentTimestepModel(), + mapped_variables=[PreviousTimeStep(:next_timestep),], + ), + new_status, +), +) +out_pseudo_multiscale = run!(mtg, mapping_pseudo_multiscale_adjusted, meteo_day) +=# + + + +# Actual multiscale version of the single-scale simulation + +mapping_multiscale = Dict( + "Scene" => ToyDegreeDaysCumulModel(), + "Plant" => ( + MultiScaleModel( + model=ToyLAIModel(), + mapped_variables=[ + :TT_cu => "Scene", + ], + ), + Beer(0.5), + ToyRUEGrowthModel(0.2), + ), +) + +# The previous mtg wasn't affected, but it is good practice to avoid unnecessarily mixing data between simulations +mtg_multiscale = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Plant", 0, 0),) +out_multiscale = run!(mtg, mapping_multiscale, meteo_day) + + +#out_dataframe_multiscale = collect(Base.Iterators.flatten(out_multiscale["Plant"][:TT_cu])) +#out_singlescale.TT_cu \ No newline at end of file From 86f534c39fa7bb701b0607cee160eb46522eb714 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Wed, 5 Mar 2025 11:11:57 +0100 Subject: [PATCH 075/147] Added a doc page describing multiscale differences from single-scale. And more edits for a more detailed discussion of conversion from single-to multi-scale --- docs/make.jl | 1 + docs/src/multiscale/multiscale.md | 2 +- .../multiscale/multiscale_considerations.md | 64 ++++++++ docs/src/multiscale/multiscale_example_1.md | 25 +-- docs/src/multiscale/single_to_multiscale.md | 151 +++++++++++------- examples/ToySingleToMultiScale.jl | 66 +++++++- 6 files changed, 235 insertions(+), 74 deletions(-) create mode 100644 docs/src/multiscale/multiscale_considerations.md diff --git a/docs/make.jl b/docs/make.jl index 2602b5ba1..d1416bc3b 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -52,6 +52,7 @@ makedocs(; "Visualizing outputs" => "./working_with_data/visualising_outputs.md" ], "Moving to multiscale" => [ + "Multiscale considerations" => "./multiscale/multiscale_considerations.md", "Converting a simulation to multi-scale" => "./multiscale/single_to_multiscale.md", "Detailed example" => "./multiscale/multiscale.md", "Handling cyclic dependencies" => "./multiscale/multiscale_cyclic.md", diff --git a/docs/src/multiscale/multiscale.md b/docs/src/multiscale/multiscale.md index f979a80cd..7da2397f0 100644 --- a/docs/src/multiscale/multiscale.md +++ b/docs/src/multiscale/multiscale.md @@ -1,6 +1,6 @@ # Multi-scale modeling -Let's look at a more advanced multi-scale example in detail. This page mostly focuses on the mapping. +Let's look at a more advanced multi-scale example in detail. This page mostly focuses on mapping subtleties. ## Simple mapping between models and scales diff --git a/docs/src/multiscale/multiscale_considerations.md b/docs/src/multiscale/multiscale_considerations.md new file mode 100644 index 000000000..a949ef4c3 --- /dev/null +++ b/docs/src/multiscale/multiscale_considerations.md @@ -0,0 +1,64 @@ +# Multi-scale considerations + +This section briefly details the subtle ways in which multi-scale simulations differ from prior single-scale simulations. The next few pages will showcase some of these subtleties with examples. + +Declaring and running a multi-scale simulation follows the same general workflow as the single-scale version, but multi-scale simulations do have some differences : + +- a simulation requires a Multi-scale Tree Graph (MTG) to run and operates on that graph +- when running, models are tied to a scale and only access local information +- models can run multiple times per timestep, +- the `ModelList` is replaced by a slightly more complex model mapping to link models to the scale they will operate at. + +The simulation dependency graph will still be computed automatically and handle most couplings, meaning users don't need to specify the order of model execution once the extra code to declare the models is written. You will still need to declare hard dependencies, with extra considerations for multi-scale hard dependencies. + +Multi-scale simulations also tend to require more extra ad hoc models to prepare some variables for some models. + +TODO link to other pages + +## Multi-scale tree graphs + +Functional-Structural Plant Models are often about simulating plant growth. A multi-scale simulation is implicitely expected to operate on a plant-like object, represented by a multi-scale tree graph. + +A multi-scale tree graph (MTG) object (see TODO) is therefore required to run a multi-scale simulations. It can be a dummy MTG if the simulation doesn't actually affect it, but is nevertheless a required argument to the multi-scale `run` function. + +## Models run once per organ instance, not once per organ level + +Some models, like the ones we've seen in single-scale simulations, work on a very simple model of a whole plant. + +More fine-grained models can be tied to a specific plant organ. + +For instance, a model computing a leaf's surface area depending on its age would operate at the "leaf" scale, and be called **for every leaf** at every timestep. On the other hand, a model computing the plant's total leaf area only needs to be run once per timestep, and can be run at the "Plant" scale. + +This is a major difference between a single-scale simulation and a multi-scale one. By default, any model in a single-scale simulation will only run **once** per timestep. However, in multi-scale, if a plant has several instances of an organ type -say it has a hundred leaves- then any model operating at the "Leaf" scale will by default run one hundred times per timestep, unless it is explicitely controlled by another model (which can happen in hard dependency configurations). + +## Mappings + +When users define which models they use, PlantSimEngine cannot determine in advance which scale level they operate at. This is partly because the plant organs in an MTG do not have standardized names, and partly because some plant organs might not be part of the initial MTG, so parsing it isn't enough to infer what scales are used. + +The user therefore needs to indicate for a simulation's which models are related to which scale. + +A multi-scale mapping links models to the scale at which they operate, and is implemented as a Julia `Dict`, tying a scale, such as "Leaf" to models operating at that scale, such as "LeafSurfaceAreaModel". It is the equivalent of a `ModelList` in a single-scale simulation. + +Multi-scale models can be similar models to the ones found in earlier sections, or, if they need to make use of variables at other scales, may need to be wrapped as part of a `MultiScaleModel` object. Many models are not tied to a particular scale, which means those models can be reused at different scales or in single-scale simulations. + +## The simulation operates on an MTG + +Unlike in single-scale simulations, which make use of a `Status` object to store the current state of every variable in a simulation, multi-scale simulations operate on a per-organ basis. + +This means every organ instance has its own `Status`, with scale-specific attributes. + +This has two **important** consequences in terms of running a simulation : + +- First, **any scale absent from the MTG will not be run**. If your MTG contains no leaves, then no model operating at the scale "Leaf" will be able to run until a "Leaf" organ is created and a node is added in the MTG. Otherwise, it has no MTG node to operate on. The only exceptions are hard dependency models which can be called from a different scale. TODO example + +- Secondly, models only have access to **local** organ information. The `status` argument in the `run!` function only contains variables **at the model's scale**, unless variables from other scales are mapped via a `MultiScaleModel` wrapping. + +## Outputs + +The output structure, like the mapping, is a Julia `Dict` structure indexed by scale. TODO node + +Multi-scale simulations, especially for plants which have thousands of leaves, internodes, root branches, buds and fruits, may compute huge amounts of data. Just like in single-scale simulations, it is possible to keep only variables whose values you want to track for every timestep, and filter the rest out. Again, those tracked variables need to be indexed by scale, see the following example : + +```julia +TODO +``` \ No newline at end of file diff --git a/docs/src/multiscale/multiscale_example_1.md b/docs/src/multiscale/multiscale_example_1.md index 2676968ae..ef4102d8a 100644 --- a/docs/src/multiscale/multiscale_example_1.md +++ b/docs/src/multiscale/multiscale_example_1.md @@ -2,25 +2,32 @@ TODO change Toy To Example ? -This section iteratively walks you through building a multi-scale simulation. +This three-part section walks you through building a multi-scale simulation from scratch. It is meant as an illustration of the iterative process you might go through when building and slowly tuning a Functional-Structural Plant Model, where previous multi-scale examples focused more on the API syntax. -The actual plant being simulated, as well as some of the ad hoc processes, mostly have no physical meaning and are very much ad hoc (which is why most of them aren't standalone in the examples folder). Similarly, some of the parameter values are pulled out of thin air, and have no ties to research papers or data. +You can find the full script for the first part's toy simulation in the [ToyMultiScalePlantModel](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/ToyMultiScalePlantModel/ToyPlantSimulation1.jl) subfolder of the examples folder. -The main purpose here is to showcase PlantSimEngine's multi-scale features and how to structure your models, not accuracy, realism or performance. +## Disclaimer + +The actual plant being created, as well as some of the custom models, have no real physical meaning and are very much ad hoc (which is why most of them aren't standalone in the examples folder). Similarly, some of the parameter values are pulled out of thin air, and have no ties to research papers or data. -You can find the full script for this simulation in the [ToyMultiScalePlantModel](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/ToyMultiScalePlantModel/ToyPlantSimulation1.jl) subfolder of the examples folder. +The main purpose here is to showcase PlantSimEngine's multi-scale features and how to structure your models, not accuracy, realism or performance. ## A basic growing plant -At minimul, to simulate some kind of fake growth, we need : +At minimum, to simulate some kind of fake growth, we need : -- A MultiScale Tree Graph representing the plant +- A Multi-scale Tree Graph representing the plant - Some way of adding organs to the plant -- Some kind of temporality to this dynamic +- Some kind of temporality to spread this growth over multiple timesteps Let's have some concept of 'leaves' that capture the (carbon) resource necessary for organ growth, and let's have the organ emergence happen at the 'internode' level, to illustrate multiple organs with different behavior. -We'll make the assumption the internodes make use of carbon from a common pool. We'll also make use of thermal time as a growth delay factor. +We'll make the assumption that the internodes make use of carbon from a common pool. We'll also make use of thermal time as a growth delay factor. + +To sum up : +- MTG with growing internodes and leaves +- Individual leaves capture carbon fed into a common pool +- Internodes take from that pool to create new organs, with a thermal time constraint. One way of modeling this approach translates into several scales and models : @@ -48,7 +55,7 @@ Some of the models will need to gather variables from scales other than their ow ### Carbon Capture -Let's start with the simplest model. Leaves continuously capture some constant amount of carbon every timestep. No inputs are required. +Let's start with the simplest model. Our fake leaves will continuously capture some constant amount of carbon every timestep. No inputs or parameters are required. ```julia PlantSimEngine.@process "leaf_carbon_capture" verbose = false diff --git a/docs/src/multiscale/single_to_multiscale.md b/docs/src/multiscale/single_to_multiscale.md index 51e7c0bb0..322eb0a32 100644 --- a/docs/src/multiscale/single_to_multiscale.md +++ b/docs/src/multiscale/single_to_multiscale.md @@ -1,46 +1,12 @@ -# Moving to multi-scale simulations +# Converting a single-scale simulation to multi-scale -PlantSimEngine provides a framework for multi-scale modeling to integrate and couple models at different scales, retaining functionalities provided in single-scale simulations. ('Multi-scale' and 'single-scale' terminology is defined [here](TODO)) - -`ModelList` structures don't have a concept of a "scale", so are insufficient when it comes to using models which work at different plant organ levels. A similar but slightly different API is provided for multi-scale simulations. - -This section showcases how to take a single-scale `ModelList` simulation, and convert it into an equivalent multi-scale simulation (with only one provided scale in practice). This eases the transition for future full-fledged multi-scale simulation which might have multiple plant organs and operate at several scales. - -There is a more detailed discussion of mappings and scales [here](TODO). You can also find a three-part tutorial implementing an example multi-scale toy plant [here](TODO) - -## Multi-scale considerations - -Declaring and running a multi-scale simulation follows the same general workflow as the single-scale version. - -Multi-scale simulations do have some differences : they require a Multi-scale Tree Graph (MTG) and the ModelList is replaced by a slightly more complex model mapping. - -The model dependency graph will still be computed automatically, meaning users don't need to specify the order of model execution once the extra code to declare the models is written. - -Multi-scale simulations also tend to require more extra ad hoc models to prepare some variables for some models. - -### Multi-scale tree graphs - -A multi-scale simulation is implicitely expected to operate on a plant-like object. Functional-Structural Plant Models are often about simulationg plant growth. - -A multi-scale tree graph (MTG) object see TODO is therefore required to run a multi-scale simulations. It can be a dummy MTG if the simulation doesn't actually affect it, but is nevertheless a required argument to the multi-scale `run` function. - -### Mappings - -Some models are tied to a specific plant organ. - -For instance, a model computing a leaf's surface area depending on its age would operate at the "leaf" scale, and be called **for every leaf** at every timestep. On the other hand, a model computing the plant's total leaf area only needs to be run once per timestep, and can be run at the "Plant" scale. - -When users define which models they use, PlantSimEngine cannot determine in advance which scale level they operate at. This is because the plant organs in an MTG do not have standardized names, and also because some plant organs might not be part of the initial MTG, so parsing it isn't enough to infer what scales are used. - -The user therefore needs to indicate the simulation's different scales and related models. - -A mapping links models to the scale at which they operate, and is implemented as a Julia `Dict`, tying a scale, such as "Leaf" to models operating at that scale, such as "LeafSurfaceAreaModel". +A single-scale simulation can be turned into a 'pseudo-multi-scale' simulation by providing a simple multi-scale tree graph, and declaring a mapping linking all models to a unique scale level. -Multi-scale models can be similar models to the ones found in earlier sections, or, if they need to make use of variables at other scales, may need to be wrapped as part of a `MultiScaleModel` object. Many models are not tied to a particular scale, which means those models can be reused at different scales or in single-scale simulations. +This section showcases the conversion, and then adds a model at a new scale to make the simulation genuinely multi-scale. -## Correspondence between single and multi-scale simulations +The full script for this section can be found in TODO -A single-scale simulation can be turned into a 'pseudo-multi-scale' simulation by providing a simple multi-scale tree graph, and declaring a mapping linking all models to a unique scale level. +# Converting the ModelList to a multi-scale mapping For example, let's consider the `ModelList` coupling a light interception model, a Leaf Area Index model, and a carbon biomass increment model that was discussed [here](Further coupling) : @@ -91,21 +57,61 @@ out_multiscale = run!(mtg, mapping, meteo_day) (Some of the optional arguments also change slightly) -Unfortunately, there is one caveat. Passing in a vector through the `Status` is possible in multi-scale mode, but requires a little more advanced tinkering with the mapping, as it generates a custom model under the hood and the implementation is less user-friendly. +Unfortunately, there is one caveat. Passing in a vector through the `Status` is still possible in multi-scale mode, but requires a little more advanced tinkering with the mapping, as it generates a custom model under the hood and the implementation is less user-friendly. If you are keen on going down that path, you can find a detailed example here TODO, but we don't recommend it for beginners. -What we'll do instead, is use a ready-made model to provide the thermal time per timestep as a variable, instead of as a single vector in the `Status`. +What we'll do instead, is write our own model provide the thermal time per timestep as a variable, instead of as a single vector in the `Status`. -Our pseudo-multiscale first approach will therefore turn into a genuine multi-scale simulation. +Our 'pseudo-multiscale' first approach will therefore turn into a genuine multi-scale simulation. ## Adding a second scale -Let's have a model provide the thermal time to our Leaf Area Index model, instead of initializing it through the `Status`. +Let's have a model provide the Cumulated Thermal Time to our Leaf Area Index model, instead of initializing it through the `Status`. + +Let's instead implement our own `ToyTT_cuModel`. + +### TT_cu model implementation + +This model doesn't require any outside data or input variables, it only operates on the weather data and outputs our desired TT_cu. The implementation doesn't require any advanced coupling and is very straightforward. + +```julia +PlantSimEngine.@process "tt_cu" verbose = false + +struct ToyTt_CuModel <: AbstractTt_CuModel +end + +function PlantSimEngine.run!(::ToyTt_CuModel, models, status, meteo, constants, extra=nothing) + status.TT_cu += + meteo.TT +end + +function PlantSimEngine.inputs_(::ToyTt_CuModel) + NamedTuple() # No input variables +end + +function PlantSimEngine.outputs_(::ToyTt_CuModel) + (TT_cu=-Inf,) +end +``` + +!!! note + The only accessible variables in the `run!` function via the status are the ones that are local to the "Scene" scale. This isn't explicit at first glance, but very important to keep in mind when developing models, or using them at different scales. If variables from other scales are required, then they need to be mapped via a `MultiScaleModel`, or sometimes a more complex coupling is necessary. + +### Linking the new TT_cu model to a scale in the mapping + +We now have our model implementation. How does it fit into our mapping ? + +Our new model doesn't really relate to a specific organ of our plant. In fact, this model doesn't represent a physiological process of the plant, but rather an environmental process affecting its physiology. We could therefore have it operate at a different scale unrelated to the plant, which we'll call "Scene". This makes sense. -There is a model for this purpose, `ToyDegreeDaysCumulModel`, which can also be found in the examples folder.TODO. +Note that we now need to add a "Scene" node to our Multi-scale Tree Graph, otherwise our model will not run, since no other model calls it. See []TODO + +```julia +mtg_multiscale = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Plant", 0, 0),) + plant = MultiScaleTreeGraph.Node(mtg_multiscale, MultiScaleTreeGraph.NodeMTG("+", "Plant", 1, 1)) +``` -This model doesn't represent a physiological process of the plant, rather an environmental process affecting its physiology. We could therefore have it operate at a different scale unrelated to the plant, which we'll call "Scene". +### Mapping between scales : the MultiScaleModel wrapper The cumulated thermal time (`:TT_cu`) which was previously provided to the LAI model as a simulation parameter now needs to be mapped from the "Scene" scale level. @@ -141,20 +147,53 @@ mapping_multiscale = Dict( ) ``` -We can then run the multiscale simulation, with a similar dummy MTG : +We can then run the multiscale simulation, with our two-node MTG : ```julia -# We didn't use the previous mtg, but it is good practice to avoid unnecessarily mixing data between simulations -mtg_multiscale = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Plant", 0, 0),) -out_multiscale = run!(mtg, mapping_multiscale, meteo_day) +out_multiscale = run!(mtg_multiscale, mapping_multiscale, meteo_day) ``` -TODO The output structure, like the mapping, is a Julia Dict structure indexed by scale. +### Comparing outputs between single- and multi-scale -#We can compare the biomass_increment with the equivalent ModelList output, and check results are identical : -#TODO slight result discrepancy +The outputs structures are slightly different : multi-scale outputs are indexed by scale, and a variable has a value for every node of the scale it operates at (for instance, there would be a "leaf_surface" value for every leaf in a plant), stored in an array. -```julia -out_dataframe_multiscale = collect(Base.Iterators.flatten(out_multiscale["Plant"][:biomass_increment])) -out_singlescale.biomass_increment -``` \ No newline at end of file +In our simple example, we only have one MTG scene node and one plant node, so the arrays for each variable in the multi-scale output only contain one value. + +We can access the output variables at the "Scene" scale by indexing our outputs: + +```julia +outputs_multiscale["Scene"] +``` +and then the computed `:TT_cu`: +```julia +outputs_multiscale["Scene"][:TT_cu] +``` + +As you can see, it is a `Vector{Vector{T}}`, whereas our single-scale output is a `Vector{T}`: +```julia +outputs_singlescale.TT_cu +``` + +To compare them value-by-value, we can flatten the multiscale Vector and then do a piecewise approximate equality test : +```julia +computed_TT_cu_multiscale = collect(Base.Iterators.flatten(outputs_multiscale["Scene"][:TT_cu])) + +for i in 1:length(computed_TT_cu_multiscale) + if !(computed_TT_cu_multiscale[i] ≈ outputs_singlescale.TT_cu[i]) + println(i) + end +end +``` +or equivalently, with broadcasting, we can write : +```julia +is_approx_equal = length(unique(multiscale_TT_cu .≈ out_singlescale.TT_cu)) == 1 +``` + +!!! note + You may be wondering why we check for approximate equality rather than strict equality. The reason for that is due to floating-point accumulation errors, which are discussed in more detail [here]TODO. + +## ToyDegreeDaysCumulModel + +There is a model able to provide Thermal Time based on weather temperature data, `ToyDegreeDaysCumulModel`, which can also be found in the examples folder. + +We didn't make use of it here for learning purposes. It also computes a thermal time based on default parameters that don't correspond to the thermal time in the example weather data, so results differ from the thermal time already present in the weather data without tinkering with the parameters. \ No newline at end of file diff --git a/examples/ToySingleToMultiScale.jl b/examples/ToySingleToMultiScale.jl index a09dd4fcd..6f18805ea 100644 --- a/examples/ToySingleToMultiScale.jl +++ b/examples/ToySingleToMultiScale.jl @@ -13,7 +13,10 @@ using MultiScaleTreeGraph # Weather data for all simulations meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) -# Single-scale simulation +############################## +### Single-scale simulation +############################## + models_singlescale = ModelList( ToyLAIModel(), Beer(0.5), @@ -23,7 +26,9 @@ models_singlescale = ModelList( out_singlescale = run!(models_singlescale, meteo_day) -# Direct translation of the single-scale simulation +############################## +#### Direct translation of the single-scale simulation +############################## mapping_pseudo_multiscale = Dict( "Plant" => ( ToyLAIModel(), @@ -58,12 +63,37 @@ mapping_pseudo_multiscale_adjusted = Dict("Plant" => ( out_pseudo_multiscale = run!(mtg, mapping_pseudo_multiscale_adjusted, meteo_day) =# +############################## +#### Ad Hoc Cumulated Thermal Time Model +############################## + +PlantSimEngine.@process "tt_cu" verbose = false + +struct ToyTt_CuModel <: AbstractTt_CuModel +end + +function PlantSimEngine.run!(::ToyTt_CuModel, models, status, meteo, constants, extra=nothing) + status.TT_cu += + meteo.TT +end + +function PlantSimEngine.inputs_(::ToyTt_CuModel) + NamedTuple() +end +function PlantSimEngine.outputs_(::ToyTt_CuModel) + (TT_cu=-Inf,) +end -# Actual multiscale version of the single-scale simulation +############################## +#### Actual multiscale version of the single-scale simulation +############################## mapping_multiscale = Dict( - "Scene" => ToyDegreeDaysCumulModel(), + "Scene" => ( + ToyTt_CuModel(), + Status(TT_cu=0.0), + ), "Plant" => ( MultiScaleModel( model=ToyLAIModel(), @@ -77,9 +107,29 @@ mapping_multiscale = Dict( ) # The previous mtg wasn't affected, but it is good practice to avoid unnecessarily mixing data between simulations -mtg_multiscale = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Plant", 0, 0),) -out_multiscale = run!(mtg, mapping_multiscale, meteo_day) +mtg_multiscale = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Scene", 1, 0)) + plant = MultiScaleTreeGraph.Node(mtg_multiscale, MultiScaleTreeGraph.NodeMTG("+", "Plant", 1, 1)) +out_multiscale = run!(mtg_multiscale, mapping_multiscale, meteo_day) + +############################## +#### Output comparison +############################## + +computed_TT_cu_multiscale = collect(Base.Iterators.flatten(outputs_multiscale["Scene"][:TT_cu])) + +is_approx_equal_1 = true + +for i in 1:length(computed_TT_cu_multiscale) + if !(computed_TT_cu_multiscale[i] ≈ outputs_singlescale.TT_cu[i]) + is_approx_equal_1 = false + break + end +end + +is_approx_equal_1 + +is_approx_equal_2 = length(unique(multiscale_TT_cu .≈ out_singlescale.TT_cu)) == 1 -#out_dataframe_multiscale = collect(Base.Iterators.flatten(out_multiscale["Plant"][:TT_cu])) -#out_singlescale.TT_cu \ No newline at end of file +# Note : it is also possible to get the weather data length via PlantSimEngine.get_nsteps(meteo_day) +# instead of checking for array length \ No newline at end of file From 2631878c1cf393cd599a51963a3288537d0fe8b1 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Wed, 5 Mar 2025 16:23:42 +0100 Subject: [PATCH 076/147] Add a page for fp summation approximation errors. Edit the already existing multiscale example to make it less verbose. Typo correction. Another error documented. Minor edits. --- docs/make.jl | 2 +- docs/src/multiscale/multiscale.md | 104 ++++++++-------- docs/src/multiscale/multiscale_example_2.md | 2 +- docs/src/multiscale/single_to_multiscale.md | 26 +++- ...lantsimengine_and_julia_troubleshooting.md | 47 ++++++++ .../floating_point_accumulation_error.md | 112 ++++++++++++++++++ 6 files changed, 238 insertions(+), 55 deletions(-) create mode 100644 docs/src/working_with_data/floating_point_accumulation_error.md diff --git a/docs/make.jl b/docs/make.jl index d1416bc3b..1c1770177 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -54,7 +54,7 @@ makedocs(; "Moving to multiscale" => [ "Multiscale considerations" => "./multiscale/multiscale_considerations.md", "Converting a simulation to multi-scale" => "./multiscale/single_to_multiscale.md", - "Detailed example" => "./multiscale/multiscale.md", + "More variable mapping examples" => "./multiscale/multiscale.md", "Handling cyclic dependencies" => "./multiscale/multiscale_cyclic.md", "Multiscale coupling considerations" => "./multiscale/multiscale_coupling.md", "Building a simple plant" => [ diff --git a/docs/src/multiscale/multiscale.md b/docs/src/multiscale/multiscale.md index 7da2397f0..9ea3b307a 100644 --- a/docs/src/multiscale/multiscale.md +++ b/docs/src/multiscale/multiscale.md @@ -1,38 +1,36 @@ -# Multi-scale modeling +# Multi-scale variable mapping -Let's look at a more advanced multi-scale example in detail. This page mostly focuses on mapping subtleties. +The previous section showed how to convert a single-scale simulation to multi-scale. -## Simple mapping between models and scales +This section provides another example showcasing the nuances in variable mapping, with a more complex fully multiscale version of a prior simulation. The models will all be taken form the examples folder []TODO -To get started, we have to define a mapping between models and scales. +## Starting with a single-model mapping -Let's import the `PlantSimEngine` package and example models we will use in this tutorial: +Let's import the `PlantSimEngine` package and all the example models we will use in this tutorial: ```@example usepkg using PlantSimEngine using PlantSimEngine.Examples # Import some example models ``` -!!! note - The `Examples` submodule exports a few simple models we will use in this tutorial. The models are also found in the `examples` folder of the package. +Let's create a simple mapping with only one initial model, the carbon assimilation process `ToyAssimModel`. +It was previously used in a single-scale simulation TODO, but we will have it be used in a more fine-grained manner and operate on leaves in this example. -We now have access to models for the simulation of different processes. We can associate each model with a scale by defining a mapping between models and scales. The mapping is a dictionary with the name of the scale as the key and the model as the value. For example, we can define a mapping to simulate the assimilation process at the leaf scale with `ToyAssimModel` as follows: +Our mapping between scale and model is therefore: ```@example usepkg mapping = Dict("Leaf" => ToyAssimModel()) ``` -In this example, the dictionary's key is the name of the scale (`"Leaf"`), and the value is the model. The model is an example model provided by `PlantSimEngine`, so we must prefix it with the module name. - -We can check if the mapping is valid by calling `to_initialize`: +Just like in single-scale simulations, we can call `to_initialize` to check whether variables need to be initialised. It will this time index by scale: ```@example usepkg to_initialize(mapping) ``` -The `to_initialize` function checks if models from any scale need further initialization before simulation. This is the case when some input variables of the model are not computed by another model. In this example, the `ToyAssimModel` needs `:aPPFD` and `:soil_water_content` as inputs. To run a simulation, we must provide a value for the variables or a model that simulates them. +In this example, the `ToyAssimModel` needs `:aPPFD` and `:soil_water_content` as inputs, which aren't initialised in our mapping. -The initialization values for the variables can be provided using the `Status` type along with the model, *e.g.*: +The initialization values for the variables can be passed along via a `Status` object: ```@example usepkg mapping = Dict( @@ -43,10 +41,7 @@ mapping = Dict( ) ``` -!!! note - The model and the `Status` are provided as a `Tuple` to the `"Leaf"` scale. - -If we re-execute `to_initialize`, we get an empty dictionary, meaning the mapping is valid, and we can start the simulation: +If we call `to_initialize` on this new mapping, it returns an empty dictionary, meaning the mapping is valid, and we can start the simulation: ```@example usepkg to_initialize(mapping) @@ -54,8 +49,11 @@ to_initialize(mapping) ## Multiscale mapping between models and scales -In our previous example, we provided the value for the `soil_water_content` variable. However, we could also provide a model that simulates it at the soil scale. The only difference now is that we have to tell PlantSimEngine that our -`ToyAssimModel` is now multiscale and takes the `soil_water_content` variable from the `"Soil"` scale. We can do that by wrapping the `ToyAssimModel` in a `MultiScaleModel`: +The `soil_water_content` variable was provided via the mapping. No model affects it, so it is constant in the above example. We could instead provide a model that computes it based on weather data, and/or a more realistic physical process. + +It also makes sense to have that model operate at a different scale than the "Leaf" scale. There is a dummy soil model called `ToySoilModel` in the examples folder. Let's put it at a new "Soil" scale level. + +`ToyAssimModel` is now makes use of the `soil_water_content` variable from the `"Soil"` scale, instead of at its own scale via the `Status` initialization. We therefore need to map `soil_water_content` from the "Soil" to the "Leaf" scale by wrapping `ToyAssimModel` in a `MultiScaleModel`: ```@example usepkg mapping = Dict( @@ -71,10 +69,9 @@ mapping = Dict( nothing # hide ``` -The `MultiScaleModel` takes two arguments: the model and the mapping between the model and the scales. The mapping is a vector of pairs of pairs mapping the variable's name with the name of the scale its value comes from, and the name of the variable at that scale. In this example, we map the `soil_water_content` variable at scale "Leaf" to the `soil_water_content` variable at the `"Soil"` scale. If the name of the variable is the same between both scales, we can omit the variable name at the origin scale, *e.g.* `[:soil_water_content => "Soil"]`. +In this example, we map the `soil_water_content` variable at scale "Leaf" to the `soil_water_content` variable at the `"Soil"` scale. If the name of the variable is the same between both scales, we can omit the variable name at the origin scale, *e.g.* `[:soil_water_content => "Soil"]`. -!!! note - The variable `aPPFD` is still provided in the `Status` type as a constant value. +The variable `aPPFD` is still provided in the `Status` type as a constant value. We can check again if the mapping is valid by calling `to_initialize`: @@ -82,15 +79,11 @@ We can check again if the mapping is valid by calling `to_initialize`: to_initialize(mapping) ``` -`to_initialize` returns an empty dictionary, meaning the mapping is valid. - -## More on MultiScaleModel +Once again, `to_initialize` returns an empty dictionary, meaning the mapping is valid. -`MultiScaleModel` is a wrapper around a model that allows it to take inputs or give outputs from other scales. It takes two arguments: the model and the mapping between the model and the scales. The mapping is a vector of pairs of pairs mapping the variable's name with the name of the scale its value comes from, and its name at that scale. +## A more elaborate multiscale model mapping -The variable can map a single value if there is only one node to map to or a vector of values if there are several. It can also map to several types of nodes at the same time. - -Let's take a look at a more complex example of a mapping: +Let's now expand this mapping, to showcase other ways in which variables can be mapped from one scale to another. We'll keep the first two models, and add several more to simulate a couple of other processes within our plant. ```@example usepkg mapping = Dict( @@ -143,24 +136,45 @@ mapping = Dict( nothing # hide ``` -In this example, we expect to make a simulation at five different scales: `"Scene"`, `"Plant"`, `"Internode"`, `"Leaf"`, and `"Soil"`. The `"Scene"` scale represents the whole scene, where one or several plants can live. The `"Plant"` scale is, well, the whole plant scale, `"Internode"` and `"Leaf"` are organ scales, and `"Soil"` is the soil scale. This mapping is used to compute the carbon allocation (`ToyCAllocationModel`) to the different organs of the plant (`"Leaf"` and `"Internode"`) from the assimilation at the `"Leaf"` scale (*i.e.* the offer) and their carbon demand (`ToyCDemandModel`). The `"Soil"` scale is used to compute the soil water content (`ToySoilWaterModel`), which is needed to calculate the assimilation at the `"Leaf"` scale (`ToyAssimModel`). We also can note that we compute the maintenance respiration at the `"Leaf"` and `"Internode"` scales (`ToyMaintenanceRespirationModel`), which is summed up to compute the total maintenance respiration at the `"Plant"` scale (`ToyPlantRmModel`). +This mapping might seem a little more daunting than previous examples, but several models should be recognizable in passing. In fact, you can consider this mapping to be an enhanced and more complex multi-scale version of a previous single-scale example. -We see that all scales are interconnected, with computations at the organ scale that may depend on the soil scale and at the plant scale that depends on the organ scale and scene scale. +TODO link +```julia +models2 = ModelList( + ToyLAIModel(), + Beer(0.5), + ToyAssimGrowthModel(), + status=(TT_cu=cumsum(meteo_day.TT),), +) +``` -Something important to note here is that we have different ways to define the mapping for the `MultiScaleModel`. For example, we have `:carbon_assimilation => ["Leaf"]` at the plant scale for `ToyCAllocationModel`. This mapping means that the variable `carbon_assimilation` is mapped to the `"Leaf"` scale. However, we could also have `:carbon_assimilation => "Leaf"`, which is not completely equivalent. +The multi-scale models simulate carbon capture via photosynthesis and carbon allocation for the plant organs' maintenance respiration and development. -!!! note - Note the difference between `:carbon_assimilation => ["Leaf"]` and `:carbon_assimilation => "Leaf"` is that "Leaf" is given as a vector in the first definition, and as a scalar in the second one. +The LAI and photosynthesis models are the same as in the ModelList example. The `ToyDegreeDaysModel` provides the Cumulative Thermal Time to the plant. + +The newly introduced models have the following dynamic : -The difference is that the first one maps to a vector of values, while the second one maps to a single value. The first one is useful when we don't know how many nodes there will be in the plant of type `"Leaf"`. In this case, the values are available as a vector in the `carbon_assimilation` variable of the `status` inside the model. The second one should only be used if we are sure that there will be only one node at this scale, and in this case, the one and single value is given as a scalar in the `carbon_assimilation` variable of the `status` inside the model. +Carbon allocation is determined (`ToyCAllocationModel`) for the different organs of the plant (`"Leaf"` and `"Internode"`) from the assimilation at the `"Leaf"` scale (*i.e.* the offer) and their carbon demand (`ToyCDemandModel`). The `"Soil"` scale is used to compute the soil water content (`ToySoilWaterModel`), which is needed to calculate the assimilation at the `"Leaf"` scale (`ToyAssimModel`). Also note that maintenance respiration at computed at the `"Leaf"` and `"Internode"` scales (`ToyMaintenanceRespirationModel`), and aggregated to compute the total maintenance respiration at the `"Plant"` scale (`ToyPlantRmModel`). -A third form for the mapping would be `:carbon_assimilation => ["Leaf", "Internode"]`. This form is useful when we need values for a variable from several scales simultaneously. In this case, the values are available as a vector in the `carbon_assimilation` variable of the `status` inside the model, sorted in the same order as nodes are traversed in the graph. +## Different possible variable mappings -A last form is to map to a specific variable name at the target scale, *e.g.* `:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm]`. This form is useful when the variable name is different between scales, and we want to map to a specific variable name at the target scale. In this example, the variable `Rm_organs` at plant scale takes its values (is mapped) from the variable `Rm` at the `"Leaf"` and `"Internode"` scales. +The above mapping showcases the different ways to define how the variables are mapped in a `MultiScaleModel` : + +```julia + mapped_variables=[:TT_cu => "Scene",], +``` + +- At the "Plant" scale, the TT_cu variable is mapped as a scalar from the "Scene" scale. There is only a single "Scene" node in the MTG, and only a single "TT_cu" value per timestep for the simulation. + +- On the other hand, we have `:carbon_allocation => ["Leaf"]` at the plant scale for `ToyCAllocationModel`. The `carbon_assimilation` variable is mapped as a vector: there are multiple "Leaf" nodes, but only one "Plant" node, which aggregrates the value over every single leaf. This gives us a 'many-to-one' vector mapping, and in the `run!` functions for models at that scale `carbon_allocation` will be available in the `status` as a vector. + +- A third type of the mapping would be `:carbon_allocation => ["Leaf", "Internode"]`, which provides values for a variable from several other scales simultaneously. In this case, the values are also available as a vector in the `carbon_assimilation` variable of the `status` inside the model, sorted in the same order as nodes are traversed in the graph. + +- Finally, to map to a specific variable name at the target scale, *e.g.* `:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm]`. This syntax is useful when the variable name is different between scales, and we want to map to a specific variable name at the target scale. In this example, the variable `Rm_organs` at plant scale takes its values (is mapped) from the variable `Rm` at the `"Leaf"` and `"Internode"` scales. ## Running a simulation -Now that we have a valid mapping, we can run a simulation. Running a multiscale simulation requires two more things compared to what we saw previously: a plant graph and the definition of the output variables we want dynamically for each scale. +Now that we have a valid mapping, we can run a simulation. Running a multiscale simulation requires a plant graph and the definition of the output variables we want dynamically for each scale. ### Plant graph @@ -177,7 +191,7 @@ This graph has a root node that defines a scene, then a soil, and a plant with t ### Output variables -Models can access only one time step at a time, so the output at the end of a simulation is only the last time step. However, we can define a list of variables we want to get dynamically for each time step and each scale. This list is given as a dictionary with the name of the scale as the key and a vector of variables as the value. For example, we can define a list of variables we want to get at each time step for different scales as follows: +For long simulations on plants with many organs, the output data can be very significant. It's possible to restrict the output variables that are tracked for the whole simulation to a subset of all the variables: ```@example usepkg outs = Dict( @@ -189,7 +203,9 @@ outs = Dict( ) ``` -These variables will be available in the output returned by `run!`, with a value for each time step. +This dictionary can be passed to the simulation via the optional `tracked_outputs` keyword argument to the `run!` function (see the next part). If no dictionary is provided, every variable will be tracked. + +These variables will be available in the output returned by `run!`, with a value for each time step. The corresponding timestep and node in the MTG are also returned. ### Meteorological data @@ -220,10 +236,4 @@ Or as a `DataFrame` using the `DataFrames` package: ```@example usepkg using DataFrames convert_outputs(outputs_sim, DataFrame) -``` - -### Wrapping up - -In this section, we saw how to define a mapping between models and scales, run a simulation, and access the outputs. - -This is just a simple example, but PlantSimEngine can be used to define and combine much more complex models at multiple scales of detail. With its modular architecture and intuitive API, PlantSimEngine is a powerful tool for multi-scale plant growth and development modeling. +``` \ No newline at end of file diff --git a/docs/src/multiscale/multiscale_example_2.md b/docs/src/multiscale/multiscale_example_2.md index 46b4a1df0..40af9f2aa 100644 --- a/docs/src/multiscale/multiscale_example_2.md +++ b/docs/src/multiscale/multiscale_example_2.md @@ -102,7 +102,7 @@ end ### Internode creation -The minor chagne is that new organs are now created only if the water stock is above a given threshold. +The minor change is that new organs are now created only if the water stock is above a given threshold. ```julia struct ToyCustomInternodeEmergence <: AbstractOrgan_EmergenceModel diff --git a/docs/src/multiscale/single_to_multiscale.md b/docs/src/multiscale/single_to_multiscale.md index 322eb0a32..d36eabdfe 100644 --- a/docs/src/multiscale/single_to_multiscale.md +++ b/docs/src/multiscale/single_to_multiscale.md @@ -19,6 +19,8 @@ models = ModelList( ToyRUEGrowthModel(0.2), status=(TT_cu=cumsum(meteo_day.TT),), ) + +out_singlescale = run!(models_singlescale, meteo_day) ``` Those models all operate on a simplified model of a single plant, without any organ-local information. We can therefore consider them to be working at the 'whole plant' scale. Their variables also operate at that "plant" scale, so there is no need to map any variable to other scales. @@ -43,9 +45,9 @@ None of these models operate on a multi-scale tree graph, either. There is no co mtg = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Plant", 0, 0),) ``` -## Running the multi-scale simulation +## Running the multi-scale simulation ? -We now have **almost** what we need to run the multiscale simulation. +We now have **almost** everything we need to run the multiscale simulation. This first conversion step can be a starting point for a more elaborate multi-scale simulation. @@ -57,7 +59,7 @@ out_multiscale = run!(mtg, mapping, meteo_day) (Some of the optional arguments also change slightly) -Unfortunately, there is one caveat. Passing in a vector through the `Status` is still possible in multi-scale mode, but requires a little more advanced tinkering with the mapping, as it generates a custom model under the hood and the implementation is less user-friendly. +Unfortunately, there is one caveat. Passing in a vector through the `Status` field is still possible in multi-scale mode, but requires a little more advanced tinkering with the mapping, as it generates a custom model under the hood and the implementation is experimental and less user-friendly. If you are keen on going down that path, you can find a detailed example here TODO, but we don't recommend it for beginners. @@ -119,7 +121,7 @@ This is done by wrapping our `ToyLAIModel` in a dedicated structure called a `Mu There can be different kinds of variable mapping with slightly different syntax, but in our case, only a single scalar value of the TT_cu is passed from the "Scene" to the "Plant" scale. -This gives us the following declaration : +This gives us the following declaration with the `MultiScaleModel` wrapper for our LAI model: ```julia MultiScaleModel( @@ -133,7 +135,7 @@ and the new mapping with two scales : ```julia mapping_multiscale = Dict( - "Scene" => ToyDegreeDaysCumulModel(), + "Scene" => ToyTt_CuModel(), "Plant" => ( MultiScaleModel( model=ToyLAIModel(), @@ -147,6 +149,8 @@ mapping_multiscale = Dict( ) ``` +### Running the multi-scale simulation + We can then run the multiscale simulation, with our two-node MTG : ```julia @@ -196,4 +200,14 @@ is_approx_equal = length(unique(multiscale_TT_cu .≈ out_singlescale.TT_cu)) == There is a model able to provide Thermal Time based on weather temperature data, `ToyDegreeDaysCumulModel`, which can also be found in the examples folder. -We didn't make use of it here for learning purposes. It also computes a thermal time based on default parameters that don't correspond to the thermal time in the example weather data, so results differ from the thermal time already present in the weather data without tinkering with the parameters. \ No newline at end of file +We didn't make use of it here for learning purposes. It also computes a thermal time based on default parameters that don't correspond to the thermal time in the example weather data, so results differ from the thermal time already present in the weather data without tinkering with the parameters. + +## The run! function's signature in multi-scale simulations + +The `run!` function differs slightly from its single-scale version, as indicated earlier. + +```julia +run!(mtg, mapping, meteo, constants, extra; nsteps, tracked_outputs) +``` + +Instead of a `ModelList`, it takes an MTG and a mapping. The optional `meteo` and `constants` argument are identical to the single-scale version. The `extra` argument is now reserved and should not be used. A new `nsteps` keyword argument is available to restrict the simulation to a specified number of steps. \ No newline at end of file diff --git a/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md b/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md index f335f9af2..b1686b538 100644 --- a/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md +++ b/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md @@ -325,3 +325,50 @@ Closest candidates are: ``` often indicate a likely syntax error somewhere in the mapping definition. +### Empty status vectors in multi-scale simulations + +Unexpectedly empty vectors can be returned as outputs if you happen to forget to a node at the corresponding scale in the MTG, and no organ creation occurs for that node. + +Here's an example taken from TODO, removing the "Plant" node in the dummy MTG. Only "Scene"-scale models can run initially, and since no nodes are created, "Plant"-scale models will never be run. + +```julia +PlantSimEngine.@process "tt_cu" verbose = false + +struct ToyTt_CuModel <: AbstractTt_CuModel end + +function PlantSimEngine.run!(::ToyTt_CuModel, models, status, meteo, constants, extra=nothing) + status.TT_cu += + meteo.TT +end + +function PlantSimEngine.inputs_(::ToyTt_CuModel) + NamedTuple() # No input variables +end + +function PlantSimEngine.outputs_(::ToyTt_CuModel) + (TT_cu=-Inf,) +end + +mapping_multiscale = Dict( + "Scene" => ToyTt_CuModel(), + "Plant" => ( + MultiScaleModel( + model=ToyLAIModel(), + mapped_variables=[ + :TT_cu => "Scene", + ], + ), + Beer(0.5), + ToyRUEGrowthModel(0.2), + ), +) + +mtg_multiscale = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Plant", 0, 0),) +#plant = MultiScaleTreeGraph.Node(mtg_multiscale, MultiScaleTreeGraph.NodeMTG("+", "Plant", 1, 1)) + +out_multiscale = run!(mtg_multiscale, mapping_multiscale, meteo_day) + +out_multiscale["Plant"][:LAI] +``` + +In the above code, uncommenting the second line will add a "Plant" node to the MTG, and the simulation will then behave as intuitively expected. \ No newline at end of file diff --git a/docs/src/working_with_data/floating_point_accumulation_error.md b/docs/src/working_with_data/floating_point_accumulation_error.md new file mode 100644 index 000000000..851b145f3 --- /dev/null +++ b/docs/src/working_with_data/floating_point_accumulation_error.md @@ -0,0 +1,112 @@ +# Floating-point considerations + +## Investigating a discrepancy + +In TODO page, a single-scale simulation was converted to an equivalent multiscale simulation, and outputs were compared. One detail that was glossed over, but worth bearing in mind when launching simulations is related to floating-point approximations. + +Single-scale simulation: + +```julia +meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) + +models = ModelList( + ToyLAIModel(), + Beer(0.5), + ToyRUEGrowthModel(0.2), + status=(TT_cu=cumsum(meteo_day.TT),), +) + +out_singlescale = run!(models_singlescale, meteo_day) +``` + +Multi-scale equivalent: + +```julia +PlantSimEngine.@process "tt_cu" verbose = false + +struct ToyTt_CuModel <: AbstractTt_CuModel end + +function PlantSimEngine.run!(::ToyTt_CuModel, models, status, meteo, constants, extra=nothing) + status.TT_cu += + meteo.TT +end + +function PlantSimEngine.inputs_(::ToyTt_CuModel) + NamedTuple() # No input variables +end + +function PlantSimEngine.outputs_(::ToyTt_CuModel) + (TT_cu=-Inf,) +end + +mapping_multiscale = Dict( + "Scene" => ToyTt_CuModel(), + "Plant" => ( + MultiScaleModel( + model=ToyLAIModel(), + mapped_variables=[ + :TT_cu => "Scene", + ], + ), + Beer(0.5), + ToyRUEGrowthModel(0.2), + ), +) + +mtg_multiscale = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Plant", 0, 0),) + plant = MultiScaleTreeGraph.Node(mtg_multiscale, MultiScaleTreeGraph.NodeMTG("+", "Plant", 1, 1)) + +out_multiscale = run!(mtg_multiscale, mapping_multiscale, meteo_day) +``` + +Output comparison : + +```julia + +computed_TT_cu_multiscale = collect(Base.Iterators.flatten(outputs_multiscale["Scene"][:TT_cu])) + +is_approx_equal = length(unique(multiscale_TT_cu .≈ out_singlescale.TT_cu)) == 1 +``` + +Why was the comparison only approximate ? Why `≈` instead of `==`? + +Let's try it out: + +```julia +is_perfectly_equal = length(unique(multiscale_TT_cu .== out_singlescale.TT_cu)) == 1 +``` + +Why is this false? Let's look at the data more closely. + +Looking more closely at the output, we can notice that values are identical up to timestep #105 : + +```julia +(multiscale_TT_cu .== out_singlescale.TT_cu)[104] +``` + +```julia +(multiscale_TT_cu .== out_singlescale.TT_cu)[105] +``` + +We have the values 132.33333333333331 (multi-scale) and 132.33333333333334 (single-scale). The final output values are : 2193.8166666666643 (multi-scale) and 2193.816666666666 (single-scale). + +The divergence isn't huge, but in other situations or over more timesteps it could start becoming a problem. + +## Floating-point summation + +The reason values aren't identical, is due to the fact that many numbers do not have an exact floating point representation. A classical example is 0.3 : + +```julia +0.1 + 0.2 - 0.3 +``` +5.551115123125783e-17 + +When summing many numbers, depnding on the order in which they are summed, floating-point approximation errors may aggregate more or less quickly. + +The default summation per-timestep in our example `Toy_Tt_CuModel` was a naive summation. The `cumsum` function used in the single-scale simulation to directly compute the TT_cu uses a pairwise summation method that provides approximation error on fewer digits compared to naive summation. Errors aggregate more slowly. + +In our simple example, using Float64 values, the difference wasn't significant enough to matter, but if you are writing a simulation over many timesteps or aggregating a value over many nodes, you may need to alter models to avoid numerical errors blowing up due to floating-point accuracy. + +Depending on what value is being computed and the mathematical operations used, changes may range from applying a simple scale to a range of values, to significant refactoring. + +TODO links \ No newline at end of file From f91394c433d6e3da028788b77ac7ab41acb3dfbd Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Wed, 5 Mar 2025 16:48:29 +0100 Subject: [PATCH 077/147] Minor documentation edits --- docs/src/multiscale/multiscale_considerations.md | 12 +++++++++++- docs/src/multiscale/single_to_multiscale.md | 12 +----------- docs/src/planned_features.md | 4 +++- .../plantsimengine_and_julia_troubleshooting.md | 11 ++++++++--- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/docs/src/multiscale/multiscale_considerations.md b/docs/src/multiscale/multiscale_considerations.md index a949ef4c3..0cf4ddbdc 100644 --- a/docs/src/multiscale/multiscale_considerations.md +++ b/docs/src/multiscale/multiscale_considerations.md @@ -61,4 +61,14 @@ Multi-scale simulations, especially for plants which have thousands of leaves, i ```julia TODO -``` \ No newline at end of file +``` + +## The run! function's signature + +The `run!` function differs slightly from its single-scale version. The current structure (excluding a couple of advanced/deprecated kwargs) is the following: + +```julia +run!(mtg, mapping, meteo, constants, extra; nsteps, tracked_outputs) +``` + +Instead of a `ModelList`, it takes an MTG and a mapping. The optional `meteo` and `constants` argument are identical to the single-scale version. The `extra` argument is now reserved and should not be used. A new `nsteps` keyword argument is available to restrict the simulation to a specified number of steps. \ No newline at end of file diff --git a/docs/src/multiscale/single_to_multiscale.md b/docs/src/multiscale/single_to_multiscale.md index d36eabdfe..141b23260 100644 --- a/docs/src/multiscale/single_to_multiscale.md +++ b/docs/src/multiscale/single_to_multiscale.md @@ -200,14 +200,4 @@ is_approx_equal = length(unique(multiscale_TT_cu .≈ out_singlescale.TT_cu)) == There is a model able to provide Thermal Time based on weather temperature data, `ToyDegreeDaysCumulModel`, which can also be found in the examples folder. -We didn't make use of it here for learning purposes. It also computes a thermal time based on default parameters that don't correspond to the thermal time in the example weather data, so results differ from the thermal time already present in the weather data without tinkering with the parameters. - -## The run! function's signature in multi-scale simulations - -The `run!` function differs slightly from its single-scale version, as indicated earlier. - -```julia -run!(mtg, mapping, meteo, constants, extra; nsteps, tracked_outputs) -``` - -Instead of a `ModelList`, it takes an MTG and a mapping. The optional `meteo` and `constants` argument are identical to the single-scale version. The `extra` argument is now reserved and should not be used. A new `nsteps` keyword argument is available to restrict the simulation to a specified number of steps. \ No newline at end of file +We didn't make use of it here for learning purposes. It also computes a thermal time based on default parameters that don't correspond to the thermal time in the example weather data, so results differ from the thermal time already present in the weather data without tinkering with the parameters. \ No newline at end of file diff --git a/docs/src/planned_features.md b/docs/src/planned_features.md index 7b02272a9..b52ad5ab2 100644 --- a/docs/src/planned_features.md +++ b/docs/src/planned_features.md @@ -40,7 +40,7 @@ Its current state doesn't enable practical declaration of several plant species, ## Other minor points -- Documenting floating-point accumulation errors +- Examples/solutions for floating-point accumulation errors - More examples for fitting/type conversion/error propagation - MTG couple of new features #106 - Other minor bugs @@ -48,6 +48,8 @@ Its current state doesn't enable practical declaration of several plant species, ## Other +- Reproducing another FSPM? + The full list of issues can be found [here](https://github.com/VirtualPlantLab/PlantSimEngine.jl/issues) TODO diff --git a/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md b/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md index b1686b538..3764d3e8b 100644 --- a/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md +++ b/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md @@ -90,7 +90,11 @@ It is sometimes properly detected and explained on PlantSimEngine's side (when p The syntax for an empty NamedTuple is `NamedTuple()`. If instead one types `()` or `(,)`an error returned respectively by PlantSimEngine or Julia will be returned. -### Forgetting a kwarg when declaring a MultiScaleModel +## PlantSimEngine user errors + +Most of these occur exclusively in multi-scale simulations, which has a slightly more complex API, but some are common to both single- and multi-scale simulations. + +### MultiScaleModel : forgetting a kwarg in the declaration A MultiScaleModel requires two kwargs, model and mapped_variables : @@ -286,13 +290,14 @@ TODO If there is a need to collect variables at two different scales, and one scale is completely absent from the mapping, the error currently occurs on the Julia side : ```julia +# No models at the E3 scale in the mapping ! + "E2" => ( MultiScaleModel( model = HardDepSameScaleEchelle2Model(), mapped_variables=[:c => "E1" => :c, :e3 => "E3" => :e3, :f3 => "E3" => :f3,], ), ), -# No E3 in the mapping ! Exception has occurred: KeyError * @@ -327,7 +332,7 @@ often indicate a likely syntax error somewhere in the mapping definition. ### Empty status vectors in multi-scale simulations -Unexpectedly empty vectors can be returned as outputs if you happen to forget to a node at the corresponding scale in the MTG, and no organ creation occurs for that node. +This situation won't trigger an error. Unexpectedly empty vectors can be returned as outputs if you happen to forget to a node at the corresponding scale in the MTG, and no organ creation occurs for that node. Here's an example taken from TODO, removing the "Plant" node in the dummy MTG. Only "Scene"-scale models can run initially, and since no nodes are created, "Plant"-scale models will never be run. From a61d72e1817e5d8c43f6bfd080d011a65d92d082 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Wed, 5 Mar 2025 16:59:33 +0100 Subject: [PATCH 078/147] Quick hackery to ensure documentation builds. Reminder : API page needs tinkering --- docs/src/multiscale/single_to_multiscale.md | 4 ++-- docs/src/prerequisites/key_concepts.md | 2 +- docs/src/step_by_step/detailed_first_example.md | 2 +- docs/src/step_by_step/implement_a_model_additional.md | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/src/multiscale/single_to_multiscale.md b/docs/src/multiscale/single_to_multiscale.md index 141b23260..6701bf176 100644 --- a/docs/src/multiscale/single_to_multiscale.md +++ b/docs/src/multiscale/single_to_multiscale.md @@ -8,7 +8,7 @@ The full script for this section can be found in TODO # Converting the ModelList to a multi-scale mapping -For example, let's consider the `ModelList` coupling a light interception model, a Leaf Area Index model, and a carbon biomass increment model that was discussed [here](Further coupling) : +For example, let's consider the `ModelList` coupling a light interception model, a Leaf Area Index model, and a carbon biomass increment model that was discussed here(TODO ref Further coupling) : ```julia meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) @@ -194,7 +194,7 @@ is_approx_equal = length(unique(multiscale_TT_cu .≈ out_singlescale.TT_cu)) == ``` !!! note - You may be wondering why we check for approximate equality rather than strict equality. The reason for that is due to floating-point accumulation errors, which are discussed in more detail [here]TODO. + You may be wondering why we check for approximate equality rather than strict equality. The reason for that is due to floating-point accumulation errors, which are discussed in more detail here TODO. ## ToyDegreeDaysCumulModel diff --git a/docs/src/prerequisites/key_concepts.md b/docs/src/prerequisites/key_concepts.md index 0018485ad..909371d4e 100644 --- a/docs/src/prerequisites/key_concepts.md +++ b/docs/src/prerequisites/key_concepts.md @@ -11,7 +11,7 @@ You'll find a brief description of some of the main concepts and terminology rel This section provides a general description of the concepts and terminology used in PlantSimEngine. For a more implementation-guided description of the design and some of the terms presented here, see [First Simulation]TODO !!! Note - Some terminology unfortunately has different meanings in different contexts. This is particularly true of the terms organ, scale and symbol, which have a different meaning for [Multi-scale tree graphs](TODO) than the rest of PlantSimEngine(TODO). Make sure to double-check this section, and relevant examples if you encounter issues relating to these terms. + Some terminology unfortunately has different meanings in different contexts. This is particularly true of the terms organ, scale and symbol, which have a different meaning for Multi-scale tree graphs(TODO) than the rest of PlantSimEngine(TODO). Make sure to double-check this section, and relevant examples if you encounter issues relating to these terms. ### Processes diff --git a/docs/src/step_by_step/detailed_first_example.md b/docs/src/step_by_step/detailed_first_example.md index 0a14d72cb..5c9156824 100644 --- a/docs/src/step_by_step/detailed_first_example.md +++ b/docs/src/step_by_step/detailed_first_example.md @@ -2,7 +2,7 @@ This section walks you through the ins and outs of a basic simulation, mostly aimed at people who have less experience programming, to showcase the various concepts presented earlier and requirements for a simulation in context. -The full example discussed in this page can be found [further down](@ref Example simulation). +The full example discussed in this page can be found further down(TODO ref Example simulation). ```@setup usepkg diff --git a/docs/src/step_by_step/implement_a_model_additional.md b/docs/src/step_by_step/implement_a_model_additional.md index c8d894f36..23eb9f1e0 100644 --- a/docs/src/step_by_step/implement_a_model_additional.md +++ b/docs/src/step_by_step/implement_a_model_additional.md @@ -2,7 +2,7 @@ ## Parametric types -In [Implementing a new model](@ref), the Beer model's structure was declared with a parametric type. +In Implementing a model(TODO ref), the Beer model's structure was declared with a parametric type. ```julia struct Beer{T} <: AbstractLight_InterceptionModel @@ -76,7 +76,7 @@ Beer(;k) = Beer(k) The `;` syntax indicates that subsequent arguments are provided as keyword arguments, so now we can call `Beer` like this: -```@example usepkg +```julia Beer(k = 0.7) ``` @@ -86,7 +86,7 @@ This helps readability when there are a lot of parameters and some have default The last optional utility function to implement is a method for the `eltype` function: -```@example usepkg +```julia Base.eltype(x::Beer{T}) where {T} = T ``` From 05a75532ab1f4a5e891e744a543ecb911bd791ff Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Wed, 5 Mar 2025 17:08:43 +0100 Subject: [PATCH 079/147] Update doc table of contents : some pages hadn't been added --- docs/make.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/make.jl b/docs/make.jl index 1c1770177..8e7edd8a2 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -49,7 +49,8 @@ makedocs(; "Reducing DoF" => "./working_with_data/reducing_dof.md", "Fitting" => "./working_with_data/fitting.md", "Input types" => "./working_with_data/inputs.md", - "Visualizing outputs" => "./working_with_data/visualising_outputs.md" + "Visualizing outputs" => "./working_with_data/visualising_outputs.md", + "Floating-point considerations" => "./working_with_data/floating_point_accumulation_error.md", ], "Moving to multiscale" => [ "Multiscale considerations" => "./multiscale/multiscale_considerations.md", @@ -66,10 +67,12 @@ makedocs(; "Troubleshooting" => "./troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md", "Automated testing" => "./troubleshooting_and_testing/downstream_tests.md", "Tips and Workarounds" => "./troubleshooting_and_testing/tips_and_workarounds.md", + "Implicit contracts" => "./troubleshooting_and_testing/implicit_contracts.md", ], "API" => [ "Public API" => "./API/API_public.md", "Internal API" => "./API/API_private.md",], "Credits" => "credits.md", + "Improving our documentation" => "documentation_improvement.md", "Planned features" => "planned_features.md", #"developer section ?" ] From 2ce132032519b51fdc6efe1d999526912c312d25 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Thu, 6 Mar 2025 13:50:18 +0100 Subject: [PATCH 080/147] Documentation : fixing up TODOs and broken reference, small syntax changes, couple of extra paragraphs --- docs/src/documentation_improvement.md | 2 +- docs/src/multiscale/multiscale.md | 7 ++- .../multiscale/multiscale_considerations.md | 49 ++++++++++++++----- docs/src/multiscale/multiscale_coupling.md | 23 ++++++--- docs/src/multiscale/multiscale_example_1.md | 2 +- docs/src/multiscale/multiscale_example_3.md | 45 ++++++++++++++--- docs/src/multiscale/single_to_multiscale.md | 8 +-- docs/src/prerequisites/julia_basics.md | 4 +- docs/src/prerequisites/key_concepts.md | 6 +-- .../step_by_step/detailed_first_example.md | 4 +- docs/src/step_by_step/implement_a_model.md | 6 +-- docs/src/step_by_step/model_switching.md | 3 -- docs/src/step_by_step/parallelization.md | 2 +- .../step_by_step/quick_and_dirty_examples.md | 2 +- ...lantsimengine_and_julia_troubleshooting.md | 22 ++++----- .../tips_and_workarounds.md | 4 +- 16 files changed, 125 insertions(+), 64 deletions(-) diff --git a/docs/src/documentation_improvement.md b/docs/src/documentation_improvement.md index 1ebfe8e97..ef472f6ce 100644 --- a/docs/src/documentation_improvement.md +++ b/docs/src/documentation_improvement.md @@ -4,4 +4,4 @@ One goal for PlantSimEngine is to ensure testing ecophysiological hypotheses, or Good documentation is essential for that purpose. -If parts of the documentation are unclear to you, you are very welcome to send a PR, an email, or a message (either on Github or on Discourse : TODO) so that we can improve upon it. \ No newline at end of file +If parts of the documentation are unclear to you, you are very welcome to send a PR, an email, or a message (either [on Github](https://github.com/VirtualPlantLab/PlantSimEngine.jl/issues) or [on the FSPM Discourse](https://fspm.discourse.group/latest)) so that we can improve upon it. \ No newline at end of file diff --git a/docs/src/multiscale/multiscale.md b/docs/src/multiscale/multiscale.md index 9ea3b307a..cdf43309f 100644 --- a/docs/src/multiscale/multiscale.md +++ b/docs/src/multiscale/multiscale.md @@ -1,8 +1,8 @@ # Multi-scale variable mapping -The previous section showed how to convert a single-scale simulation to multi-scale. +The previous page showed how to convert a single-scale simulation to multi-scale. -This section provides another example showcasing the nuances in variable mapping, with a more complex fully multiscale version of a prior simulation. The models will all be taken form the examples folder []TODO +This page provides another example showcasing the nuances in variable mapping, with a more complex fully multiscale version of a prior simulation. The models will all be taken form the examples folder []TODO ## Starting with a single-model mapping @@ -136,9 +136,8 @@ mapping = Dict( nothing # hide ``` -This mapping might seem a little more daunting than previous examples, but several models should be recognizable in passing. In fact, you can consider this mapping to be an enhanced and more complex multi-scale version of a previous single-scale example. +This mapping might seem a little more daunting than previous examples, but several models should be recognizable in passing. In fact, you can consider this mapping to be an enhanced and more complex multi-scale version of a previous single-scale example, the coupling between photosynthesis model, a LAI model and a carbon biomass increment model, used in the [Example model switching](@ref) subsection. -TODO link ```julia models2 = ModelList( ToyLAIModel(), diff --git a/docs/src/multiscale/multiscale_considerations.md b/docs/src/multiscale/multiscale_considerations.md index 0cf4ddbdc..91c731f37 100644 --- a/docs/src/multiscale/multiscale_considerations.md +++ b/docs/src/multiscale/multiscale_considerations.md @@ -1,6 +1,6 @@ # Multi-scale considerations -This section briefly details the subtle ways in which multi-scale simulations differ from prior single-scale simulations. The next few pages will showcase some of these subtleties with examples. +This page briefly details the subtle ways in which multi-scale simulations differ from prior single-scale simulations. The next few pages will showcase some of these subtleties with examples. Declaring and running a multi-scale simulation follows the same general workflow as the single-scale version, but multi-scale simulations do have some differences : @@ -13,13 +13,24 @@ The simulation dependency graph will still be computed automatically and handle Multi-scale simulations also tend to require more extra ad hoc models to prepare some variables for some models. -TODO link to other pages +Other pages in this section describe : + +- How to write a direct conversion of a single-scale ModelList simulation to a multi-scale simulation and add a second scale to it: [Converting a single-scale simulation to multi-scale](@ref), +- A more complex multi-scale version of the single-scale simulation showcasing different variable mappings between scales: [Multi-scale variable mapping](@ref), +- A three-part tutorial describing how to build up a combination of models to simulate a growing toy plant: [Writing a multiscale simulation](@ref), +- Ways to handle situations where a variable ends up causing a cyclic dependency: [Avoiding cyclic dependencies](@ref), +- Multi-scale specific coupling considerations and subtleties:[Handling dependencies in a multiscale context](@ref) ## Multi-scale tree graphs Functional-Structural Plant Models are often about simulating plant growth. A multi-scale simulation is implicitely expected to operate on a plant-like object, represented by a multi-scale tree graph. -A multi-scale tree graph (MTG) object (see TODO) is therefore required to run a multi-scale simulations. It can be a dummy MTG if the simulation doesn't actually affect it, but is nevertheless a required argument to the multi-scale `run` function. +A multi-scale tree graph (MTG) object (see the [Multi-scale Tree Graphs](@ref) subsection for a quick description) is therefore required to run a multi-scale simulations. It can be a dummy MTG if the simulation doesn't actually affect it, but is nevertheless a required argument to the multi-scale `run` function. + +All the multi-scale examples make use of the companion package [MultiScaleTreeGraph.jl](https://github.com/VEZY/MultiScaleTreeGraph.jl), which we therefore recommend for running your own multi-scale simulations. + +!!! note + Multi-scale Tree Graphs make use of conflicting terminology with PlantSimEngine's concepts, which is discussed in [Organ/Scale](@ref). If you are new to the concepts, make sure to read that section and keep note of it. ## Models run once per organ instance, not once per organ level @@ -49,26 +60,38 @@ This means every organ instance has its own `Status`, with scale-specific attrib This has two **important** consequences in terms of running a simulation : -- First, **any scale absent from the MTG will not be run**. If your MTG contains no leaves, then no model operating at the scale "Leaf" will be able to run until a "Leaf" organ is created and a node is added in the MTG. Otherwise, it has no MTG node to operate on. The only exceptions are hard dependency models which can be called from a different scale. TODO example +- First, **any scale absent from the MTG will not be run**. If your MTG contains no leaves, then no model operating at the scale "Leaf" will be able to run until a "Leaf" organ is created and a node is added in the MTG. Otherwise, it has no MTG node to operate on. The only exceptions are hard dependency models which can be called from a different scale, since they can be called directly by a model on a node at a different existing scale, even if there is no node at their own scale. - Secondly, models only have access to **local** organ information. The `status` argument in the `run!` function only contains variables **at the model's scale**, unless variables from other scales are mapped via a `MultiScaleModel` wrapping. -## Outputs - -The output structure, like the mapping, is a Julia `Dict` structure indexed by scale. TODO node +## The run! function's signature -Multi-scale simulations, especially for plants which have thousands of leaves, internodes, root branches, buds and fruits, may compute huge amounts of data. Just like in single-scale simulations, it is possible to keep only variables whose values you want to track for every timestep, and filter the rest out. Again, those tracked variables need to be indexed by scale, see the following example : +The `run!` function differs slightly from its single-scale version. The current structure (excluding a couple of advanced/deprecated kwargs) is the following: ```julia -TODO +run!(mtg, mapping, meteo, constants, extra; nsteps, tracked_outputs) ``` -## The run! function's signature +Instead of a `ModelList`, it takes an MTG and a mapping. The optional `meteo` and `constants` argument are identical to the single-scale version. The `extra` argument is now reserved and should not be used. A new `nsteps` keyword argument is available to restrict the simulation to a specified number of steps. -The `run!` function differs slightly from its single-scale version. The current structure (excluding a couple of advanced/deprecated kwargs) is the following: +## Outputs + +The output structure, like the mapping, is a Julia `Dict` structure indexed by scale. TODO node + +Multi-scale simulations, especially for plants which have thousands of leaves, internodes, root branches, buds and fruits, may compute huge amounts of data. Just like in single-scale simulations, it is possible to keep only variables whose values you want to track for every timestep, and filter the rest out, using the `tracked_outputs` keyword argument for the `run!` function. + +Those tracked variables also need to be indexed by scale to avoid ambiguity: ```julia -run!(mtg, mapping, meteo, constants, extra; nsteps, tracked_outputs) +outs = Dict( + "Scene" => (:TT, :TT_cu,), + "Plant" => (:aPPFD, :LAI), + "Leaf" => (:carbon_assimilation, :carbon_demand, :carbon_allocation, :TT), + "Internode" => (:carbon_allocation,), + "Soil" => (:soil_water_content,), +) ``` -Instead of a `ModelList`, it takes an MTG and a mapping. The optional `meteo` and `constants` argument are identical to the single-scale version. The `extra` argument is now reserved and should not be used. A new `nsteps` keyword argument is available to restrict the simulation to a specified number of steps. \ No newline at end of file +## Coupling and multi-scale hard dependencies + +Multi-scale brings new types of coupling: mappings are part of the approach used to handle variables used by models at different scales. A model can also have a hard dependency on another model that operates at another scale. This multi-scale-specific complexity is discussed in [Handling dependencies in a multiscale context](@ref) \ No newline at end of file diff --git a/docs/src/multiscale/multiscale_coupling.md b/docs/src/multiscale/multiscale_coupling.md index 085d5844c..7a7f5a9b7 100644 --- a/docs/src/multiscale/multiscale_coupling.md +++ b/docs/src/multiscale/multiscale_coupling.md @@ -3,7 +3,7 @@ ## Scalar and vector variable mappings -In the detailed example discussed previously (TODO), there were several instances of mapping a variable from one scale to another. Here's a relevant exerpt from the mapping : +In the detailed example discussed previously [Multi-scale variable mapping](@ref), there were several instances of mapping a variable from one scale to another, which we'll briefly describe again to help transition to the next and more advanced subsection. Here's a relevant exerpt from the mapping : ```julia "Plant" => ( @@ -49,16 +49,15 @@ Note that there may be instances where you might wish to write your own model to If a model requires some input variable that is computed at another scale, then providing the appropriate mapping for that variable will resolve name conflicts and enable that model to run with no further steps for the user or the modeler when the coupling is a 'soft dependency'. -In the case of a hard dependency that operates at the same scale as its parent, declaring the hard dependency is exactly the same as in single-scale simulations and there are also no new extra steps on the user-side. - -On the other hand, modelers do need to bear in mind a couple of subtleties when developing models that possess hard dependencies that operate at a different organ level from their parent : +In the case of a hard dependency that operates **at the same scale as its parent**, declaring the hard dependency is exactly the same as in single-scale simulations and there are also no new extra steps on the user-side: -The parent model directly handles the call to its hard dependency model(s), meaning they are not explicitely managed by the top-level dependency graph. - Therefore only the owning model of that dependency is visible in the graph, and its hard dependency nodes are internal. +- The parent model directly handles the call to its hard dependency model(s), meaning they are not explicitely managed by the top-level dependency graph. +- This means only the owning model of that dependency is visible in the graph, and its hard dependency nodes are internal. +- When the caller (or any downstream model that requires some variables from the hard dependency model) operates at the same scale, variables are easily accessible, and no mapping is required. -When the caller (or any downstream model that requires some variables from the hard dependency) operates at the same scale, variables are easily accessible, and no mapping is required. +On the other hand, modelers do need to bear in mind a couple of subtleties when developing models that possess hard dependencies that operate **at a different organ level from their parent**: -If an inner model operates at a different scale/organ level, a modeler must declare hard dependencies with their respective organ level, similarly to the way the user provides a mapping. +If an model needs to be directly called by a parent but operates at a different scale/organ level, a modeler must declare hard dependencies with their respective organ level, similarly to the way the user provides a mapping. Conceptually : @@ -68,8 +67,12 @@ Conceptually : ) ``` +### An example from the toy plant simulation tutorial + TODO example discussed in toy plant +### An example from XPalm.jl + Here's a concrete example in [XPalm](https://github.com/PalmStudio/XPalm.jl), an oil palm model developed on top of PlantSimEngine. Organs are produced at the phytomer scale, but need to run an age model and a biomass model at the reproductive organs' scales. @@ -108,11 +111,15 @@ function ReproductiveOrganEmission(mtg::MultiScaleTreeGraph.Node; phytomer_symbo end ``` +## Implementation details: accessing a hard dependency's variables from a different scale + But how does a model M calling a hard dependency H provide H's variables when calling H's `run!` function ? The status the user provides M operates at M's organ level, so if used to call H's run! function any required variable for H will be missing. PlantSimEngine provides what are called Status Templates in the simulation graph. Each organ level has its own Status template listing the available variables at that scale. So when a model M calls a hard dependency H's `run!` function, any required variables can be accessed through the status template of H's organ level. +### XPalm.jl example to illustrate + Using the same example in XPalm : ```julia diff --git a/docs/src/multiscale/multiscale_example_1.md b/docs/src/multiscale/multiscale_example_1.md index ef4102d8a..3b8882970 100644 --- a/docs/src/multiscale/multiscale_example_1.md +++ b/docs/src/multiscale/multiscale_example_1.md @@ -2,7 +2,7 @@ TODO change Toy To Example ? -This three-part section walks you through building a multi-scale simulation from scratch. It is meant as an illustration of the iterative process you might go through when building and slowly tuning a Functional-Structural Plant Model, where previous multi-scale examples focused more on the API syntax. +This three-part subsection walks you through building a multi-scale simulation from scratch. It is meant as an illustration of the iterative process you might go through when building and slowly tuning a Functional-Structural Plant Model, where previous multi-scale examples focused more on the API syntax. You can find the full script for the first part's toy simulation in the [ToyMultiScalePlantModel](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/ToyMultiScalePlantModel/ToyPlantSimulation1.jl) subfolder of the examples folder. diff --git a/docs/src/multiscale/multiscale_example_3.md b/docs/src/multiscale/multiscale_example_3.md index 9d9471351..8bc009026 100644 --- a/docs/src/multiscale/multiscale_example_3.md +++ b/docs/src/multiscale/multiscale_example_3.md @@ -33,11 +33,11 @@ Indeed, if both the root and leaf water thresholds are met, and there is enough This occurs because carbon_stock is only computed once, and won't update until the next timestep. -What we can do to avoid that problem in our specific case (for other situations TODO), is to couple the root growth model and the internode emission model, and pass the carbon_root_creation_consumed so that internode emission can take it into account. Or we could have an intermediate model recompute the new stock to pass along to the internode emission model. +To avoid that problem in our specific case, we can couple the root growth model and the internode emission model, and pass the carbon_root_creation_consumed so that internode emission can take it into account. Or we could have an intermediate model recompute the new stock to pass along to the internode emission model. -We'll go for the first option. +There is a section in the 'Tips and workarounds' page discussing this situation and other potential solutions: [Having a variable simultaneously as input and output of a model](@ref). -TODO previous timestep ? +We'll go for the first option and couple the root growth and internode emission model. ### Internode emission adjustments @@ -53,10 +53,40 @@ The only change required for our internode emission model is to take into accoun end ``` -### Root growth decision model with a hard dependency +### A multi-scale hard dependency appears Our root growth decision model inherits some of the responsibility from last chapter's root growth model, so inputs, parameters and condition checks will be similar. We'll let the root growth model keep the length check and only focus on resources. +Since the decision model is now directly responsible for calling the actual root growth model, we need to declare that it requires a root growth model as a hard dependency and cannot be run standalone. TODO see. + +This hard dependency is in fact multiscale, since both models operate at different scales, "Plant" and "Root". To specify which scale the hard dependency comes from, the declaration additionally requires mapping the scale, compared to the single-scale equivalent: + +```julia +PlantSimEngine.dep(::ToyRootGrowthDecisionModel) = (root_growth=AbstractRoot_GrowthModel=>["Root"],) +``` + +The `status` argument `run!` function of the root growth decision model only contains variables from the "Plant" scale, or explicitely mapped to this scale, which isn't the case for the root growth's variables. To make use of the root growth model's variables, we need to recover the `status` at the "Root" scale. It is accessible from the `extra` argument in `run!`'s signature. + +In multi-scale simulations, this `extra` argument implicitely contains an object storing the simulation state. It contains the statuses at various scales, and all the models indexed per scale and process name. + +Access to the "Root" status within the root growth decision model `run!` function is done like so: + +```julia +status_Root= extra_args.statuses["Root"][1] +``` + +It is then possible to call the root growth model from the parent's `run!` function: + +```julia +PlantSimEngine.run!(extra.models["Root"].root_growth, models, status_Root, meteo, constants, extra) +``` + +Which will enable writing the rest of the `run!` function. + +### Root growth decision model implementation + +With that new coupling consideration properly handled, we can complete the full model implementation: + ```julia PlantSimEngine.@process "root_growth_decision" verbose = false @@ -72,20 +102,23 @@ PlantSimEngine.outputs_(::ToyRootGrowthDecisionModel) = NamedTuple() PlantSimEngine.dep(::ToyRootGrowthDecisionModel) = (root_growth=AbstractRoot_GrowthModel=>["Root"],) +# "status" is at the "Plant" scale function PlantSimEngine.run!(m::ToyRootGrowthDecisionModel, models, status, meteo, constants=nothing, extra=nothing) if status.water_stock < m.water_threshold && status.carbon_stock > m.carbon_root_creation_cost + # Obtain "status" at "Root" scale status_Root= extra_args.statuses["Root"][1] + # Call the hard dependency model directly with its status PlantSimEngine.run!(extra.models["Root"].root_growth, models, status_Root, meteo, constants, extra) end end ``` -Note the hard dependency declaration, and the direct call to the root growth `run!` function. The root growth model will output the `carbon_root_creation_consumed` computation, but it'll still be exposed to downstream models despite the root growth model being an 'hidden' model since it's a hard dependency. +The root growth model will output the `carbon_root_creation_consumed` computation, but it'll still be exposed to downstream models despite the root growth model being a 'hidden' model in the dependency graph due to its hard dependency nature. ### Root growth -This version is a simplifed version of last chapter's. +This iteration turns into a simplifed version of last chapter's. ```julia PlantSimEngine.@process "root_growth" verbose = false diff --git a/docs/src/multiscale/single_to_multiscale.md b/docs/src/multiscale/single_to_multiscale.md index 6701bf176..a2ebd2490 100644 --- a/docs/src/multiscale/single_to_multiscale.md +++ b/docs/src/multiscale/single_to_multiscale.md @@ -2,13 +2,13 @@ A single-scale simulation can be turned into a 'pseudo-multi-scale' simulation by providing a simple multi-scale tree graph, and declaring a mapping linking all models to a unique scale level. -This section showcases the conversion, and then adds a model at a new scale to make the simulation genuinely multi-scale. +This page showcases how to do the conversion, and then adds a model at a new scale to make the simulation genuinely multi-scale. -The full script for this section can be found in TODO +The full script for the example can be found in [https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/ToySingleToMultiScale.jl] # Converting the ModelList to a multi-scale mapping -For example, let's consider the `ModelList` coupling a light interception model, a Leaf Area Index model, and a carbon biomass increment model that was discussed here(TODO ref Further coupling) : +For example, let's return to the `ModelList` coupling a light interception model, a Leaf Area Index model, and a carbon biomass increment model that was discussed in the [Example model switching](@ref) subsection: ```julia meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) @@ -61,7 +61,7 @@ out_multiscale = run!(mtg, mapping, meteo_day) Unfortunately, there is one caveat. Passing in a vector through the `Status` field is still possible in multi-scale mode, but requires a little more advanced tinkering with the mapping, as it generates a custom model under the hood and the implementation is experimental and less user-friendly. -If you are keen on going down that path, you can find a detailed example here TODO, but we don't recommend it for beginners. +If you are keen on going down that path, you can find a detailed example [here](@ref multiscale_vector), but we don't recommend it for beginners. What we'll do instead, is write our own model provide the thermal time per timestep as a variable, instead of as a single vector in the `Status`. diff --git a/docs/src/prerequisites/julia_basics.md b/docs/src/prerequisites/julia_basics.md index 91d1af283..10c86ac0a 100644 --- a/docs/src/prerequisites/julia_basics.md +++ b/docs/src/prerequisites/julia_basics.md @@ -6,11 +6,11 @@ Julia is a language that is gaining traction, but it isn't the most widely used Many elements will be familiar to those with an R, Python or Matlab background, but there are some noteworthy differences, and if you are new to the language, there will be a few hurdles you might have to overcome to be comfortable using the language. -This section is here to help you with that, and provides a short introduction to the parts of Julia that are most relevant regarding usage of PlantSimEngine. +This page is here to help you with that, and provides a short introduction to the parts of Julia that are most relevant regarding usage of PlantSimEngine. It is not meant as a full-fledged Julia tutorial. If you are completely new to programming, you may wish to check some other resources first, such as ones found [here](https://docs.julialang.org/en/v1/manual/getting-started/). -If you wish to compare Julia to a specific language, [this page](https://docs.julialang.org/en/v1/manual/noteworthy-differences/#Noteworthy-differences-from-Python) will provide you with a quick overview of the differences. +If you wish to compare Julia to a specific language, [the noteworthy differences section](https://docs.julialang.org/en/v1/manual/noteworthy-differences/#Noteworthy-differences-from-Python) will provide you with a quick overview of the differences. You can also find a few cheatsheets [here](https://palmstudio.github.io/Biophysics_database_palm/cheatsheets/) as well as a [short introductory notebook](https://palmstudio.github.io/Biophysics_database_palm/basic_syntax/) along with [install instructions](https://palmstudio.github.io/Biophysics_database_palm/installation/) diff --git a/docs/src/prerequisites/key_concepts.md b/docs/src/prerequisites/key_concepts.md index 909371d4e..bb68e17f3 100644 --- a/docs/src/prerequisites/key_concepts.md +++ b/docs/src/prerequisites/key_concepts.md @@ -8,10 +8,10 @@ You'll find a brief description of some of the main concepts and terminology rel ## PlantSimEngine terminology -This section provides a general description of the concepts and terminology used in PlantSimEngine. For a more implementation-guided description of the design and some of the terms presented here, see [First Simulation]TODO +This page provides a general description of the concepts and terminology used in PlantSimEngine. For a more implementation-guided description of the design and some of the terms presented here, see the [Detailed walkthrough of a simple simulation](@ref) !!! Note - Some terminology unfortunately has different meanings in different contexts. This is particularly true of the terms organ, scale and symbol, which have a different meaning for Multi-scale tree graphs(TODO) than the rest of PlantSimEngine(TODO). Make sure to double-check this section, and relevant examples if you encounter issues relating to these terms. + Some terminology unfortunately has different meanings in different contexts. This is particularly true of the terms organ, scale and symbol, which have a different meaning for [Multi-scale Tree Graphs](@ref) than the rest of PlantSimEngine (see [Organ/Scale](@ref) further down). Make sure to double-check those subsections, and relevant examples if you encounter issues relating to these terms. ### Processes @@ -116,7 +116,7 @@ TODO !!! Note When you encounter the terms "Single-scale simulations", or "ModelList simulations", they will refer to simulations that are "not multi-scale". A multi-scale simulation makes use of a mapping between different organ/scale levels. A single-scale simulation has no such mapping, and uses the simpler ModelList interface. - You can implement a mapping that only makes use of a single scale level, of course, making it a "single-scale multi-scale simulation", but unless otherwise specified, single-scale, and the whole section dedicated to single-scale simulations, refer to simulations with ModelList objects, and no mapping. + You can implement a mapping that only makes use of a single scale level, of course, making it a "single-scale multi-scale simulation", but **unless otherwise specified, single-scale, and the whole section dedicated to single-scale simulations, refer to simulations with ModelList objects, and no mapping**. ### Multi-scale Tree Graphs diff --git a/docs/src/step_by_step/detailed_first_example.md b/docs/src/step_by_step/detailed_first_example.md index 5c9156824..0a0ca05b3 100644 --- a/docs/src/step_by_step/detailed_first_example.md +++ b/docs/src/step_by_step/detailed_first_example.md @@ -1,6 +1,6 @@ # Detailed walkthrough of a simple simulation -This section walks you through the ins and outs of a basic simulation, mostly aimed at people who have less experience programming, to showcase the various concepts presented earlier and requirements for a simulation in context. +This page walks you through the ins and outs of a basic simulation, mostly aimed at people who have less experience programming, to showcase the various concepts presented earlier and requirements for a simulation in context. The full example discussed in this page can be found further down(TODO ref Example simulation). @@ -167,7 +167,7 @@ run!(model_list, meteo) The first argument is the model list (see [`ModelList`](@ref)), and the second defines the micro-climatic conditions. -The `ModelList` should already be initialized for the given process before calling the function. Refer to the earlier section [Variables (inputs, outputs)](@ref) for more details. +The `ModelList` should already be initialized for the given process before calling the function. Refer to the earlier subsection [Variables (inputs, outputs)](@ref) for more details. ### Example simulation diff --git a/docs/src/step_by_step/implement_a_model.md b/docs/src/step_by_step/implement_a_model.md index 91a92a6c7..2c6bf30aa 100644 --- a/docs/src/step_by_step/implement_a_model.md +++ b/docs/src/step_by_step/implement_a_model.md @@ -8,7 +8,7 @@ struct Beer{T} <: AbstractLight_InterceptionModel end ``` -For your own simulations, you might want to move beyond simple usage at some point and implement your own models. In this section, we'll go through the required steps for writing a new model. The detailed version is tailored for people less familiar with programming. +For your own simulations, you might want to move beyond simple usage at some point and implement your own models. In this page, we'll go through the required steps for writing a new model. The detailed version is tailored for people less familiar with programming. ## Quick version @@ -60,8 +60,8 @@ And that is all you need to get going, for this example with a single parameter The `@process` macro does some boilerplate work described [here](@ref under_the_hood) -If you have more than one parameter, then type conversion utility functions might also be interesting to implement. See here TODO -If you need to deal with more complex couplings, the hard dependency section will detail TODO +Some extra utility functions can also be interesting to implement to make users' lives simpler. See the [Model implementation additional notes](@ref) page for details. +If your custom model needs to handle more complex couplings than the simple input/output described in this example, check out the [Coupling more complex models](@ref) page. ## Detailed version diff --git a/docs/src/step_by_step/model_switching.md b/docs/src/step_by_step/model_switching.md index ca595552a..1a1a32138 100644 --- a/docs/src/step_by_step/model_switching.md +++ b/docs/src/step_by_step/model_switching.md @@ -59,9 +59,6 @@ meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), nothing # hide ``` -!!! tip - For meteorology data details, you can check the code presented [in this section in the FAQ](@ref defining_the_meteo) - We can now run the simulation: ```@example usepkg diff --git a/docs/src/step_by_step/parallelization.md b/docs/src/step_by_step/parallelization.md index 060a358e9..0365ad23d 100644 --- a/docs/src/step_by_step/parallelization.md +++ b/docs/src/step_by_step/parallelization.md @@ -1,7 +1,7 @@ ## Parallel execution !!! note - This section is likely to change and become outdated. In any case, parallel execution only currently applies to single-scale simulations (multi-scale simulations' changing MTGs and extra complexity don't allow for straightforward parallelisation) + This page is likely to change and become outdated. In any case, parallel execution only currently applies to single-scale simulations (multi-scale simulations' changing MTGs and extra complexity don't allow for straightforward parallelisation) ### FLoops diff --git a/docs/src/step_by_step/quick_and_dirty_examples.md b/docs/src/step_by_step/quick_and_dirty_examples.md index eaa340752..148b83731 100644 --- a/docs/src/step_by_step/quick_and_dirty_examples.md +++ b/docs/src/step_by_step/quick_and_dirty_examples.md @@ -7,7 +7,7 @@ If you wish for a more detailed rundown of the examples, you can instead have a These examples are all for single-scale simulations. For multi-scale modelling tutorials and examples, refer to [this section][#multiscale] -You can find the implementation for these example models, as well as other toy models [in the examples folder](https://github.com/VirtualPlantLab/PlantSimEngine.jl/tree/main/examples). +You can find the implementation for all the example models, as well as other toy models [in the examples folder](https://github.com/VirtualPlantLab/PlantSimEngine.jl/tree/main/examples). ## Example with a single light interception model and a single weather timestep diff --git a/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md b/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md index 3764d3e8b..ddfdb024d 100644 --- a/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md +++ b/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md @@ -260,16 +260,16 @@ PlantSimEngine.dep(::Process3Model) = (process2=Process2Model,) However, the model provided in the examples, Process2Model is absent from the mapping : ```julia - simple_mtg = Node(MultiScaleTreeGraph.NodeMTG("/", "Plant", 1, 1)) - mapping = Dict( - "Leaf" => ( - Process3Model(), - Status(var5=15.0,) - ) - ) - outs = Dict( - "Leaf" => (:var5,), +simple_mtg = Node(MultiScaleTreeGraph.NodeMTG("/", "Plant", 1, 1)) +mapping = Dict( + "Leaf" => ( + Process3Model(), + Status(var5=15.0,) ) +) +outs = Dict( + "Leaf" => (:var5,), +) run!(simple_mtg, mapping, meteo_day, tracked_outputs=outs) ERROR: type NamedTuple has no field process2 @@ -278,7 +278,7 @@ Stacktrace: @ Base ./Base.jl:49 [2] run!(::Process3Model, models::@NamedTuple{…}, status::Status{…}, meteo::DataFrameRow{…}, constants::Constants{…}, extra::PlantSimEngine.GraphSimulation{…}) ... - ``` +``` The fix is to add Process2Model() -or an other model for the same process- to the mapping. @@ -334,7 +334,7 @@ often indicate a likely syntax error somewhere in the mapping definition. This situation won't trigger an error. Unexpectedly empty vectors can be returned as outputs if you happen to forget to a node at the corresponding scale in the MTG, and no organ creation occurs for that node. -Here's an example taken from TODO, removing the "Plant" node in the dummy MTG. Only "Scene"-scale models can run initially, and since no nodes are created, "Plant"-scale models will never be run. +Here's an example taken from the [Converting a single-scale simulation to multi-scale](@ref) page. It was modified by removing the "Plant" node in the dummy MTG passed into the `run!`function. Without that "Plant" node, only "Scene"-scale models can run initially, and since no nodes are created, "Plant"-scale models will never be run. ```julia PlantSimEngine.@process "tt_cu" verbose = false diff --git a/docs/src/troubleshooting_and_testing/tips_and_workarounds.md b/docs/src/troubleshooting_and_testing/tips_and_workarounds.md index 43805b8e9..396f3f8ff 100644 --- a/docs/src/troubleshooting_and_testing/tips_and_workarounds.md +++ b/docs/src/troubleshooting_and_testing/tips_and_workarounds.md @@ -38,7 +38,9 @@ For example, one model in [XPalm.jl](https://github.com/PalmStudio/XPalm.jl/blob TODO use toy plant as example -## Multiscale : passing in a vector in a mapping status at a specific scale +## [Multiscale : passing in a vector in a mapping status at a specific scale](@id multiscale_vector) + +TODO example from single to multiscale You may have noticed that sometimes a vector (1-dimensional array) variable is passed into the `status` component of a `ModelList` in documentation examples (An example here with cumulative thermal time : [Model switching](@ref)). From 0d9d215e009421e2e6e80b530f12657abc5515a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Vezy?= Date: Thu, 6 Mar 2025 14:34:19 +0100 Subject: [PATCH 081/147] Update README.md Clearer message for newcomers --- README.md | 55 +++++++++++++++++++++++++------------------------------ 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index a0b101df9..6b77ba8d6 100644 --- a/README.md +++ b/README.md @@ -9,31 +9,27 @@ [![DOI](https://zenodo.org/badge/571659510.svg)](https://zenodo.org/badge/latestdoi/571659510) [![JOSS](https://joss.theoj.org/papers/137e3e6c2ddc349bec39e06bb04e4e09/status.svg)](https://joss.theoj.org/papers/137e3e6c2ddc349bec39e06bb04e4e09) - ## Overview -`PlantSimEngine` is a modelling framework for simulating and modelling plants, soil and atmosphere. It provides tools to **prototype, evaluate, test, and deploy** plant/crop models at any scale, with a strong emphasis on performance and efficiency. - -**Key Features:** +`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. -- Process Definition: Easily define new processes such as light interception, photosynthesis, growth, soil water transfer, and more. -- Interactive Prototyping: Fast and interactive prototyping of models with built-in constraints to avoid errors and sensible defaults to streamline the model writing process. -- Control Degrees of Freedom: Fix variables, pass measurements, or use simpler models for specific processes to reduce complexity. -- Automatic Management: The package automatically manages input and output variables, time-steps, objects, and the coupling of models using a dependency graph. -- Flexible Model Switching: Switch between models without changing any code, using a simple syntax to specify the model for a given process. -- Integrated Data Use: Force variables to take measured values instead of model predictions, reducing degrees of freedom during model development and increasing accuracy during production mode. -- High-Performance Computation: Achieve high-speed computations, with benchmarks showing operations in the 100th of nanoseconds range for complex models (see this [benchmark script](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/benchmark.jl)). -- Parallel and Distributed Computing: Out-of-the-box support for sequential, multi-threaded, or distributed computations over objects, time-steps, and independent processes, thanks to [Floops.jl](https://juliafolds.github.io/FLoops.jl/stable/). -- Scalability: Scale easily with methods for computing over objects, time-steps, and [Multi-Scale Tree Graphs](https://github.com/VEZY/MultiScaleTreeGraph.jl). -- Composability: Use any types as inputs, including [Unitful](https://github.com/PainterQubits/Unitful.jl) for unit propagation and [MonteCarloMeasurements.jl](https://github.com/baggepinnen/MonteCarloMeasurements.jl) for propagating measurement error. +**Why choose PlantSimEngine?** -**Benefits:** +- 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. -Improved Accuracy and Reliability: +## Batteries included -- Enhance the accuracy of plant growth and yield predictions by integrating detailed physiological processes and environmental interactions. -- Reduced Modeling Time: Streamline the modeling process with automated management and fast prototyping capabilities. -- Collaborative Research: Facilitate collaborative research efforts with flexible and composable modeling tools. +- Automated management of inputs, outputs, time-steps, objects, and dependency resolution. +- Iterative model 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. +- Flexible Model Switching: Switch between models without changing model's code, using a simple syntax to specify the model for a given process and scale. +- Achieve high-speed computations, with benchmarks showing operations in the 100th of nanoseconds range for complex models (see this [benchmark script](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/benchmark.jl)). +- 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](https://juliafolds.github.io/FLoops.jl/stable/). +- Scale: Scale easily with methods for computing over objects, time-steps, and [Multi-Scale Tree Graphs](https://github.com/VEZY/MultiScaleTreeGraph.jl). +- Compose: Use any types as inputs, including [Unitful](https://github.com/PainterQubits/Unitful.jl) for unit propagation and [MonteCarloMeasurements.jl](https://github.com/baggepinnen/MonteCarloMeasurements.jl) for measurement error propagation. ## Ask Questions @@ -57,7 +53,7 @@ using PlantSimEngine The package is designed to be easy to use, and to help users avoid errors when implementing, coupling and simulating models. -### Simple example +### Simple example Here's a simple example of a model that simulates the growth of a plant, using a simple exponential growth model: @@ -193,13 +189,13 @@ fig ![LAI Growth and light interception](examples/LAI_growth2.png) -### Multiscale modelling +### Multiscale modelling -> See the [Multi-scale modeling](#multi-scale-modeling) section for more details. +> See the Multi-scale modeling section of the docs 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: -```@example readme +```julia mapping = Dict( "Scene" => ToyDegreeDaysCumulModel(), "Plant" => ( @@ -254,13 +250,13 @@ mapping = Dict( We can import an example plant from the package: -```@example readme +```julia mtg = import_mtg_example() ``` Make a fake meteorological data: -```@example readme +```julia meteo = Weather( [ Atmosphere(T=20.0, Wind=1.0, Rh=0.65, Ri_PAR_f=300.0), @@ -271,7 +267,7 @@ meteo = Weather( And run the simulation: -```@example readme +```julia out_vars = Dict( "Scene" => (:TT_cu,), "Plant" => (:carbon_allocation, :carbon_assimilation, :soil_water_content, :aPPFD, :TT_cu, :LAI), @@ -285,7 +281,7 @@ out = run!(mtg, mapping, meteo, outputs=out_vars, executor=SequentialEx()); We can then extract the outputs in a `DataFrame` and sort them: -```@example readme +```julia using DataFrames df_out = convert_outputs(out, DataFrame) sort!(df_out, [:timestep, :node]) @@ -310,7 +306,6 @@ sort!(df_out, [:timestep, :node]) | 2 | Internode | 8 | 0.0627036 | | | | | | 0.75 | | 2 | Leaf | 9 | 0.0627036 | | | | | | 0.75 | - An example output of a multiscale simulation is shown in the documentation of PlantBiophysics.jl: ![Plant growth simulation](docs/src/www/image.png) @@ -322,8 +317,8 @@ Take a look at these projects that use PlantSimEngine: - [PlantBiophysics.jl](https://github.com/VEZY/PlantBiophysics.jl) - [XPalm](https://github.com/PalmStudio/XPalm.jl) -## Make it yours +## 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. +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! 😃 Make sure to read the community guidelines before in case you're not familiar with such things. From b0ea66c05b41d9c008046a36d3c645732996aed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Vezy?= Date: Thu, 6 Mar 2025 14:42:30 +0100 Subject: [PATCH 082/147] Update index.md following changes in the readme --- docs/src/index.md | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index 83c0335bd..94831a89f 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -45,21 +45,29 @@ Depth = 5 ## Overview -`PlantSimEngine` is a comprehensive package for simulating and modelling plants, soil and atmosphere. It provides tools to **prototype, evaluate, test, and deploy** plant/crop models at any scale. At its core, PlantSimEngine is designed with a strong emphasis on performance and efficiency. +`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. -The package defines a framework for declaring processes and implementing associated models for their simulation. +**Why choose PlantSimEngine?** -It focuses on key aspects of simulation and modeling such as: +- 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. -- Easy definition of new processes, such as light interception, photosynthesis, growth, soil water transfer... -- Fast, interactive prototyping of models, with constraints to help users avoid errors, but sensible defaults to avoid over-complicating the model writing process -- No hassle, the package manages automatically input and output variables, time-steps, objects, soft and hard coupling of models with a dependency graph -- Switch between models without changing any code, with a simple syntax to define the model to use for a given process -- Reduce the degrees of freedom by fixing variables, passing measurements, or using a simpler model for a given process -- 🚀(very) fast computation 🚀, think of 100th of nanoseconds for one model, two coupled models (see this [benchmark script](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/benchmark.jl)), or the full energy balance of a leaf using [PlantBiophysics.jl](https://github.com/VEZY/PlantBiophysics.jl) that uses PlantSimEngine -- Out of the box Sequential, Parallel (Multi-threaded) or Distributed (Multi-Process) computations over objects, time-steps and independent processes (thanks to [Floops.jl](https://juliafolds.github.io/FLoops.jl/stable/)) -- Easily scalable, with methods for computing over objects, time-steps and even [Multi-Scale Tree Graphs](https://github.com/VEZY/MultiScaleTreeGraph.jl) -- Composable, allowing the use of any types as inputs such as [Unitful](https://github.com/PainterQubits/Unitful.jl) to propagate units, or [MonteCarloMeasurements.jl](https://github.com/baggepinnen/MonteCarloMeasurements.jl) to propagate measurement error +## Batteries included + +- Automated management of inputs, outputs, time-steps, objects, and dependency resolution. +- Iterative model 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. +- Flexible Model Switching: Switch between models without changing model's code, using a simple syntax to specify the model for a given process and scale. +- Achieve high-speed computations, with benchmarks showing operations in the 100th of nanoseconds range for complex models (see this [benchmark script](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/benchmark.jl)). +- 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](https://juliafolds.github.io/FLoops.jl/stable/). +- Scale: Scale easily with methods for computing over objects, time-steps, and [Multi-Scale Tree Graphs](https://github.com/VEZY/MultiScaleTreeGraph.jl). +- Compose: Use any types as inputs, including [Unitful](https://github.com/PainterQubits/Unitful.jl) for unit propagation and [MonteCarloMeasurements.jl](https://github.com/baggepinnen/MonteCarloMeasurements.jl) for measurement error propagation. + +## Ask Questions + +If you have any questions or feedback, [open an issue](https://github.com/VirtualPlantLab/PlantSimEngine.jl/issues) or ask on [discourse](https://fspm.discourse.group/c/software/virtual-plant-lab). ## Installation @@ -79,7 +87,7 @@ using PlantSimEngine The package is designed to be easy to use, and to help users avoid errors when implementing, coupling and simulating models. -### Simple example +### Simple example Here's a simple example of a model that simulates the growth of a plant, using a simple exponential growth model: @@ -170,7 +178,7 @@ lines!(ax2, out2[:TT_cu], out2[:aPPFD], color=:firebrick1) fig ``` -### Multiscale modelling +### Multiscale modelling > See the [Multi-scale modeling](#multi-scale-modeling) section for more details. @@ -282,8 +290,8 @@ Take a look at these projects that use PlantSimEngine: - [PlantBiophysics.jl](https://github.com/VEZY/PlantBiophysics.jl) - [XPalm](https://github.com/PalmStudio/XPalm.jl) -## Make it yours +## 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. +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! 😃 \ No newline at end of file +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! 😃 From 2999bfcbd65c437cf9540889f9d68e939135aae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Vezy?= Date: Thu, 6 Mar 2025 14:53:09 +0100 Subject: [PATCH 083/147] Update why_julia.md Fill the TODOs I could. --- docs/src/introduction/why_julia.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/src/introduction/why_julia.md b/docs/src/introduction/why_julia.md index bbdf11f0e..3678f518b 100644 --- a/docs/src/introduction/why_julia.md +++ b/docs/src/introduction/why_julia.md @@ -8,9 +8,9 @@ Other modelling frameworks, FSPMs and crop models are -often- written in combina PlantSimEngine is a goal oriented framework. Its features arose -and continue to evolve- out of necessity for more and more complex simulations. It wasn't a pre-designed piece of software. -It was therefore originally a means to an end, and not a product in itself. +It was therefore originally a means to an end, and not a product in itself. -TODO +PlantSimEngine is designed to balance scientific rigor with developer productivity. Researchers need to focus primarily on the scientific aspects of their models while still creating efficient, maintainable code. Julia provides an ideal environment where researchers can express complex mathematical concepts directly in code without sacrificing performance. ## PlantSimEngine's constraints @@ -22,11 +22,11 @@ Simulating multiple processes with user-provided variables over many plants with In fact, part of the initial motivation to commit to Julia happened after porting an ecophysiological simulation from R to Julia and getting an order of magnitude difference in performance 'out-of-the-box'. -Julia, with its 'Just-ahead-of-time' compilation model and its flexibility allowing to do some lower-level optimisation, doesn't suffer from the limitations one would encounter by using only Python or R. +Julia, with its 'Just-ahead-of-time' compilation model and its flexibility allowing to do some lower-level optimisation, doesn't suffer from the limitations one would encounter by using only Python or R. ### Flexibility, ease of use -PlantSimEngine was also developed with a few goals in mind, one of them being to make hypothesis testing quite easy. It is currently difficult to validate FSPM, crop model or ecophysiological hypotheses TODO +PlantSimEngine was also developed with a few goals in mind, one of them being to make hypothesis testing quite easy. It is currently difficult to validate FSPM, crop model or ecophysiological hypotheses in many existing frameworks due to their rigid structure or steep learning curve. Similarly, when developing a full-featured FSPM, there might be a need to test different models for a specific process, or to switch a model for a more complex one. API and language ease of use is as much of a factor as automated model coupling in keeping these changes smooth. @@ -64,7 +64,7 @@ It seems we aren't the only ones to feel Julia is the right tool for our job for ### A good balance in terms of accessibility -Another argument in favour of Julia is that one of the aims for PlantSimEngine is to be easy-to-use for researchers wishing to test hypothesis, or reproduce results from other papers. TODO +Another argument in favour of Julia is that one of the aims for PlantSimEngine is to be easy-to-use for researchers wishing to test hypothesis, or reproduce results from other papers. Scientific reproducibility is greatly enhanced when the barrier to running and modifying simulations is lowered. Many researchers are not developers by trade or heart, and a Java-only or C++-only implementation, on top of the earlier points, would not be accessible enough and would not gain much traction. @@ -76,8 +76,16 @@ Users will also find it easier to quickly implement new models without the poten Similarly, Julia's language and package installation is -mostly- fairly straightforward and requires little additional knowledge. +The package manager is built directly into the language, making dependency management straightforward. This is particularly important for reproducible scientific workflows, where consistent environments are crucial. + ### Downsides acceptable While very practical for a 'researcher-developer', Julia is of course far from being the perfect language in every discipline. It is massive in terms of features, has a heavy runtime, is more involved to learn and master quickly compared to Python, has a few hurdles for beginners, some quirks that can be awkward for developers, tools that aren't fully mature, no clear 'recommended' workflow, and so on. -The cost for switching may not be worth it in many other circumstances. However, several of these downsides, while very relevant for embedded systems, or game development, are much less relevant regarding PlantSimEngine. And others can be mitigated with, hopefully, adequate learning resources and documentation. \ No newline at end of file +The cost for switching may not be worth it in many other circumstances. However, several of these downsides, while very relevant for embedded systems, or game development, are much less relevant regarding PlantSimEngine. And others can be mitigated with, hopefully, adequate learning resources and documentation. + +## Conclusion + +For PlantSimEngine's specific requirements—balancing performance with flexibility, enabling rapid iteration while maintaining computational efficiency, and providing an accessible interface for researchers—Julia represents an optimal choice. The language allows us to build an ecosystem where plant modeling can advance through collaborative, efficient, and scientifically rigorous development. + +While no language solution is perfect, Julia's unique combination of features makes it particularly well-suited to the challenges of modern plant modeling and simulation. \ No newline at end of file From 2ecf44e0c53ee79ae596ee4fc32bfbfeff78c540 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Thu, 6 Mar 2025 15:12:36 +0100 Subject: [PATCH 084/147] Fixing up many broken refs and TODOs, still a fair few left --- docs/src/multiscale/multiscale.md | 6 ++-- docs/src/multiscale/multiscale_coupling.md | 4 +-- docs/src/multiscale/multiscale_example_1.md | 5 ++-- docs/src/multiscale/multiscale_example_2.md | 2 +- docs/src/multiscale/multiscale_example_3.md | 28 ++++++++++--------- docs/src/multiscale/single_to_multiscale.md | 6 ++-- docs/src/prerequisites/key_concepts.md | 14 ++++------ .../step_by_step/detailed_first_example.md | 9 +++--- docs/src/step_by_step/implement_a_model.md | 4 +-- .../implement_a_model_additional.md | 2 +- docs/src/step_by_step/model_switching.md | 4 +-- .../src/step_by_step/simple_model_coupling.md | 2 ++ .../implicit_contracts.md | 2 +- .../floating_point_accumulation_error.md | 19 +++++++++++-- 14 files changed, 59 insertions(+), 48 deletions(-) diff --git a/docs/src/multiscale/multiscale.md b/docs/src/multiscale/multiscale.md index cdf43309f..2eeb718cc 100644 --- a/docs/src/multiscale/multiscale.md +++ b/docs/src/multiscale/multiscale.md @@ -2,7 +2,7 @@ The previous page showed how to convert a single-scale simulation to multi-scale. -This page provides another example showcasing the nuances in variable mapping, with a more complex fully multiscale version of a prior simulation. The models will all be taken form the examples folder []TODO +This page provides another example showcasing the nuances in variable mapping, with a more complex fully multiscale version of a prior simulation. The models will all be taken form the [examples folder](https://github.com/VirtualPlantLab/PlantSimEngine.jl/tree/main/examples). ## Starting with a single-model mapping @@ -13,8 +13,8 @@ using PlantSimEngine using PlantSimEngine.Examples # Import some example models ``` -Let's create a simple mapping with only one initial model, the carbon assimilation process `ToyAssimModel`. -It was previously used in a single-scale simulation TODO, but we will have it be used in a more fine-grained manner and operate on leaves in this example. +Let's create a simple mapping with only one initial model, the carbon assimilation process `ToyAssimModel`, which will operate on leaves. +It resembles the `ToyAssimGrowth` model used in the single-scale simulation [Example model switching](@ref) subsection. Our mapping between scale and model is therefore: diff --git a/docs/src/multiscale/multiscale_coupling.md b/docs/src/multiscale/multiscale_coupling.md index 7a7f5a9b7..c3e94e08f 100644 --- a/docs/src/multiscale/multiscale_coupling.md +++ b/docs/src/multiscale/multiscale_coupling.md @@ -69,7 +69,7 @@ Conceptually : ### An example from the toy plant simulation tutorial -TODO example discussed in toy plant +You can find an example of a hard dependency discussed in the [A multi-scale hard dependency appears](@ref) subsection of the third part of toy plant tutorial. ### An example from XPalm.jl @@ -113,7 +113,7 @@ end ## Implementation details: accessing a hard dependency's variables from a different scale -But how does a model M calling a hard dependency H provide H's variables when calling H's `run!` function ? The status the user provides M operates at M's organ level, so if used to call H's run! function any required variable for H will be missing. +But how does a model M calling a hard dependency H provide H's variables when calling H's `run!` function ? The `status` argument the user provides M operates at M's organ level, so if used to call H's run! function any required variable for H will be missing. PlantSimEngine provides what are called Status Templates in the simulation graph. Each organ level has its own Status template listing the available variables at that scale. So when a model M calls a hard dependency H's `run!` function, any required variables can be accessed through the status template of H's organ level. diff --git a/docs/src/multiscale/multiscale_example_1.md b/docs/src/multiscale/multiscale_example_1.md index 3b8882970..23831ebfc 100644 --- a/docs/src/multiscale/multiscale_example_1.md +++ b/docs/src/multiscale/multiscale_example_1.md @@ -77,9 +77,7 @@ end ### Resource storage -The model storing resources for the whole plant needs a couple of inputs : the amount of carbon captured by the leaves, as well as the amount consumed by the creation of new organs. It outputs the current stock. - -TODO +The model storing resources for the whole plant needs a couple of inputs: the amount of carbon captured by the leaves, as well as the amount consumed by the creation of new organs. It outputs the current stock. ```julia PlantSimEngine.@process "resource_stock_computation" verbose = false @@ -267,3 +265,4 @@ And that's it ! Feel free to tinker with the parameters and see when things brea Of course, this is a very crude and unrealistic simulation, with many dubious assumptions and parameters. But significantly more complex modelling is possible using the same approach : XPalm runs using a few dozen models spread out over nine scales. +This is a three-part tutorial and continues in the [Expanding on the multiscale simulation](@ref) page. \ No newline at end of file diff --git a/docs/src/multiscale/multiscale_example_2.md b/docs/src/multiscale/multiscale_example_2.md index 40af9f2aa..55de510a1 100644 --- a/docs/src/multiscale/multiscale_example_2.md +++ b/docs/src/multiscale/multiscale_example_2.md @@ -224,4 +224,4 @@ And that's it ! ...Or is it ? -If you inspect the code and output data closely, you may notice some distinctive problems with the way the simulation runs... Some things aren't quite right. If you wish to know more, onwards to the next chapter : TODO \ No newline at end of file +If you inspect the code and output data closely, you may notice some distinctive problems with the way the simulation runs... Some things aren't quite right. If you wish to know more, onwards to the next chapter: [Fixing bugs in the plant simulation](@ref) \ No newline at end of file diff --git a/docs/src/multiscale/multiscale_example_3.md b/docs/src/multiscale/multiscale_example_3.md index 8bc009026..ca20bde65 100644 --- a/docs/src/multiscale/multiscale_example_3.md +++ b/docs/src/multiscale/multiscale_example_3.md @@ -10,10 +10,12 @@ There is one quirk you may have noticed when inspecting the data : when a root e This is an implementation decision in PlantSimEngine. By default, new organs are active, and models can affect them as soon as they are created. -The internode growth also depends on a threshold thermal time value, so doesn't immediately expand within a single timestep. XPalm's organ emission models TODO also +The internode growth depends on a threshold thermal time value, which accumulates over several timesteps, so even though new internodes are immediately active, they can't themselves grow new organs within the same timestep. + +This quirk is also avoided in [XPalm.jl](https://github.com/PalmStudio/XPalm.jl), a package using PlantSimEngine: some organs make use of state machines, and are considered "immature" when they are created. Immature organs cannot grow new organs until some conditions are met for their state to change. There are also other conditions governing organ emergence, such as specific threshold values relating to Thermal Time (see [here](https://github.com/PalmStudio/XPalm.jl/blob/433e1c47c743e7a53e764672818a43ed8feb10c6/src/plant/phytomer/leaves/phyllochron.jl#L46) for an example). !!! Note - This may be subject to change in future versions of PlantSimEngine. Also note that the way the dependency graph is structured determines the order in which models run. Meaning that which models are run before or after organ creation might change with new additions and updates to your mapping. Some models might run "one timestep later". + This implementation decision for new organs to be immediately active may be subject to change in future versions of PlantSimEngine. Also note that the way the dependency graph is structured determines the order in which models run. Meaning that which models are run before or after organ creation might change with new additions and updates to your mapping. Some models might run "one timestep later", see [Simulation order instability when adding models](@ref) for more details. How do we avoid this extreme instant growth ? We can, of course, add some thermal time constraint. We could arbitrarily tinker with water resources. @@ -21,21 +23,19 @@ We can otherwise add a simple state machine variable to our root and internodes In fact, we could change the scale at which the check is made to extend the root, and have another model call this one directly. This enables running this model only for the end root when those occasional timesteps when root growth is possible, instead of at every timestep for every root node. -You can find several similar patterns in XPalm TODO. - -## Fixing resource computation +## Fixing resource computation: a root growth decision model Another problem you may have noticed, is that the water and carbon stock are computed by aggregating photosynthesis over leaves and absorption over roots... But they aren't always properly decremented when consumed ! -If the end root grows, it outputs a carbon_root_creation_consumed value, but under certain conditions, we might also create other roots and internodes even when there shouldn't be enough carbon left for them. +If the end root grows, it outputs a `carbon_root_creation_consumed` value, but under certain conditions, we might also create other roots and internodes even when there shouldn't be enough carbon left for them. Indeed, if both the root and leaf water thresholds are met, and there is enough carbon for a single root or internode but not for both, and the root model runs before the internode model, both will use the carbon_stock variable prior to organ emission. The internode emission model won't account for the root carbon consumption. -This occurs because carbon_stock is only computed once, and won't update until the next timestep. +This occurs because `carbon_stock` is only computed once, and won't update until the next timestep. -To avoid that problem in our specific case, we can couple the root growth model and the internode emission model, and pass the carbon_root_creation_consumed so that internode emission can take it into account. Or we could have an intermediate model recompute the new stock to pass along to the internode emission model. +To avoid that problem in our specific case, we can couple the root growth model and the internode emission model, and pass the `carbon_root_creation_consumed` variable to the internode emission model so that it can use an updated carbon stock. Or we could have an intermediate model recompute the new stock to pass along to the internode emission model. -There is a section in the 'Tips and workarounds' page discussing this situation and other potential solutions: [Having a variable simultaneously as input and output of a model](@ref). +There is a section in the [Tips and workarounds] page discussing this situation and other potential solutions: [Having a variable simultaneously as input and output of a model](@ref). We'll go for the first option and couple the root growth and internode emission model. @@ -57,9 +57,11 @@ The only change required for our internode emission model is to take into accoun Our root growth decision model inherits some of the responsibility from last chapter's root growth model, so inputs, parameters and condition checks will be similar. We'll let the root growth model keep the length check and only focus on resources. -Since the decision model is now directly responsible for calling the actual root growth model, we need to declare that it requires a root growth model as a hard dependency and cannot be run standalone. TODO see. +Since the decision model is now directly responsible for calling the actual root growth model, we need to declare that it requires a root growth model as a hard dependency and cannot be run standalone. + +This hard dependency is in fact multiscale, since both models operate at different scales, "Plant" and "Root". You can read more about multi-scale hard dependencies in the [Handling dependencies in a multiscale context](@ref) page. -This hard dependency is in fact multiscale, since both models operate at different scales, "Plant" and "Root". To specify which scale the hard dependency comes from, the declaration additionally requires mapping the scale, compared to the single-scale equivalent: +Compared to the single-scale equivalent, the multi-scale declaration additionally requires mapping the scale: ```julia PlantSimEngine.dep(::ToyRootGrowthDecisionModel) = (root_growth=AbstractRoot_GrowthModel=>["Root"],) @@ -116,6 +118,8 @@ end The root growth model will output the `carbon_root_creation_consumed` computation, but it'll still be exposed to downstream models despite the root growth model being a 'hidden' model in the dependency graph due to its hard dependency nature. +With this new coupling, we will only be creating at most a single new root per timestep, as the root growth decision will only be called once per timestep. + ### Root growth This iteration turns into a simplifed version of last chapter's. @@ -147,8 +151,6 @@ function PlantSimEngine.run!(m::ToyRootGrowthModel, models, status, meteo, const end ``` -TODO state machine - ### Mapping adjustments The new mapping only has straightforward changes. Some models cease to be multi-scale, others require new variables to be mapped for them. `carbon_root_creation_consumed` ceases to be a vector mapping and is a scalar variable. diff --git a/docs/src/multiscale/single_to_multiscale.md b/docs/src/multiscale/single_to_multiscale.md index a2ebd2490..62e4e218f 100644 --- a/docs/src/multiscale/single_to_multiscale.md +++ b/docs/src/multiscale/single_to_multiscale.md @@ -106,10 +106,10 @@ We now have our model implementation. How does it fit into our mapping ? Our new model doesn't really relate to a specific organ of our plant. In fact, this model doesn't represent a physiological process of the plant, but rather an environmental process affecting its physiology. We could therefore have it operate at a different scale unrelated to the plant, which we'll call "Scene". This makes sense. -Note that we now need to add a "Scene" node to our Multi-scale Tree Graph, otherwise our model will not run, since no other model calls it. See []TODO +Note that we now need to add a "Scene" node to our Multi-scale Tree Graph, otherwise our model will not run, since no other model calls it and "Plant" nodes will only call models at the "Plant" scale. See [Empty status vectors in multi-scale simulations](@ref) for more details. ```julia -mtg_multiscale = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Plant", 0, 0),) +mtg_multiscale = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Scene", 0, 0),) plant = MultiScaleTreeGraph.Node(mtg_multiscale, MultiScaleTreeGraph.NodeMTG("+", "Plant", 1, 1)) ``` @@ -194,7 +194,7 @@ is_approx_equal = length(unique(multiscale_TT_cu .≈ out_singlescale.TT_cu)) == ``` !!! note - You may be wondering why we check for approximate equality rather than strict equality. The reason for that is due to floating-point accumulation errors, which are discussed in more detail here TODO. + You may be wondering why we check for approximate equality rather than strict equality. The reason for that is due to floating-point accumulation errors, which are discussed in more detail in [Floating-point considerations](@ref). ## ToyDegreeDaysCumulModel diff --git a/docs/src/prerequisites/key_concepts.md b/docs/src/prerequisites/key_concepts.md index bb68e17f3..3c4baf86b 100644 --- a/docs/src/prerequisites/key_concepts.md +++ b/docs/src/prerequisites/key_concepts.md @@ -58,11 +58,11 @@ PlantSimEngine handles this internally by not having those "heavily-coupled" mod This approach does have implications when developing interdependent models : hard dependencies need to be made explicit, and the ancestor needs to call the hard dependency model's `run!` function explicitely in its own `run!` function. Hard dependency models therefore must have only one parent model. -You can find an example TODO +You can find a typical example in a companion package: [PlantBioPhysics.jl](). An energy balance model, the [Monteith model](https://github.com/VEZY/PlantBiophysics.jl/blob/master/src/processes/energy/Monteith.jl), needs to [iteratively run a photosynthesis model](https://github.com/VEZY/PlantBiophysics.jl/blob/c1a75f294109d52dc619f764ce51c6ca1ea897e8/src/processes/energy/Monteith.jl#L154) in its `run!` function. -This makes them slightly more complex to develop and validate, and less versatile than other models. Occasional refactoring may be necessary to handle a hard dependency creeping up when adding new models to a simulation. +This reliance on another process makes these models slightly more complex to develop and validate, and less versatile than simpler models. Occasional refactoring may be necessary to handle a hard dependency creeping up when adding new models to a simulation. -Note that hard dependencies can also have their own hard dependencies, and some complex couplings are therefore possible. A hard dependency model can have another hard dependency model as a parent. +Note that hard dependencies can also have their own hard dependencies, and some complex couplings can happen. A hard dependency model can have another hard dependency model as a parent. ### Weather data @@ -103,7 +103,7 @@ This example excerpt links specific models to a specific scale. Note that one mo ### Multiscale modeling -Multi-scale modeling is the process of simulating a system at multiple levels of detail simultaneously. For example, some models can run at the organ scale while others run at the plot scale. Each model can access variables at its scale and other scales if needed, allowing for a more comprehensive system representation. It can also help identify emergent properties that are not apparent at a single level of detail. +Multi-scale modeling is the process of simulating a system at multiple levels of detail simultaneously. Some models might run at the organ scale while others run at the plot scale. Each model can access variables at its scale and other scales if needed, allowing for a more comprehensive system representation. It can also help identify emergent properties that are not apparent at a single level of detail. For example, a model of photosynthesis at the leaf scale can be combined with a model of carbon allocation at the plant scale to simulate the growth and development of the plant. Another example is a combination of models to simulate the energy balance of a forest. To simulate it, you need a model for each organ type of the plant, another for the soil, and finally, one at the plot scale, integrating all others. @@ -111,7 +111,7 @@ When running multi-scale simulations which contain models operating at different This is why multi-scale simulations make use of a 'mapping' : the ModelList in the single-scale examples does not have a way to tie models to plant organs,and the more versatile models could be used in various places. The user must also indicate how models operate with other scales, e.g. if an input variable comes from another scale, it is required to indicate which scale it is mapped from. -TODO +You can read more about some practical differences as a user between single- and multi-scale simulations here: [Multi-scale considerations](@ref). !!! Note When you encounter the terms "Single-scale simulations", or "ModelList simulations", they will refer to simulations that are "not multi-scale". A multi-scale simulation makes use of a mapping between different organ/scale levels. A single-scale simulation has no such mapping, and uses the simpler ModelList interface. @@ -133,7 +133,3 @@ TODO lien avec AMAP ? TODO scale, symbol terminology ambiguity ### State machines - - - -TODO differences mono/multiscale ? \ No newline at end of file diff --git a/docs/src/step_by_step/detailed_first_example.md b/docs/src/step_by_step/detailed_first_example.md index 0a0ca05b3..12eb2d2df 100644 --- a/docs/src/step_by_step/detailed_first_example.md +++ b/docs/src/step_by_step/detailed_first_example.md @@ -2,8 +2,7 @@ This page walks you through the ins and outs of a basic simulation, mostly aimed at people who have less experience programming, to showcase the various concepts presented earlier and requirements for a simulation in context. -The full example discussed in this page can be found further down(TODO ref Example simulation). - +A working trimmed-down script can be found further down in the [Example simulation](@ref), and other subsections in this page will detail setup and helper functions, and querying outputs. ```@setup usepkg using PlantSimEngine, PlantMeteo @@ -214,8 +213,8 @@ Or similarly using the dot syntax: outputs_example.aPPFD ``` -You can then print the outputs, convert them to another format, or visualize them, using other Julia packages. -TODO +You can then print the outputs, convert them to another format, or visualize them, using other Julia packages. You can read more on how to do that in the [Visualizing outputs](@ref) page. + Another convenient way to get the results is to transform the outputs into a `DataFrame`. Which is very easy because the `TimeStepTable` implements the Tables.jl interface: ```@example usepkg @@ -227,4 +226,4 @@ convert_outputs(outputs_example, DataFrame) A model can work either independently or in conjunction with other models. For example a stomatal conductance model is often associated with a photosynthesis model, *i.e.* it is called from the photosynthesis model. -`PlantSimEngine.jl` is designed to make model coupling painless for modelers and users. Please see [Standard model coupling](@ref) and [Coupling more complex models](@ref) for more details, or Multiscale coupling considerations TODO for multi-scale specific coupling considerations. \ No newline at end of file +`PlantSimEngine.jl` is designed to make model coupling painless for modelers and users. Please see [Standard model coupling](@ref) and [Coupling more complex models](@ref) for more details, or [Handling dependencies in a multiscale context](@ref) for multi-scale specific coupling considerations. diff --git a/docs/src/step_by_step/implement_a_model.md b/docs/src/step_by_step/implement_a_model.md index 2c6bf30aa..3e833848b 100644 --- a/docs/src/step_by_step/implement_a_model.md +++ b/docs/src/step_by_step/implement_a_model.md @@ -127,7 +127,7 @@ We can therefore infer from the declaration that `Beer` is a model to simulate t Then come the parameters names, and their types. -### Parametric types +### User types and parametric types There is a little Julia specificity here, to enable the user to pass their own types to the simulation. @@ -145,7 +145,7 @@ struct CustomModel{T,S} <: AbstractLight_InterceptionModel end ``` -Parameterized types are practical because they let the user choose the type of the parameters, and potentially change them at runtime. For example a user could use the `Particles` type from [MonteCarloMeasurements.jl](https://github.com/baggepinnen/MonteCarloMeasurements.jl) for automatic uncertainty propagation throughout the simulation. We refer you to [this page]TODO for more information on parametric types. +Parameterized types are practical because they let the user choose the type of the parameters, and potentially change them at runtime. For example a user could use the `Particles` type from [MonteCarloMeasurements.jl](https://github.com/baggepinnen/MonteCarloMeasurements.jl) for automatic uncertainty propagation throughout the simulation. We refer you to the [Parametric types](@ref) subsection of the [Model implementation additional notes](@ref) page for more information on parametric types. ### Inputs and outputs diff --git a/docs/src/step_by_step/implement_a_model_additional.md b/docs/src/step_by_step/implement_a_model_additional.md index 23eb9f1e0..007ce8d40 100644 --- a/docs/src/step_by_step/implement_a_model_additional.md +++ b/docs/src/step_by_step/implement_a_model_additional.md @@ -2,7 +2,7 @@ ## Parametric types -In Implementing a model(TODO ref), the Beer model's structure was declared with a parametric type. +In [Implementing a model](@ref model_implementation_page), the Beer model's structure was declared with a parametric type. ```julia struct Beer{T} <: AbstractLight_InterceptionModel diff --git a/docs/src/step_by_step/model_switching.md b/docs/src/step_by_step/model_switching.md index 1a1a32138..9d06e3b60 100644 --- a/docs/src/step_by_step/model_switching.md +++ b/docs/src/step_by_step/model_switching.md @@ -91,8 +91,8 @@ output_updated = run!(models2, meteo_day) And that's it! We can switch between models without changing the code, and without having to recompute the dependency graph manually. This is a very powerful feature of PlantSimEngine!💪 !!! note - This was a very standard but easy example. Sometimes other models will require to add other models to the `ModelList`. For example `ToyAssimGrowthModel` could have required a maintenance respiration model. In this case `PlantSimEngine` will tell you that this kind of model is required for the simulation. + This was a very standard but easy example. Sometimes other models will require to add other models to the `ModelList`. For example `ToyAssimGrowthModel` could have required a maintenance respiration model. In this case `PlantSimEngine` will tell you that what kind of model is required for the simulation. !!! note - In our example we replaced a soft-dependency model, but the same principle applies to hard-dependency models. Hard and Soft dependencies are concepts explained [here](@ref hard_dependency_def) TODO remove this note ? + In our example we replaced what we call a [soft-dependency coupling](@ref hard_dependency_def), but the same principle applies to [hard-dependencies](@ref hard_dependency_def). Hard and Soft dependencies are concepts related to model coupling, and are discussed in more detail in [Standard model coupling](@ref) and [Coupling more complex models](@ref). diff --git a/docs/src/step_by_step/simple_model_coupling.md b/docs/src/step_by_step/simple_model_coupling.md index 09e800018..a76a79bf3 100644 --- a/docs/src/step_by_step/simple_model_coupling.md +++ b/docs/src/step_by_step/simple_model_coupling.md @@ -43,6 +43,8 @@ Suppose we want our `ToyLAIModel()` to compute the `LAI` for the light intercept We can couple the two models by having them be part of a single `ModelList`. The `LAI` variable will then be a coupled output-input and no longer will need to be declared. +This is an instance of what we call a ["soft dependency" coupling](@ref hard_dependency_def): a model depends on another model's outputs for its inputs. + Here's a first attempt : ```julia diff --git a/docs/src/troubleshooting_and_testing/implicit_contracts.md b/docs/src/troubleshooting_and_testing/implicit_contracts.md index 2b26e69cc..34a2c9e0f 100644 --- a/docs/src/troubleshooting_and_testing/implicit_contracts.md +++ b/docs/src/troubleshooting_and_testing/implicit_contracts.md @@ -23,7 +23,7 @@ This has been explained elsewhere TODO : the dependency graph is comprised of s Any user model coupling which causes a cyclic dependency to occur will require some extra tinkering to run : either design models differently, create a hard dependency with some of the problematic models, or break the cycle by having a variable take the previous timestep's value as input. -Note : Only the previous timestep is accessible in PlantSimEngine without any kind of dedicated model. How to create a model to store more past timesteps of a specific variable is described here TODO +Note : Only the previous timestep is accessible in PlantSimEngine without any kind of dedicated model. How to create a model to store more past timesteps of a specific variable is described in the [Tips and workarounds](@ref) page: [Making use of past states in multi-scale simulations](@ref) ## Hard dependencies need to be declared in the model definition diff --git a/docs/src/working_with_data/floating_point_accumulation_error.md b/docs/src/working_with_data/floating_point_accumulation_error.md index 851b145f3..933a224c1 100644 --- a/docs/src/working_with_data/floating_point_accumulation_error.md +++ b/docs/src/working_with_data/floating_point_accumulation_error.md @@ -2,7 +2,7 @@ ## Investigating a discrepancy -In TODO page, a single-scale simulation was converted to an equivalent multiscale simulation, and outputs were compared. One detail that was glossed over, but worth bearing in mind when launching simulations is related to floating-point approximations. +In the [Converting a single-scale simulation to multi-scale](@ref) page, a single-scale simulation was converted to an equivalent multiscale simulation, and outputs were compared. One detail that was glossed over, but important to bear in mind as a PlantSimEngine user is related to floating-point approximations. Single-scale simulation: @@ -94,7 +94,7 @@ The divergence isn't huge, but in other situations or over more timesteps it cou ## Floating-point summation -The reason values aren't identical, is due to the fact that many numbers do not have an exact floating point representation. A classical example is 0.3 : +The reason values aren't identical, is due to the fact that many numbers do not have an exact floating point representation. A classical example is the fact that [0.1 + 0.2 != 0.3](https://blog.reverberate.org/2016/02/06/floating-point-demystified-part2.html) : ```julia 0.1 + 0.2 - 0.3 @@ -109,4 +109,17 @@ In our simple example, using Float64 values, the difference wasn't significant e Depending on what value is being computed and the mathematical operations used, changes may range from applying a simple scale to a range of values, to significant refactoring. -TODO links \ No newline at end of file + +## Other links related to floating-point numerical concerns + +Note that many of the examples in these blogposts discuss Float32 accuracy. Float64 values have several extra precision bits to work. + +A series of blog posts on floating-point accuracy : https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/ +Floating-Point Visually Explained : https://fabiensanglard.net/floating_point_visually_explained/ +Examples of floating point problems: https://jvns.ca/blog/2023/01/13/examples-of-floating-point-problems/ + +Relating specifically to floating-point sums: + +Pairwise summation: https://en.wikipedia.org/wiki/Pairwise_summation +Kahan summation: https://en.wikipedia.org/wiki/Kahan_summation_algorithm +Taming Floating-Point Sums : https://orlp.net/blog/taming-float-sums/ \ No newline at end of file From 0f3d8acb5bfa4520072df0c2e110ea344ae09254 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Thu, 6 Mar 2025 15:31:59 +0100 Subject: [PATCH 085/147] Removed more TODOs, added tables of contents to most of the longer pages with many headers --- docs/src/multiscale/multiscale.md | 5 +++++ docs/src/multiscale/multiscale_considerations.md | 9 ++++++++- docs/src/multiscale/multiscale_coupling.md | 5 +++++ docs/src/multiscale/multiscale_example_1.md | 5 +++++ docs/src/multiscale/multiscale_example_2.md | 5 +++++ docs/src/multiscale/multiscale_example_3.md | 5 +++++ docs/src/multiscale/single_to_multiscale.md | 5 +++++ docs/src/planned_features.md | 3 ++- docs/src/prerequisites/key_concepts.md | 5 +++++ docs/src/step_by_step/advanced_coupling.md | 4 ++-- docs/src/step_by_step/detailed_first_example.md | 5 +++++ docs/src/step_by_step/implement_a_model.md | 2 -- docs/src/step_by_step/implement_a_model_additional.md | 5 +++++ docs/src/step_by_step/quick_and_dirty_examples.md | 5 +++++ .../troubleshooting_and_testing/implicit_contracts.md | 11 +++++++---- .../plantsimengine_and_julia_troubleshooting.md | 5 +++++ .../tips_and_workarounds.md | 6 +++++- 17 files changed, 79 insertions(+), 11 deletions(-) diff --git a/docs/src/multiscale/multiscale.md b/docs/src/multiscale/multiscale.md index 2eeb718cc..d6e094d61 100644 --- a/docs/src/multiscale/multiscale.md +++ b/docs/src/multiscale/multiscale.md @@ -4,6 +4,11 @@ The previous page showed how to convert a single-scale simulation to multi-scale This page provides another example showcasing the nuances in variable mapping, with a more complex fully multiscale version of a prior simulation. The models will all be taken form the [examples folder](https://github.com/VirtualPlantLab/PlantSimEngine.jl/tree/main/examples). +```@contents +Pages = ["multiscale.md"] +Depth = 3 +``` + ## Starting with a single-model mapping Let's import the `PlantSimEngine` package and all the example models we will use in this tutorial: diff --git a/docs/src/multiscale/multiscale_considerations.md b/docs/src/multiscale/multiscale_considerations.md index 91c731f37..b5dfbb0b2 100644 --- a/docs/src/multiscale/multiscale_considerations.md +++ b/docs/src/multiscale/multiscale_considerations.md @@ -1,5 +1,10 @@ # Multi-scale considerations +```@contents +Pages = ["multiscale_considerations.md"] +Depth = 3 +``` + This page briefly details the subtle ways in which multi-scale simulations differ from prior single-scale simulations. The next few pages will showcase some of these subtleties with examples. Declaring and running a multi-scale simulation follows the same general workflow as the single-scale version, but multi-scale simulations do have some differences : @@ -13,7 +18,9 @@ The simulation dependency graph will still be computed automatically and handle Multi-scale simulations also tend to require more extra ad hoc models to prepare some variables for some models. -Other pages in this section describe : +## Related pages + +Other pages in the multiscale section describe : - How to write a direct conversion of a single-scale ModelList simulation to a multi-scale simulation and add a second scale to it: [Converting a single-scale simulation to multi-scale](@ref), - A more complex multi-scale version of the single-scale simulation showcasing different variable mappings between scales: [Multi-scale variable mapping](@ref), diff --git a/docs/src/multiscale/multiscale_coupling.md b/docs/src/multiscale/multiscale_coupling.md index c3e94e08f..09792995e 100644 --- a/docs/src/multiscale/multiscale_coupling.md +++ b/docs/src/multiscale/multiscale_coupling.md @@ -1,6 +1,11 @@ # Handling dependencies in a multiscale context +```@contents +Pages = ["multiscale_coupling.md"] +Depth = 3 +``` + ## Scalar and vector variable mappings In the detailed example discussed previously [Multi-scale variable mapping](@ref), there were several instances of mapping a variable from one scale to another, which we'll briefly describe again to help transition to the next and more advanced subsection. Here's a relevant exerpt from the mapping : diff --git a/docs/src/multiscale/multiscale_example_1.md b/docs/src/multiscale/multiscale_example_1.md index 23831ebfc..add7665e4 100644 --- a/docs/src/multiscale/multiscale_example_1.md +++ b/docs/src/multiscale/multiscale_example_1.md @@ -6,6 +6,11 @@ This three-part subsection walks you through building a multi-scale simulation f You can find the full script for the first part's toy simulation in the [ToyMultiScalePlantModel](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/ToyMultiScalePlantModel/ToyPlantSimulation1.jl) subfolder of the examples folder. +```@contents +Pages = ["multiscale_example_1.md"] +Depth = 3 +``` + ## Disclaimer The actual plant being created, as well as some of the custom models, have no real physical meaning and are very much ad hoc (which is why most of them aren't standalone in the examples folder). Similarly, some of the parameter values are pulled out of thin air, and have no ties to research papers or data. diff --git a/docs/src/multiscale/multiscale_example_2.md b/docs/src/multiscale/multiscale_example_2.md index 55de510a1..0bdf34f18 100644 --- a/docs/src/multiscale/multiscale_example_2.md +++ b/docs/src/multiscale/multiscale_example_2.md @@ -4,6 +4,11 @@ Let's build on the previous example and add some other organ growth, as well as You can find the full script for this simulation in the [ToyMultiScalePlantModel](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/ToyMultiScalePlantModel/ToyPlantSimulation2.jl) subfolder of the examples folder. +```@contents +Pages = ["multiscale_example_2.md"] +Depth = 3 +``` + ## Adding roots to our plant We'll add a root that extracts water and adds it to the stock. Initial water stocks are low, so root growth is prioritized, then the plant also grows leaves and a new internode like it did before. Roots only grow up to a certain point, and don't branch. diff --git a/docs/src/multiscale/multiscale_example_3.md b/docs/src/multiscale/multiscale_example_3.md index ca20bde65..0217d9114 100644 --- a/docs/src/multiscale/multiscale_example_3.md +++ b/docs/src/multiscale/multiscale_example_3.md @@ -4,6 +4,11 @@ There are two major issues hinted at in last chapter's implementation, which we' You can find the full script for this simulation in the [ToyMultiScalePlantModel](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/ToyMultiScalePlantModel/ToyPlantSimulation3.jl) subfolder of the examples folder. +```@contents +Pages = ["multiscale_example_3.md"] +Depth = 3 +``` + ## Delaying organ maturity 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. diff --git a/docs/src/multiscale/single_to_multiscale.md b/docs/src/multiscale/single_to_multiscale.md index 62e4e218f..0d2ffb2e7 100644 --- a/docs/src/multiscale/single_to_multiscale.md +++ b/docs/src/multiscale/single_to_multiscale.md @@ -6,6 +6,11 @@ This page showcases how to do the conversion, and then adds a model at a new sca The full script for the example can be found in [https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/ToySingleToMultiScale.jl] +```@contents +Pages = ["single_to_multiscale.md"] +Depth = 3 +``` + # Converting the ModelList to a multi-scale mapping For example, let's return to the `ModelList` coupling a light interception model, a Leaf Area Index model, and a carbon biomass increment model that was discussed in the [Example model switching](@ref) subsection: diff --git a/docs/src/planned_features.md b/docs/src/planned_features.md index b52ad5ab2..d646539c8 100644 --- a/docs/src/planned_features.md +++ b/docs/src/planned_features.md @@ -48,7 +48,8 @@ Its current state doesn't enable practical declaration of several plant species, ## Other -- Reproducing another FSPM? +- Reproducing another FSPM ? +- Diffusion model example ? The full list of issues can be found [here](https://github.com/VirtualPlantLab/PlantSimEngine.jl/issues) diff --git a/docs/src/prerequisites/key_concepts.md b/docs/src/prerequisites/key_concepts.md index 3c4baf86b..e205995ab 100644 --- a/docs/src/prerequisites/key_concepts.md +++ b/docs/src/prerequisites/key_concepts.md @@ -2,6 +2,11 @@ You'll find a brief description of some of the main concepts and terminology related to and used in PlantSimEngine. +```@contents +Pages = ["key_concepts.md"] +Depth = 3 +``` + ## Crop models ## FSPM diff --git a/docs/src/step_by_step/advanced_coupling.md b/docs/src/step_by_step/advanced_coupling.md index 864a26184..e7f23e7e3 100644 --- a/docs/src/step_by_step/advanced_coupling.md +++ b/docs/src/step_by_step/advanced_coupling.md @@ -59,6 +59,6 @@ While not encouraged, if you have a valid reason to force the coupling with a pa PlantSimEngine.dep(::Process2Model) = (process1=Process1Model,) ``` -##  +## Examples in the wild -There are examples in PlantBioPhysics of such models TODO. \ No newline at end of file +You can find a typical example in a companion package: [PlantBioPhysics.jl](). An energy balance model, the [Monteith model](https://github.com/VEZY/PlantBiophysics.jl/blob/master/src/processes/energy/Monteith.jl), needs to [iteratively run a photosynthesis model](https://github.com/VEZY/PlantBiophysics.jl/blob/c1a75f294109d52dc619f764ce51c6ca1ea897e8/src/processes/energy/Monteith.jl#L154) in its `run!` function. \ No newline at end of file diff --git a/docs/src/step_by_step/detailed_first_example.md b/docs/src/step_by_step/detailed_first_example.md index 12eb2d2df..8e78a01c5 100644 --- a/docs/src/step_by_step/detailed_first_example.md +++ b/docs/src/step_by_step/detailed_first_example.md @@ -12,6 +12,11 @@ leaf = ModelList(Beer(0.5), status = (LAI = 2.0,)) out_sim = run!(leaf, meteo) ``` +```@contents +Pages = ["detailed_first_example.md"] +Depth = 3 +``` + ## Definitions ### Processes diff --git a/docs/src/step_by_step/implement_a_model.md b/docs/src/step_by_step/implement_a_model.md index 3e833848b..253679dd5 100644 --- a/docs/src/step_by_step/implement_a_model.md +++ b/docs/src/step_by_step/implement_a_model.md @@ -90,8 +90,6 @@ If you have a look at example models, you'll see that in order to implement a ne - the actual model, developed as a method for the process it simulates - some helper functions used by the package and/or the users -TODO If you create your own process, the function will print a short tutorial on how to do all that, adapted to the process you just created (see [Implementing a new process](@ref)). - ## Example: the Beer-Lambert model ### The process diff --git a/docs/src/step_by_step/implement_a_model_additional.md b/docs/src/step_by_step/implement_a_model_additional.md index 007ce8d40..8d192b9f2 100644 --- a/docs/src/step_by_step/implement_a_model_additional.md +++ b/docs/src/step_by_step/implement_a_model_additional.md @@ -1,5 +1,10 @@ # Model implementation additional notes +```@contents +Pages = ["implement_a_model_additional.md"] +Depth = 3 +``` + ## Parametric types In [Implementing a model](@ref model_implementation_page), the Beer model's structure was declared with a parametric type. diff --git a/docs/src/step_by_step/quick_and_dirty_examples.md b/docs/src/step_by_step/quick_and_dirty_examples.md index 148b83731..8420e8590 100644 --- a/docs/src/step_by_step/quick_and_dirty_examples.md +++ b/docs/src/step_by_step/quick_and_dirty_examples.md @@ -9,6 +9,11 @@ These examples are all for single-scale simulations. For multi-scale modelling t You can find the implementation for all the example models, as well as other toy models [in the examples folder](https://github.com/VirtualPlantLab/PlantSimEngine.jl/tree/main/examples). +```@contents +Pages = ["quick_and_dirty_examples.md"] +Depth = 2 +``` + ## Example with a single light interception model and a single weather timestep ```julia diff --git a/docs/src/troubleshooting_and_testing/implicit_contracts.md b/docs/src/troubleshooting_and_testing/implicit_contracts.md index 34a2c9e0f..0c7e362b7 100644 --- a/docs/src/troubleshooting_and_testing/implicit_contracts.md +++ b/docs/src/troubleshooting_and_testing/implicit_contracts.md @@ -1,6 +1,11 @@ -This page details some of the assumptions, coupling constraints and inner workings of PlantSimEngine which may be particular relevant when implementing new models. +This page summarizes some of the assumptions, coupling constraints and inner workings of PlantSimEngine which may be particular relevant when implementing new models. -TODO est-ce le meilleur endroit ? +If you are unsure of an implementation subtlety, check this page out to see whether it answers your question. + +```@contents +Pages = ["implicit_contracts.md"] +Depth = 2 +``` ## Weather data provides the simulation timestep, but models can veer away from it @@ -62,8 +67,6 @@ A workaround for some of the situations where this occurs is described here : [H ## Status template intialisation order TODO -## Diffusion systems ? - ## TODO simulation order, node order, etc. ## Simulation order instability when adding models diff --git a/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md b/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md index ddfdb024d..eee67c227 100644 --- a/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md +++ b/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md @@ -11,6 +11,11 @@ If you need some advice on the FSPM side, the research community has [its own di If the issue seems PlantSimEngine-related, or you have questions regarding modeling or have suggestions, you can also [file an issue](https://github.com/VirtualPlantLab/PlantSimEngine.jl/issues) on Github. +```@contents +Pages = ["plantsimengine_and_julia_troubleshooting.md"] +Depth = 3 +``` + ## Tips and workflow Some errors are very specific as to their cause, and the PlantSimEngine errors tend to be explicit about which parameter / variable / organ is causing the error, helping narrow down its origin. diff --git a/docs/src/troubleshooting_and_testing/tips_and_workarounds.md b/docs/src/troubleshooting_and_testing/tips_and_workarounds.md index 396f3f8ff..289315a45 100644 --- a/docs/src/troubleshooting_and_testing/tips_and_workarounds.md +++ b/docs/src/troubleshooting_and_testing/tips_and_workarounds.md @@ -10,6 +10,11 @@ There are also a couple of features that are quick hacks or that are meant for q We'll list a few of them here, and will likely add some entry in the future listing some built-in limitations or implicit expectations of the package. +```@contents +Pages = ["tips_and_workarounds.md"] +Depth = 2 +``` + ## Making use of past states in multi-scale simulations It is possible to make use of the value of a variable in the past simulation timestep via the `PreviousTimeStep` mechanism in the mapping API (In fact, as mentioned elsewhere, it is the default way to break undesirable cyclic dependencies that can come up when coupling models, see : [Avoiding cyclic dependencies](@ref)). @@ -67,7 +72,6 @@ It will parse your mapping, generate custom models to store and feed the vector This feature is likely to break in simulations that make use of planned future features (such as mixing models with different timesteps), without guarantee of a fix on a short notice. Again, bear in mind it is mostly a convenient shortcut for prototyping, when doing multi-scale simulations. -TODO examples of other ad hoc models TODO state machines ? TODO workaround status initialisation bug ? From ce40f5e02dee37ff5987f0c06ae42c9a7169bae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Vezy?= Date: Thu, 6 Mar 2025 15:47:00 +0100 Subject: [PATCH 086/147] Update why_julia.md Further develop the page --- docs/src/introduction/why_julia.md | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/docs/src/introduction/why_julia.md b/docs/src/introduction/why_julia.md index 3678f518b..90c65ef21 100644 --- a/docs/src/introduction/why_julia.md +++ b/docs/src/introduction/why_julia.md @@ -1,16 +1,18 @@ # The choice of using Julia -PlantSimEngine is implemented in Julia. It arose from a particular combination of [needs and requirements](why_plantsimengine.md), a combination which Julia seemed to fill adequately. +PlantSimEngine is implemented in Julia. It arose from a particular combination of [needs and requirements](why_plantsimengine.md) that Julia addresses effectively. -Other modelling frameworks, FSPMs and crop models are -often- written in combinations of Java, C++, Python, or Fortran. Given that it isn't the language many researchers (and developers !) are most familiar with, this page provides a short explanation of the reasoning behind that language choice. It might not have been the only possible valid choice, of course. +Other modelling frameworks, FSPMs and crop models are -often- written in combinations of Java, C++, Python, or Fortran. Given that Julia isn't the language many researchers (and developers!) are most familiar with yet, this page provides a short explanation of the reasoning behind that language choice. -## A researcher-developer tool +## From research to real-world applications -PlantSimEngine is a goal oriented framework. Its features arose -and continue to evolve- out of necessity for more and more complex simulations. It wasn't a pre-designed piece of software. +PlantSimEngine was originally a goal-oriented framework. Its features arose -and continue to evolve- out of necessity for more and more complex simulation setups. -It was therefore originally a means to an end, and not a product in itself. +While PlantSimEngine primarily helps researchers prototype and test their models efficiently, we consistently work with the vision of making it suitable for real-world applications. Our goal is to build a bridge between academic plant modeling and practical field applications. Ideally, researchers should be able to develop and refine their models in a comfortable environment, and these models could eventually be deployed in production environments. -PlantSimEngine is designed to balance scientific rigor with developer productivity. Researchers need to focus primarily on the scientific aspects of their models while still creating efficient, maintainable code. Julia provides an ideal environment where researchers can express complex mathematical concepts directly in code without sacrificing performance. +This vision of dual-purpose functionality drives our focus on performance optimization. We aspire for the models you develop to be useful beyond academic papers, potentially serving reliably in production environments where efficiency and accuracy are crucial. Julia's strong performance characteristics support this vision in ways other languages would struggle to match. + +PlantSimEngine aims to balance scientific rigor with developer productivity, with the long-term goal of ensuring that models can be deployed at scale. Julia provides an environment where researchers can express complex mathematical concepts directly in code with good performance potential, creating a pathway for these models to potentially reach practical implementation. ## PlantSimEngine's constraints @@ -20,9 +22,9 @@ While computers have gained several orders of magnitude of power and memory over Simulating multiple processes with user-provided variables over many plants with tens of thousands of leaves requires a lot of computation. Using a higher-level language such as Python or R would not lead to adequate simulation times. -In fact, part of the initial motivation to commit to Julia happened after porting an ecophysiological simulation from R to Julia and getting an order of magnitude difference in performance 'out-of-the-box'. +In fact, part of the initial motivation to commit to Julia happened after porting [a model](https://github.com/VEZY/DynACof.jl) from R to Julia and getting several orders of magnitude difference in performance 'out-of-the-box'. Seeing computations that previously took minutes suddenly completing in seconds was quite convincing (see also [this benchmark](https://vezy.github.io/PlantBiophysics-paper/notebooks_performance_Fig5_PlantBiophysics_performance/) showing a difference of 5 orders of magnitude). -Julia, with its 'Just-ahead-of-time' compilation model and its flexibility allowing to do some lower-level optimisation, doesn't suffer from the limitations one would encounter by using only Python or R. +Julia, with its well-designed 'Just-ahead-of-time' compilation model and its flexibility allowing to do some lower-level optimisation, doesn't suffer from the limitations one would encounter by using only Python or R. ### Flexibility, ease of use @@ -54,13 +56,13 @@ Combining two different languages requires a lot of language expertise, with con Speed of iteration is also lost whenever performance is a concern, which happens often in our context. However modular and easy-to-use a language like Python might be, whenever it's time to switch to a low-level language, development speed will slow down. -Julia, while likely being a little harder to learn than Python, and require extra knowledge to properly make use of its flexibility and performance capabilities, leads to a significantly smoother development experience. +Julia effectively solves this problem. While it might be a little harder to learn than Python, and require extra knowledge to properly make use of its flexibility and performance capabilities, it leads to a smoother development experience. -Everything can be done using Julia exclusively, so there is no need to learn two languages. No need to interface between them. Iteration speed doesn't suddenly grind to a halt if a low-level implementation is needed. A competent researcher-developer might have less need of engineering resources, while still being able to work a lot on modeling and the actual plant side of things. +Everything can be done using Julia exclusively, so there is no need to learn two languages. No need to interface between them. Iteration speed doesn't suddenly grind to a halt if a low-level implementation is needed. A competent researcher-developer can move seamlessly from prototype to production, while still being able to focus on modeling and the actual plant side of things. TODO image ML -It seems we aren't the only ones to feel Julia is the right tool for our job for those reasons. Indeed, other niches where Julia seems to thrive tend to be other computationally heavy areas with much active research, such as ML and climate modeling. +It seems we aren't the only ones to find Julia a good tool for our job. Other niches where Julia is gaining traction tend to be other computationally heavy areas with much active research, such as machine learning and climate modeling - areas where this balance of expressivity and performance is equally valuable. ### A good balance in terms of accessibility @@ -68,7 +70,7 @@ Another argument in favour of Julia is that one of the aims for PlantSimEngine i Many researchers are not developers by trade or heart, and a Java-only or C++-only implementation, on top of the earlier points, would not be accessible enough and would not gain much traction. -Julia, while less ubiquitous than other languages in research circles, resembles Python and R and is more beginner-friendly than Java or C++. It is easier for a Python user to learn to use a simple Julia package than a C++ one. +Julia, while less ubiquitous than other languages in research circles, resembles Python and R and is more beginner-friendly than Java or C++. It is easier for a Python user to learn to use a simple Julia package than a C++ one. Users will also find it easier to quickly implement new models without the potential hurdle of a low-level implementation, or some language interfacing also being required. The prototyping phase doesn't require a subsequent performance tuning phase. @@ -86,6 +88,6 @@ The cost for switching may not be worth it in many other circumstances. However, ## Conclusion -For PlantSimEngine's specific requirements—balancing performance with flexibility, enabling rapid iteration while maintaining computational efficiency, and providing an accessible interface for researchers—Julia represents an optimal choice. The language allows us to build an ecosystem where plant modeling can advance through collaborative, efficient, and scientifically rigorous development. +For PlantSimEngine's specific requirements—balancing performance with flexibility, enabling rapid iteration while maintaining computational efficiency, and providing an accessible interface for both researchers and field practitioners—Julia represents a suitable choice. The language allows us to build an ecosystem where plant modeling can advance through collaborative, efficient, and scientifically rigorous development while delivering real-world value through production deployments. -While no language solution is perfect, Julia's unique combination of features makes it particularly well-suited to the challenges of modern plant modeling and simulation. \ No newline at end of file +While no language solution is perfect, Julia's combination of features makes it well-suited to the challenges of modern plant modeling and simulation, both in research and practical applications. We're optimistic about the possibilities it offers for the future of plant modeling. From f6ba12d7914aa95a67ac8aaafe616f5f271127e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Vezy?= Date: Thu, 6 Mar 2025 16:23:09 +0100 Subject: [PATCH 087/147] Update why_plantsimengine.md Add references --- docs/src/introduction/why_plantsimengine.md | 31 ++++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/docs/src/introduction/why_plantsimengine.md b/docs/src/introduction/why_plantsimengine.md index d06756ee2..5be284106 100644 --- a/docs/src/introduction/why_plantsimengine.md +++ b/docs/src/introduction/why_plantsimengine.md @@ -6,19 +6,36 @@ PlantSimEngine arose out of a perceived need for a new modelling framework for e ## Existing FSPM systems ### Monoliths + Often massive codebases Rigid Implicit hypothesis Parameters hard to change -### Distributed systems +E.g. APSIM[^1], CropBox[^2], GroIMP[^3], AMAPSim[^4], Helios[^5], CPlantBox[^6] + +### Distributed Systems + +E.g. OpenAlea[^7], Crops in silico[^8] -two-language problem +The two-language problem presents challenges in integration and usability. -### other tools +### Other Tools -Architectural primary focus -Adding functional and environmental models is less straightforward -C++, Java, less accessible -User interface +Architectural primary focus (e.g. AMAPSim[^4], L-Py[^9]) where adding functional and environmental models is less straightforward + +C++, Java = less accessible +User interface is important for models users, and also for prototyping and debugging faster Less tailored to autonomous 'researcher-developer', requires a 'developer-modeller' + +# References + +[^1]: Holzworth, D. P. et al. APSIM – Evolution towards a new generation of agricultural systems simulation. Environ. Model. Softw. 62, 327–350 (2014). +[^2]: Yun, K. & Kim, S.-H. Cropbox: a declarative crop modelling framework. Silico Plants 5, (2022). +[^3]: Kniemeyer, O. (2004). Rule-based modelling with the XL/GroIMP software. The logic of artificial life. Proceedings of 6th GWAL. AKA Akademische Verlagsges Berlin, 56-65. +[^4]: Barczi, J.-F. et al. AmapSim: A Structural Whole-plant Simulator Based on Botanical Knowledge and Designed to Host External Functional Models. Ann. Bot. 101, 1125–1138 (2008). +[^5]: Bailey, B. N. (2019). Helios: A scalable 3D plant and environmental biophysical modeling framework. Frontiers in Plant Science, 10, 1185. +[^6]: Giraud, M. et al. CPlantBox: a fully coupled modelling platform for the water and carbon fluxes in the soil–plant–atmosphere continuum. Silico Plants 5, diad009 (2023). +[^7]: Pradal, C., Dufour-Kowalski, S., Boudon, F., Fournier, C. & Godin, C. OpenAlea: a visual programming and component-based software platform for plant modelling. Funct. Plant Biol. 35, 751–760 (2008). +[^8]: Marshall-Colon, A. et al. Crops In Silico: Generating Virtual Crops Using an Integrative and Multi-scale Modeling Platform. Front. Plant Sci. 8, (2017). +[^9]: Boudon, F., Pradal, C., Cokelaer, T., Prusinkiewicz, P. & Godin, C. L-Py: An L-System Simulation Framework for Modeling Plant Architecture Development Based on a Dynamic Language. Front. Plant Sci. 3, (2012). \ No newline at end of file From ca96db262d009a7bea543f6d33071fc87e73b63f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Vezy?= Date: Thu, 6 Mar 2025 16:42:44 +0100 Subject: [PATCH 088/147] Update why_julia.md add ref to Alejandro discourse post --- docs/src/introduction/why_julia.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/introduction/why_julia.md b/docs/src/introduction/why_julia.md index 90c65ef21..5abf3a280 100644 --- a/docs/src/introduction/why_julia.md +++ b/docs/src/introduction/why_julia.md @@ -2,7 +2,7 @@ PlantSimEngine is implemented in Julia. It arose from a particular combination of [needs and requirements](why_plantsimengine.md) that Julia addresses effectively. -Other modelling frameworks, FSPMs and crop models are -often- written in combinations of Java, C++, Python, or Fortran. Given that Julia isn't the language many researchers (and developers!) are most familiar with yet, this page provides a short explanation of the reasoning behind that language choice. +Other modelling frameworks, FSPMs and crop models are -often- written in combinations of Java, C++, Python, or Fortran. Given that Julia isn't the language many researchers (and developers!) are most familiar with yet, this page provides a short explanation of the reasoning behind that language choice. Another nice resource is [this discourse post](https://fspm.discourse.group/t/why-is-julia-meant-for-fspm/175) by Alejandro Morales Sierra, the creator and maintainer of Virtual Plant Lab. ## From research to real-world applications From 96c5ada29edc4a100eee04062d9007ba14c52bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Vezy?= Date: Thu, 6 Mar 2025 16:42:59 +0100 Subject: [PATCH 089/147] Update why_plantsimengine.md Write the content --- docs/src/introduction/why_plantsimengine.md | 107 ++++++++++++++++---- 1 file changed, 90 insertions(+), 17 deletions(-) diff --git a/docs/src/introduction/why_plantsimengine.md b/docs/src/introduction/why_plantsimengine.md index 5be284106..2bf7e6a43 100644 --- a/docs/src/introduction/why_plantsimengine.md +++ b/docs/src/introduction/why_plantsimengine.md @@ -1,34 +1,107 @@ +# Why PlantSimEngine? -PlantSimEngine arose out of a perceived need for a new modelling framework for ecophysiological and FSPM simulations. +PlantSimEngine was created to address specific challenges in plant modeling that weren't adequately met by existing frameworks. This page outlines the motivations behind its development and explains what sets it apart. -## Goals +## The Landscape of Plant Modeling Tools -## Existing FSPM systems +Plant modeling has traditionally been approached through several different paradigms, each with its own strengths and limitations: -### Monoliths +### Monolithic Systems -Often massive codebases -Rigid -Implicit hypothesis -Parameters hard to change +Systems like APSIM[^1], CropBox[^2], GroIMP[^3], AMAPSim[^4], Helios[^5], and CPlantBox[^6] are powerful but present several challenges: -E.g. APSIM[^1], CropBox[^2], GroIMP[^3], AMAPSim[^4], Helios[^5], CPlantBox[^6] +- Massive codebases that are difficult to navigate +- Rigid structures that resist heavy modifications +- Limited flexibility for integrating new models and difficulty to handle model coupling (has to be done by hand), especially when considering multi-scale modeling +- Implementations that are difficult to modify for specific use cases +- Steep learning curves for new users with a lot of implementation details (*e.g.* complex data structures) +- Require specialized developer-modeler expertise rather than supporting researcher-developers +- Not designed for rapid hypothesis testing or model iteration ### Distributed Systems -E.g. OpenAlea[^7], Crops in silico[^8] +Frameworks such as OpenAlea[^7] and Crops in Silico[^8] offer more flexibility but suffer from what's known as the "two-language problem": -The two-language problem presents challenges in integration and usability. +- The interface language (Python) is accessible but computationally inefficient +- The computational backend is fast but difficult to modify +- Interfacing between components requires expertise in multiple languages +- Iteration cycles slow down significantly when performance optimization is needed -### Other Tools +### Architecture-Focused Tools -Architectural primary focus (e.g. AMAPSim[^4], L-Py[^9]) where adding functional and environmental models is less straightforward +Many existing tools like AMAPSim[^4] and L-Py[^9] prioritize plant architectural modeling: -C++, Java = less accessible -User interface is important for models users, and also for prototyping and debugging faster -Less tailored to autonomous 'researcher-developer', requires a 'developer-modeller' +- Integration of functional and environmental models is often an afterthought +- Implementation in languages like C++ or Java creates accessibility barriers +- Less suited for rapidly testing ecophysiological hypotheses -# References +## PlantSimEngine's Innovations + +PlantSimEngine addresses these limitations through several key innovations: + +### Automatic Model Coupling + +One of PlantSimEngine's most powerful features is its ability to automatically couple models: + +- Leverages Julia's multiple-dispatch to compute the models dependency graph +- Models can be combined without writing explicit connection code +- Variables are automatically passed between processes based on dependencies, whatever the scale +- Supports multi-scale modeling with minimal effort, from organ to plant to landscape levels + +### Flexibility with Control + +PlantSimEngine gives researchers unprecedented control over their modeling process: + +- Switch between different model implementations with a simple syntax, without modifying the models code +- Reduce degrees of freedom by fixing variables to constant values +- Force variables to use observations instead of model predictions +- Use simpler models for specific processes to reduce complexity +- Scale from single plants to complex ecosystems with the same framework + +### Performance Without Compromise + +Performance is a core feature, not an afterthought: + +- Benchmarks show operations in the 100th of nanoseconds range for complex models +- The [PlantBiophysics.jl implementation is over 38,000 times faster](https://vezy.github.io/PlantBiophysics-paper/notebooks_performance_Fig5_PlantBiophysics_performance/) than equivalent implementations in R +- Julia's just-ahead-of-time compilation model enables both high-level abstraction and low-level performance +- Automatic parallelization across objects, time-steps, and independent processes +- Performance optimization is just a little bit more work on the same code, not a complete rewrite + +### Streamlined Model Development + +The framework handles tedious aspects automatically: + +- Input and output variable management +- Time-step handling +- Object instantiation and tracking +- Dependency resolution between processes +- Unit propagation through integration with Unitful.jl +- Error propagation via MonteCarloMeasurements.jl + +## From Research to Application + +PlantSimEngine aims at bridging the gap between academic modeling and practical applications: + +- Researchers can develop and refine models in a comfortable environment +- Models can be deployed in production environments with minimal modification, as a script or an executable +- The same framework works for hypothesis testing and real-world simulations +- Performance in the prototype directly translates to performance in the field + +## Community-Oriented Development + +PlantSimEngine is designed with the community in mind: + +- MIT license encourages wide adoption and contribution +- Accessible to researchers with varying levels of programming experience +- Evolving based on real-world requirements from projects like XPalm and PlantBiophysics +- Standardized approach encourages model sharing and reproducibility + +## Conclusion + +By combining high-level abstraction with exceptional performance, PlantSimEngine lets researchers focus on their scientific questions rather than computational implementation details. It represents a new approach to plant modeling that emphasizes accessibility, flexibility, and efficiency—accelerating innovation in plant science and its applications. + +## References [^1]: Holzworth, D. P. et al. APSIM – Evolution towards a new generation of agricultural systems simulation. Environ. Model. Softw. 62, 327–350 (2014). [^2]: Yun, K. & Kim, S.-H. Cropbox: a declarative crop modelling framework. Silico Plants 5, (2022). From 94026351f52417ec1bb0e41c17dab76858bab1e4 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Thu, 6 Mar 2025 17:25:10 +0100 Subject: [PATCH 090/147] Avoid hardcoding types in toy plant example. Attempt at proper public/private API display, blurb explaining MTG terminology ambiguity --- docs/src/API/API_private.md | 4 +-- docs/src/API/API_public.md | 3 +- .../multiscale/multiscale_considerations.md | 2 +- docs/src/multiscale/multiscale_example_1.md | 16 +++++---- docs/src/multiscale/multiscale_example_2.md | 19 ++++++----- docs/src/multiscale/multiscale_example_3.md | 34 ++++++++++++++++--- docs/src/prerequisites/key_concepts.md | 18 ++++++++-- .../implicit_contracts.md | 4 ++- .../ToyPlantSimulation1.jl | 20 ++++------- .../ToyPlantSimulation2.jl | 23 ++++++------- .../ToyPlantSimulation3.jl | 29 +++++++--------- 11 files changed, 99 insertions(+), 73 deletions(-) diff --git a/docs/src/API/API_private.md b/docs/src/API/API_private.md index 0d0f9fae7..40b584bd8 100644 --- a/docs/src/API/API_private.md +++ b/docs/src/API/API_private.md @@ -6,9 +6,7 @@ Private functions, types or constants from `PlantSimEngine`. These are not expor ## Index ```@index -Modules = [PlantSimEngine] -Public = false -Private = true +Pages = ["API_private.md"] ``` ## API documentation diff --git a/docs/src/API/API_public.md b/docs/src/API/API_public.md index 7716289c6..536c93cc9 100644 --- a/docs/src/API/API_public.md +++ b/docs/src/API/API_public.md @@ -3,8 +3,7 @@ ## Index ```@index -Modules = [PlantSimEngine] -Private = false +Pages = ["API_public.md"] ``` ## API documentation diff --git a/docs/src/multiscale/multiscale_considerations.md b/docs/src/multiscale/multiscale_considerations.md index b5dfbb0b2..11d3b136b 100644 --- a/docs/src/multiscale/multiscale_considerations.md +++ b/docs/src/multiscale/multiscale_considerations.md @@ -37,7 +37,7 @@ A multi-scale tree graph (MTG) object (see the [Multi-scale Tree Graphs](@ref) s All the multi-scale examples make use of the companion package [MultiScaleTreeGraph.jl](https://github.com/VEZY/MultiScaleTreeGraph.jl), which we therefore recommend for running your own multi-scale simulations. !!! note - Multi-scale Tree Graphs make use of conflicting terminology with PlantSimEngine's concepts, which is discussed in [Organ/Scale](@ref). If you are new to the concepts, make sure to read that section and keep note of it. + Multi-scale Tree Graphs make use of conflicting terminology with PlantSimEngine's concepts, which is discussed in [Scale/symbol terminology ambiguity](@ref). If you are new to the concepts, make sure to read that section and keep note of it. ## Models run once per organ instance, not once per organ level diff --git a/docs/src/multiscale/multiscale_example_1.md b/docs/src/multiscale/multiscale_example_1.md index add7665e4..f6dd90343 100644 --- a/docs/src/multiscale/multiscale_example_1.md +++ b/docs/src/multiscale/multiscale_example_1.md @@ -125,15 +125,17 @@ We'll also add a couple of other parameters, which could go elsewhere : ```julia PlantSimEngine.@process "organ_emergence" verbose = false -struct ToyCustomInternodeEmergence <: AbstractOrgan_EmergenceModel - TT_emergence::Float64 - carbon_internode_creation_cost::Float64 - leaf_surface_area::Float64 - leaves_max_surface_area::Float64 +struct ToyCustomInternodeEmergence{T} <: AbstractOrgan_EmergenceModel + TT_emergence::T + carbon_internode_creation_cost::T + leaf_surface_area::T + leaves_max_surface_area::T end - ``` +!!! note + We make use of parametric types instead of the intuitive Float64 for flexibility. See [Parametric types](@ref) for a more in-depth explanation + And give them some default values : ```julia @@ -157,7 +159,7 @@ and then updates the MTG. ```julia function PlantSimEngine.run!(m::ToyCustomInternodeEmergence, models, status, meteo, constants=nothing, sim_object=nothing) - leaves_surface_area = m.leaf_surface * get_n_leaves(status.node) + leaves_surface_area = m.leaf_surface_area * get_n_leaves(status.node) status.carbon_organ_creation_consumed = 0.0 if leaves_surface_area > m.leaves_max_surface_area diff --git a/docs/src/multiscale/multiscale_example_2.md b/docs/src/multiscale/multiscale_example_2.md index 0bdf34f18..c9e2ad173 100644 --- a/docs/src/multiscale/multiscale_example_2.md +++ b/docs/src/multiscale/multiscale_example_2.md @@ -54,9 +54,9 @@ end PlantSimEngine.@process "root_growth" verbose = false -struct ToyRootGrowthModel <: AbstractRoot_GrowthModel - water_threshold::Float64 - carbon_root_creation_cost::Float64 +struct ToyRootGrowthModel{T} <: AbstractRoot_GrowthModel + water_threshold::T + carbon_root_creation_cost::T root_max_len::Int end @@ -110,12 +110,12 @@ end The minor change is that new organs are now created only if the water stock is above a given threshold. ```julia -struct ToyCustomInternodeEmergence <: AbstractOrgan_EmergenceModel - TT_emergence::Float64 - carbon_internode_creation_cost::Float64 - leaf_surface_area::Float64 - leaves_max_surface_area::Float64 - water_leaf_threshold::Float64 +struct ToyCustomInternodeEmergence{T} <: AbstractOrgan_EmergenceModel + TT_emergence::T + carbon_internode_creation_cost::T + leaf_surface_area::T + leaves_max_surface_area::T + water_leaf_threshold::T end ToyCustomInternodeEmergence(;TT_emergence=300.0, carbon_internode_creation_cost=200.0, leaf_surface_area=3.0,leaves_max_surface_area=100.0, @@ -223,6 +223,7 @@ mtg = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Scene", 1, 0)) meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) outs = run!(mtg, mapping, meteo_day) +mtg ``` And that's it ! diff --git a/docs/src/multiscale/multiscale_example_3.md b/docs/src/multiscale/multiscale_example_3.md index 0217d9114..7274bf55c 100644 --- a/docs/src/multiscale/multiscale_example_3.md +++ b/docs/src/multiscale/multiscale_example_3.md @@ -97,9 +97,9 @@ With that new coupling consideration properly handled, we can complete the full ```julia PlantSimEngine.@process "root_growth_decision" verbose = false -struct ToyRootGrowthDecisionModel <: AbstractRoot_Growth_DecisionModel - water_threshold::Float64 - carbon_root_creation_cost::Float64 +struct ToyRootGrowthDecisionModel{T} <: AbstractRoot_Growth_DecisionModel + water_threshold::T + carbon_root_creation_cost::T end PlantSimEngine.inputs_(::ToyRootGrowthDecisionModel) = @@ -211,9 +211,35 @@ Fortunately, the logic here is quite straightforward. We can't be computing our The solution is hopefully quite intuitive : when we compute resource stocks, we should be computing it using the previous timestep's values. Then root creation happens (or doesn't), and the computed `carbon_root_creation_consumed` corresponds to the current timestep value. We could also do the same for water to be consistent. +### Updated mapping + +The relevant part of the mapping that needs to be updated is the following: + +```julia +mapping = Dict( +... +"Plant" => ( + MultiScaleModel( + model=ToyStockComputationModel(), + mapped_variables=[ + :carbon_captured=>["Leaf"], + :water_absorbed=>["Root"], + PreviousTimeStep(:carbon_root_creation_consumed)=>"Root", + PreviousTimeStep(:carbon_organ_creation_consumed)=>["Internode"], + ], + ), + ToyRootGrowthDecisionModel(10.0, 50.0), + Status(water_stock = 0.0, carbon_stock = 0.0) + ), +... +) +``` + ## Final words -The full script for this simulation can be found [here](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/ToyMultiScalePlantModel/ToyPlantSimulation3.jl), in the ToyMultiScalePlantModel subfolder of the examples folder. +And you're now ready to run the simulation. + +The full script can be found [here](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/ToyMultiScalePlantModel/ToyPlantSimulation3.jl), in the ToyMultiScalePlantModel subfolder of the examples folder. We now have a plant with two different growth directions. Roots are added at the beginning, until water is considered abundant enough. diff --git a/docs/src/prerequisites/key_concepts.md b/docs/src/prerequisites/key_concepts.md index e205995ab..f8bc7c6cb 100644 --- a/docs/src/prerequisites/key_concepts.md +++ b/docs/src/prerequisites/key_concepts.md @@ -4,7 +4,7 @@ You'll find a brief description of some of the main concepts and terminology rel ```@contents Pages = ["key_concepts.md"] -Depth = 3 +Depth = 4 ``` ## Crop models @@ -16,7 +16,7 @@ Depth = 3 This page provides a general description of the concepts and terminology used in PlantSimEngine. For a more implementation-guided description of the design and some of the terms presented here, see the [Detailed walkthrough of a simple simulation](@ref) !!! Note - Some terminology unfortunately has different meanings in different contexts. This is particularly true of the terms organ, scale and symbol, which have a different meaning for [Multi-scale Tree Graphs](@ref) than the rest of PlantSimEngine (see [Organ/Scale](@ref) further down). Make sure to double-check those subsections, and relevant examples if you encounter issues relating to these terms. + Some terminology unfortunately has different meanings in different contexts. This is particularly true of the terms organ, scale and symbol, which have a different meaning for [Multi-scale Tree Graphs](@ref) than the rest of PlantSimEngine (see [Scale/symbol terminology ambiguity](@ref) further down). Make sure to double-check those subsections, and relevant examples if you encounter issues relating to these terms. ### Processes @@ -135,6 +135,18 @@ TODO example ? TODO image ? TODO lien avec AMAP ? -TODO scale, symbol terminology ambiguity +#### Scale/symbol terminology ambiguity + +Multi-scale tree graphs have different terminology (see [Organ/Scale](@ref)): + +- a symbol corresponds to a PlantSimEngine scale, eg "Plant", "Root", and has nothing to do with the Julia programming language's definition of symbol (eg `:var`) +- Scales are integers passed to the Node constructor describing the level of description of the tree graph object. They don't always have a one-to-one correspondence to a multi-scale simulation's scales, but are similar. + +You can find a brief description of the MTG concepts [here](https://vezy.github.io/MultiScaleTreeGraph.jl/stable/the_mtg/mtg_concept/#Node-MTG-and-attributes). + +Other words are unfortunately reused in various contexts with different meanings: tree/leaf/root have a different meaning when talking about computer science data structure (eg, graphs, dependency graphs and trees). + +!!! note + In the majority of cases, you can assume the tree-related terminology refers to the biological terms, and that "organ" refer to plant organs, and "single-scale", "multi-scale" and "scale" to PlantSimEngine's concept of scales described in [Organ/Scale](@ref). MTG objects are mostly manipulated no a per-node basis, unless a model makes use of functions relating to MTG traversal, in which case you may expect computer science terminology. ### State machines diff --git a/docs/src/troubleshooting_and_testing/implicit_contracts.md b/docs/src/troubleshooting_and_testing/implicit_contracts.md index 0c7e362b7..57b00774d 100644 --- a/docs/src/troubleshooting_and_testing/implicit_contracts.md +++ b/docs/src/troubleshooting_and_testing/implicit_contracts.md @@ -24,10 +24,12 @@ If your weather data isn't adjusted to conform to a regular timestep, you will n ## No cyclic dependencies in the simplified dependency graph -This has been explained elsewhere TODO : the dependency graph is comprised of soft and hard dependency nodes, and the final version only links soft dependency nodes and is guaranteed to contain no cycles. +The model dependency graph used for running the simulation is comprised of soft and hard dependency nodes, and the final version only links soft dependency nodes together, and is expected to contain no cycles. Any user model coupling which causes a cyclic dependency to occur will require some extra tinkering to run : either design models differently, create a hard dependency with some of the problematic models, or break the cycle by having a variable take the previous timestep's value as input. +See [Dependency graphs](@ref) and the following subsections for more discussion related to dependency graph constraints. + Note : Only the previous timestep is accessible in PlantSimEngine without any kind of dedicated model. How to create a model to store more past timesteps of a specific variable is described in the [Tips and workarounds](@ref) page: [Making use of past states in multi-scale simulations](@ref) ## Hard dependencies need to be declared in the model definition diff --git a/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation1.jl b/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation1.jl index f872b9ec9..f32284b76 100644 --- a/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation1.jl +++ b/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation1.jl @@ -12,11 +12,11 @@ end PlantSimEngine.@process "organ_emergence" verbose = false -struct ToyCustomInternodeEmergence <: AbstractOrgan_EmergenceModel - TT_emergence::Float64 - carbon_internode_creation_cost::Float64 - leaf_surface_area::Float64 - leaves_max_surface_area::Float64 +struct ToyCustomInternodeEmergence{T} <: AbstractOrgan_EmergenceModel + TT_emergence::T + carbon_internode_creation_cost::T + leaf_surface_area::T + leaves_max_surface_area::T end ToyCustomInternodeEmergence(;TT_emergence=300.0, carbon_internode_creation_cost=200.0, leaf_surface_area=3.0, leaves_max_surface_area=100.0) = ToyCustomInternodeEmergence(TT_emergence, carbon_internode_creation_cost, leaf_surface_area, leaves_max_surface_area) @@ -26,7 +26,7 @@ PlantSimEngine.outputs_(m::ToyCustomInternodeEmergence) = (TT_cu_emergence=0.0, function PlantSimEngine.run!(m::ToyCustomInternodeEmergence, models, status, meteo, constants=nothing, sim_object=nothing) - leaves_surface_area = m.leaf_surface * get_n_leaves(status.node) + leaves_surface_area = m.leaf_surface_area * get_n_leaves(status.node) status.carbon_organ_creation_consumed = 0.0 if leaves_surface_area > m.leaves_max_surface_area @@ -138,11 +138,3 @@ mapping = Dict( mtg length(MultiScaleTreeGraph.traverse(mtg,x->x, symbol="Leaf")) - - for i in 1:365 - for j in 1:length(outs["Plant"][:carbon_organ_creation_consumed][i]) - if outs["Plant"][:carbon_organ_creation_consumed][i][1][j] != 0.0 - println(i) - end - end - end \ No newline at end of file diff --git a/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation2.jl b/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation2.jl index d8d67f2b0..a06bd6c2c 100644 --- a/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation2.jl +++ b/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation2.jl @@ -24,12 +24,12 @@ end PlantSimEngine.@process "organ_emergence" verbose = false -struct ToyCustomInternodeEmergence <: AbstractOrgan_EmergenceModel - TT_emergence::Float64 - carbon_internode_creation_cost::Float64 - leaf_surface_area::Float64 - leaves_max_surface_area::Float64 - water_leaf_threshold::Float64 +struct ToyCustomInternodeEmergence{T} <: AbstractOrgan_EmergenceModel + TT_emergence::T + carbon_internode_creation_cost::T + leaf_surface_area::T + leaves_max_surface_area::T + water_leaf_threshold::T end ToyCustomInternodeEmergence(;TT_emergence=300.0, carbon_internode_creation_cost=200.0, leaf_surface_area=3.0,leaves_max_surface_area=100.0, @@ -98,9 +98,9 @@ PlantSimEngine.ObjectDependencyTrait(::Type{<:ToyWaterAbsorptionModel}) = PlantS PlantSimEngine.@process "root_growth" verbose = false -struct ToyRootGrowthModel <: AbstractRoot_GrowthModel - water_threshold::Float64 - carbon_root_creation_cost::Float64 +struct ToyRootGrowthModel{T} <: AbstractRoot_GrowthModel + water_threshold::T + carbon_root_creation_cost::T root_max_len::Int end @@ -212,7 +212,7 @@ mapping = Dict( ) mtg = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Scene", 1, 0)) -#MultiScaleTreeGraph.Node(mtg, MultiScaleTreeGraph.NodeMTG("/", "Soil", 1, 1)) + plant = MultiScaleTreeGraph.Node(mtg, MultiScaleTreeGraph.NodeMTG("+", "Plant", 1, 1)) internode1 = MultiScaleTreeGraph.Node(plant, MultiScaleTreeGraph.NodeMTG("/", "Internode", 1, 2)) @@ -224,12 +224,9 @@ mapping = Dict( MultiScaleTreeGraph.Node(internode2, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) plant_root_start = MultiScaleTreeGraph.Node( - #MultiScaleTreeGraph.new_id(MultiScaleTreeGraph.get_root(plant)), plant, MultiScaleTreeGraph.NodeMTG("+", "Root", 1, 3), - #Dict{String, Any}("Root_len"=> 1) ) - #plant_root_start[:Root_len]=1 meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) diff --git a/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation3.jl b/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation3.jl index 1699f1f24..f28b3cdb6 100644 --- a/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation3.jl +++ b/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation3.jl @@ -24,12 +24,12 @@ end PlantSimEngine.@process "organ_emergence" verbose = false -struct ToyCustomInternodeEmergence <: AbstractOrgan_EmergenceModel - TT_emergence::Float64 - carbon_internode_creation_cost::Float64 - leaf_surface_area::Float64 - leaves_max_surface_area::Float64 - water_leaf_threshold::Float64 +struct ToyCustomInternodeEmergence{T} <: AbstractOrgan_EmergenceModel + TT_emergence::T + carbon_internode_creation_cost::T + leaf_surface_area::T + leaves_max_surface_area::T + water_leaf_threshold::T end ToyCustomInternodeEmergence(;TT_emergence=300.0, carbon_internode_creation_cost=200.0, leaf_surface_area=3.0,leaves_max_surface_area=100.0, @@ -101,8 +101,8 @@ PlantSimEngine.ObjectDependencyTrait(::Type{<:ToyWaterAbsorptionModel}) = PlantS PlantSimEngine.@process "root_growth" verbose = false -struct ToyRootGrowthModel <: AbstractRoot_GrowthModel - carbon_root_creation_cost +struct ToyRootGrowthModel{T} <: AbstractRoot_GrowthModel + carbon_root_creation_cost::T root_max_len::Int end @@ -130,9 +130,9 @@ end ########################## PlantSimEngine.@process "root_growth_decision" verbose = false -struct ToyRootGrowthDecisionModel <: AbstractRoot_Growth_DecisionModel - water_threshold::Float64 - carbon_root_creation_cost::Float64 +struct ToyRootGrowthDecisionModel{T} <: AbstractRoot_Growth_DecisionModel + water_threshold::T + carbon_root_creation_cost::T end PlantSimEngine.inputs_(::ToyRootGrowthDecisionModel) = @@ -180,7 +180,7 @@ PlantSimEngine.@process "leaf_carbon_capture" verbose = false struct ToyLeafCarbonCaptureModel<: AbstractLeaf_Carbon_CaptureModel end function PlantSimEngine.inputs_(::ToyLeafCarbonCaptureModel) - NamedTuple()#(TT_cu=-Inf) + NamedTuple() end function PlantSimEngine.outputs_(::ToyLeafCarbonCaptureModel) @@ -226,7 +226,7 @@ mapping = Dict( ) mtg = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", "Scene", 1, 0)) -#MultiScaleTreeGraph.Node(mtg, MultiScaleTreeGraph.NodeMTG("/", "Soil", 1, 1)) + plant = MultiScaleTreeGraph.Node(mtg, MultiScaleTreeGraph.NodeMTG("+", "Plant", 1, 1)) internode1 = MultiScaleTreeGraph.Node(plant, MultiScaleTreeGraph.NodeMTG("/", "Internode", 1, 2)) @@ -238,12 +238,9 @@ mapping = Dict( MultiScaleTreeGraph.Node(internode2, MultiScaleTreeGraph.NodeMTG("+", "Leaf", 1, 2)) plant_root_start = MultiScaleTreeGraph.Node( - #MultiScaleTreeGraph.new_id(MultiScaleTreeGraph.get_root(plant)), plant, MultiScaleTreeGraph.NodeMTG("+", "Root", 1, 3), - #Dict{String, Any}("Root_len"=> 1) ) - #plant_root_start[:Root_len]=1 meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18) From 36d5d2f402711413fe5a85b3d57e774b8be59191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Vezy?= Date: Fri, 7 Mar 2025 10:20:38 +0100 Subject: [PATCH 091/147] Update why_plantsimengine.md --- docs/src/introduction/why_plantsimengine.md | 125 ++++++++------------ 1 file changed, 52 insertions(+), 73 deletions(-) diff --git a/docs/src/introduction/why_plantsimengine.md b/docs/src/introduction/why_plantsimengine.md index 2bf7e6a43..9f7a77681 100644 --- a/docs/src/introduction/why_plantsimengine.md +++ b/docs/src/introduction/why_plantsimengine.md @@ -1,114 +1,93 @@ # Why PlantSimEngine? -PlantSimEngine was created to address specific challenges in plant modeling that weren't adequately met by existing frameworks. This page outlines the motivations behind its development and explains what sets it apart. +PlantSimEngine was developed to address fundamental limitations in existing plant modeling tools. This framework emerged from the need for a system that could efficiently handle the complex dynamics of the soil-plant-atmosphere continuum while remaining accessible to researchers and practitioners from diverse disciplines. -## The Landscape of Plant Modeling Tools +## The Current Landscape of Plant Modeling -Plant modeling has traditionally been approached through several different paradigms, each with its own strengths and limitations: +Plant modeling has evolved significantly over the years, but many existing tools face persistent challenges that limit their accessibility and efficiency. These tools generally fall into three categories: ### Monolithic Systems -Systems like APSIM[^1], CropBox[^2], GroIMP[^3], AMAPSim[^4], Helios[^5], and CPlantBox[^6] are powerful but present several challenges: +Systems like APSIM[^1], GroIMP[^2], AMAPStudio[^3], Helios[^4], and CPlantBox[^5] often present significant barriers to entry and adaptation. These include: -- Massive codebases that are difficult to navigate -- Rigid structures that resist heavy modifications -- Limited flexibility for integrating new models and difficulty to handle model coupling (has to be done by hand), especially when considering multi-scale modeling -- Implementations that are difficult to modify for specific use cases -- Steep learning curves for new users with a lot of implementation details (*e.g.* complex data structures) -- Require specialized developer-modeler expertise rather than supporting researcher-developers -- Not designed for rapid hypothesis testing or model iteration +Large, complex codebases that are difficult to navigate and modify, especially for scientists without extensive programming expertise. Researchers often spend more time understanding the implementation than developing the science behind their models. + +The rigid structure of these systems can limit the integration of new scientific ideas or methodologies, as they typically follow predefined frameworks that may not accommodate novel approaches. + +Many of these systems struggle with seamless multi-scale simulations and model coupling, making it challenging to represent the complex interactions between different processes in the soil-plant-atmosphere continuum. ### Distributed Systems -Frameworks such as OpenAlea[^7] and Crops in Silico[^8] offer more flexibility but suffer from what's known as the "two-language problem": +Platforms like OpenAlea[^6] and Crops in Silico[^7] have attempted to address some limitations of monolithic systems, but introduce their own challenges: + +These systems typically use accessible interfaces (often in Python) that prioritize ease of use but suffer from computational inefficiency, making large-scale simulations time-consuming. -- The interface language (Python) is accessible but computationally inefficient -- The computational backend is fast but difficult to modify -- Interfacing between components requires expertise in multiple languages -- Iteration cycles slow down significantly when performance optimization is needed +While their computational backends may be optimized for performance, extending or modifying them typically requires proficiency in multiple programming languages, creating a barrier for many researchers. + +The iteration cycle between design, implementation, and performance tuning is often slow, hindering rapid hypothesis testing and prototyping that is essential in research contexts. ### Architecture-Focused Tools -Many existing tools like AMAPSim[^4] and L-Py[^9] prioritize plant architectural modeling: +Tools like AMAPSim[^8] excel in specific aspects but have limitations in broader applications: + +These systems often prioritize structural modeling of plants over functional and environmental processes, limiting their utility for integrated studies of plant physiology and environmental responses. -- Integration of functional and environmental models is often an afterthought -- Implementation in languages like C++ or Java creates accessibility barriers -- Less suited for rapidly testing ecophysiological hypotheses +Implementation in languages like C++ or Java optimizes performance but can deter potential users who lack expertise in these languages, especially researchers with backgrounds in plant science rather than computer science. -## PlantSimEngine's Innovations +The design of these tools often makes them less suitable for rapid hypothesis testing and model prototyping, key activities in exploratory research. -PlantSimEngine addresses these limitations through several key innovations: +## The PlantSimEngine Solution + +PlantSimEngine brings together innovative ideas to overcome these limitations, offering a unique combination of features: ### Automatic Model Coupling -One of PlantSimEngine's most powerful features is its ability to automatically couple models: +**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. + +**Fine-Grained Model Control:** PlantSimEngine allows users to fix parameters, force variables to match observed values, or select simpler models for specific processes. This flexibility helps reduce overall system complexity while maintaining precision where it matters most. -- Leverages Julia's multiple-dispatch to compute the models dependency graph -- Models can be combined without writing explicit connection code -- Variables are automatically passed between processes based on dependencies, whatever the scale -- Supports multi-scale modeling with minimal effort, from organ to plant to landscape levels +**Adaptive Scalability:** The same framework efficiently supports both simple prototypes for single-plant studies and complex ecosystem simulations, scaling computational resources appropriately to the problem at hand. -### Flexibility with Control +### Outstanding Performance -PlantSimEngine gives researchers unprecedented control over their modeling process: +**High-Speed Computation:** Benchmarks demonstrate operations completing in hundreds of nanoseconds, making PlantSimEngine suitable for computationally intensive applications. For example, the [PlantBiophysics.jl implementation is over 38,000 times faster](https://vezy.github.io/PlantBiophysics-paper/notebooks_performance_Fig5_PlantBiophysics_performance/) than equivalent implementations in R. -- Switch between different model implementations with a simple syntax, without modifying the models code -- Reduce degrees of freedom by fixing variables to constant values -- Force variables to use observations instead of model predictions -- Use simpler models for specific processes to reduce complexity -- Scale from single plants to complex ecosystems with the same framework +**Computational Efficiency:** Julia's just-ahead-of-time compilation and native support for parallelism ensure that optimizations made during prototyping directly transfer to larger-scale applications, eliminating the need for reimplementation in a different language for performance gains. -### Performance Without Compromise +## Key Innovations -Performance is a core feature, not an afterthought: +PlantSimEngine's approach to plant modeling represents a paradigm shift in how scientists can build and use models: -- Benchmarks show operations in the 100th of nanoseconds range for complex models -- The [PlantBiophysics.jl implementation is over 38,000 times faster](https://vezy.github.io/PlantBiophysics-paper/notebooks_performance_Fig5_PlantBiophysics_performance/) than equivalent implementations in R -- Julia's just-ahead-of-time compilation model enables both high-level abstraction and low-level performance -- Automatic parallelization across objects, time-steps, and independent processes -- Performance optimization is just a little bit more work on the same code, not a complete rewrite +- **Uniform API:** Standardized interfaces make it easy to define new processes and component models, reducing the cognitive load on researchers. -### Streamlined Model Development +- **Automatic Dependency Resolution:** The system automatically determines the relationships between different models and processes, eliminating the need for manual coupling. -The framework handles tedious aspects automatically: +- **Seamless Parallelization:** Out-of-the-box support for parallel and distributed computation allows researchers to focus on the science rather than implementation details. -- Input and output variable management -- Time-step handling -- Object instantiation and tracking -- Dependency resolution between processes -- Unit propagation through integration with Unitful.jl -- Error propagation via MonteCarloMeasurements.jl +- **Flexible Model Integration:** The ability to easily combine models from different sources and at different scales facilitates more comprehensive and realistic simulations. -## From Research to Application +- **User-Centric Design:** Emphasizing usability ensures that researchers with varied programming backgrounds can effectively engage with the system. -PlantSimEngine aims at bridging the gap between academic modeling and practical applications: +By addressing the key limitations of existing plant modeling tools, PlantSimEngine enables researchers to focus more on scientific questions and less on technical implementation details, accelerating the pace of discovery in plant science, agronomy, and related fields. -- Researchers can develop and refine models in a comfortable environment -- Models can be deployed in production environments with minimal modification, as a script or an executable -- The same framework works for hypothesis testing and real-world simulations -- Performance in the prototype directly translates to performance in the field +[^1]: Holzworth, D. P. et al. APSIM – Evolution towards a new generation of agricultural systems simulation. Environmental Modelling & Software 62, 327-350 (2014). -## Community-Oriented Development +[^2]: Hemmerling, R., Kniemeyer, O., Lanwert, D., Kurth, W. & Buck-Sorlin, G. The rule-based language XL and the modelling environment GroIMP illustrated with simulated tree competition. Funct. Plant Biol. 35, 739 (2008). -PlantSimEngine is designed with the community in mind: +[^3]: Griffon, S., and de Coligny, F. « AMAPstudio: An editing and simulation software suite for plants architecture modelling ». Ecological Modelling 290 (2014): 3‑10. https://doi.org/10.1016/j.ecolmodel.2013.10.037. -- MIT license encourages wide adoption and contribution -- Accessible to researchers with varying levels of programming experience -- Evolving based on real-world requirements from projects like XPalm and PlantBiophysics -- Standardized approach encourages model sharing and reproducibility +[^4]: Bailey, R. Spatial Modeling Environment for Enhancing Conifer Crown Management. Front. For. Glob. Change 3, 106 (2020). -## Conclusion +[^5]: Schnepf, A., Leitner, D., Landl, M., Lobet, G., Mai, T. H., Morandage, S., Sheng, C., Zörner, M., Vanderborght, J., & Vereecken, H. CPlantBox: A whole-plant modelling framework for the simulation of water- and carbon-related processes. in silico Plants, 63 (2018). -By combining high-level abstraction with exceptional performance, PlantSimEngine lets researchers focus on their scientific questions rather than computational implementation details. It represents a new approach to plant modeling that emphasizes accessibility, flexibility, and efficiency—accelerating innovation in plant science and its applications. +[^6]: Pradal, C. et al. OpenAlea: A visual programming and component-based software platform for plant modeling. Funct. Plant Biol. 35, 751-760 (2008). -## References +[^7]: Marshall-Colon, A. et al. Crops In Silico: Generating Virtual Crops Using an Integrative and Multi-Scale Modeling Platform. Frontiers in Plant Science 8 (2017). https://doi.org/10.3389/fpls.2017.00786. -[^1]: Holzworth, D. P. et al. APSIM – Evolution towards a new generation of agricultural systems simulation. Environ. Model. Softw. 62, 327–350 (2014). -[^2]: Yun, K. & Kim, S.-H. Cropbox: a declarative crop modelling framework. Silico Plants 5, (2022). -[^3]: Kniemeyer, O. (2004). Rule-based modelling with the XL/GroIMP software. The logic of artificial life. Proceedings of 6th GWAL. AKA Akademische Verlagsges Berlin, 56-65. -[^4]: Barczi, J.-F. et al. AmapSim: A Structural Whole-plant Simulator Based on Botanical Knowledge and Designed to Host External Functional Models. Ann. Bot. 101, 1125–1138 (2008). -[^5]: Bailey, B. N. (2019). Helios: A scalable 3D plant and environmental biophysical modeling framework. Frontiers in Plant Science, 10, 1185. -[^6]: Giraud, M. et al. CPlantBox: a fully coupled modelling platform for the water and carbon fluxes in the soil–plant–atmosphere continuum. Silico Plants 5, diad009 (2023). -[^7]: Pradal, C., Dufour-Kowalski, S., Boudon, F., Fournier, C. & Godin, C. OpenAlea: a visual programming and component-based software platform for plant modelling. Funct. Plant Biol. 35, 751–760 (2008). -[^8]: Marshall-Colon, A. et al. Crops In Silico: Generating Virtual Crops Using an Integrative and Multi-scale Modeling Platform. Front. Plant Sci. 8, (2017). -[^9]: Boudon, F., Pradal, C., Cokelaer, T., Prusinkiewicz, P. & Godin, C. L-Py: An L-System Simulation Framework for Modeling Plant Architecture Development Based on a Dynamic Language. Front. Plant Sci. 3, (2012). \ No newline at end of file +[^8]: Barczi, J.-F., Rey, H., Caraglio, Y., Reffye, P. de, Barthélémy, D., Dong, Q. X., & Fourcaud, T. AmapSim: A Structural Whole-plant Simulator Based on Botanical Knowledge and Designed to Host External Functional Models. Annals of botany, 101(8), 1125-1138 (2008). From 816376b9d2175a11400fc0513e32ebead07e2f4f Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Fri, 7 Mar 2025 16:58:28 +0100 Subject: [PATCH 092/147] Minor documentation edits --- docs/src/multiscale/multiscale_example_1.md | 8 +++++--- docs/src/prerequisites/key_concepts.md | 4 +++- docs/src/step_by_step/implement_a_model_additional.md | 2 +- docs/src/step_by_step/quick_and_dirty_examples.md | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/src/multiscale/multiscale_example_1.md b/docs/src/multiscale/multiscale_example_1.md index f6dd90343..fabee44dc 100644 --- a/docs/src/multiscale/multiscale_example_1.md +++ b/docs/src/multiscale/multiscale_example_1.md @@ -1,7 +1,5 @@ # Writing a multiscale simulation -TODO change Toy To Example ? - This three-part subsection walks you through building a multi-scale simulation from scratch. It is meant as an illustration of the iterative process you might go through when building and slowly tuning a Functional-Structural Plant Model, where previous multi-scale examples focused more on the API syntax. You can find the full script for the first part's toy simulation in the [ToyMultiScalePlantModel](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/ToyMultiScalePlantModel/ToyPlantSimulation1.jl) subfolder of the examples folder. @@ -206,7 +204,7 @@ as opposed to the single-valued carbon stock mapped variable : PreviousTimeStep(:carbon_stock)=>"Plant"], ``` -And of course, some variables need to be initialized in the status +And of course, some variables need to be initialized in the status: ```julia mapping = Dict( @@ -232,6 +230,10 @@ mapping = Dict( "Leaf" => ToyLeafCarbonCaptureModel(), ) ``` + +!!! note + This excerpt (and the complete script file) showcase the final properly initialized mapping, but when developing, you are encouraged to make liberal use of the helper function `to_initialize`(@ref) and check the PlantSimEngine user errors. + ### Running a simulation We only need an MTG, and some weather data, and then we'll be set. Let's create a simple MTG : diff --git a/docs/src/prerequisites/key_concepts.md b/docs/src/prerequisites/key_concepts.md index f8bc7c6cb..e2f039ad6 100644 --- a/docs/src/prerequisites/key_concepts.md +++ b/docs/src/prerequisites/key_concepts.md @@ -34,7 +34,9 @@ The companion package PlantBiophysics.jl provides the [`Beer`](https://vezy.gith Models can also be used for ad hoc computations that aren't directly tied to a specific literature-defined physiological process. In PlantSimEngine, everything is a model. There are many instances where a custom model might be practical to aggregate some computations or handle other information. To illustrate, XPalm, the Oil Palm model has a few models that handle the state of different organs, and a mdoel to handle leaf pruning, which you can find [here](https://github.com/PalmStudio/XPalm.jl/blob/main/src/plant/phytomer/leaves/leaf_pruning.jl). -To prepare a simulation, you declare a ModelList with whatever models you wish to make use of. TODO example For multi-scale simulations, a more involved mapping is required, see below. TODO +To prepare a simulation, you declare a ModelList with whatever models you wish to make use of and initialize necessary parameters: see the [step_by_step](#step_by_step) section to learn how to use them in practice. + +For multi-scale simulations, models need to be tied to a particular scale when used. See the [Multiscale modeling](@ref) section below, or the [Multi-scale considerations](@ref) page for a more detailed description of multi-scale peculiarities. ### Variables, inputs, outputs, and simple model coupling diff --git a/docs/src/step_by_step/implement_a_model_additional.md b/docs/src/step_by_step/implement_a_model_additional.md index 8d192b9f2..4e3f24ae5 100644 --- a/docs/src/step_by_step/implement_a_model_additional.md +++ b/docs/src/step_by_step/implement_a_model_additional.md @@ -85,7 +85,7 @@ The `;` syntax indicates that subsequent arguments are provided as keyword argum Beer(k = 0.7) ``` -This helps readability when there are a lot of parameters and some have default values, but again. +This helps readability when there are a lot of parameters and some have default values. ### eltype diff --git a/docs/src/step_by_step/quick_and_dirty_examples.md b/docs/src/step_by_step/quick_and_dirty_examples.md index 8420e8590..44615cdb3 100644 --- a/docs/src/step_by_step/quick_and_dirty_examples.md +++ b/docs/src/step_by_step/quick_and_dirty_examples.md @@ -3,7 +3,7 @@ This page is meant for people who have set up their environment and just want to copy-paste an example or two, see what the REPL returns and start tinkering. If you are less comfortable with Julia, or need to set up an environment first, see this page : [Getting started with Julia](@ref). -If you wish for a more detailed rundown of the examples, you can instead have a look at the [step by step][#step_by_step] section, which will go into more detail. +If you wish for a more detailed rundown of the examples, you can instead have a look at the [step by step](#step_by_step) section, which will go into more detail. These examples are all for single-scale simulations. For multi-scale modelling tutorials and examples, refer to [this section][#multiscale] From 143fdaf26d6d5221b0be9038d7f5186cf2a21808 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Fri, 7 Mar 2025 16:59:02 +0100 Subject: [PATCH 093/147] Add some more to_initialize tests (only on correct mappings though, need some more on incomplete mappings) --- test/test-ModelList.jl | 1 + test/test-corner-cases.jl | 12 +++++++++++- test/test-mapping.jl | 10 +++++++--- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/test/test-ModelList.jl b/test/test-ModelList.jl index d5a60f898..b21df0bef 100644 --- a/test/test-ModelList.jl +++ b/test/test-ModelList.jl @@ -286,6 +286,7 @@ end=# meteo_adjusted = PlantSimEngine.adjust_weather_timesteps_to_given_length( PlantSimEngine.get_status_vector_max_length(modellist.status) , meteo) mtg, mapping, outputs_mapping, nsteps, filtered_outputs_modellist = test_filtered_output_begin(modellist, status_tuple, out_tuple, meteo_adjusted) + @test to_initialize(mapping) == Dict() @test test_filtered_output(mtg, mapping, nsteps, outputs_mapping, meteo_adjusted, filtered_outputs_modellist) end end diff --git a/test/test-corner-cases.jl b/test/test-corner-cases.jl index 9bb8dbb60..cc881a23d 100644 --- a/test/test-corner-cases.jl +++ b/test/test-corner-cases.jl @@ -345,6 +345,8 @@ end ), ) + @test to_initialize(mapping) == Dict() + outs = Dict( "E1" => (:e1, :f1, :e2, :f2), "E3" => (:e3,) @@ -469,6 +471,8 @@ end ), ) + @test to_initialize(mapping) == Dict() + outs = Dict( "E1" => (:out, :out1), "E2" => (:out, :out2), @@ -520,6 +524,9 @@ end Status(var1=10.0, var2=1.0,) ) ) + + @test to_initialize(m) == Dict() + vars = Dict{String,Any}("Leaf" => (:var1,)) out = run!(mtg, m, Atmosphere(T=20.0, Wind=1.0, Rh=0.65), tracked_outputs=vars, executor=SequentialEx()) df = convert_outputs(out, DataFrame) @@ -552,6 +559,7 @@ end Status(var1=15.0, var2=0.3,), ), ) + @test to_initialize(mapping) == Dict() sim = run!(mtg, mapping, meteo; tracked_outputs=outs) using DataFrames @@ -601,7 +609,9 @@ end ToyToyModel(1), status=(a=1, b=0, c=0), #nsteps = length(meteo) - ) + ) + @test to_initialize(model) == NamedTuple() + sim = run!(model, meteo) @test DataFrames.nrow(sim) == PlantSimEngine.get_nsteps(meteo) end \ No newline at end of file diff --git a/test/test-mapping.jl b/test/test-mapping.jl index 3c6ffc8d5..a96c6aa80 100755 --- a/test/test-mapping.jl +++ b/test/test-mapping.jl @@ -198,11 +198,11 @@ PlantSimEngine.ObjectDependencyTrait(::Type{<:ToyTestDegreeDaysCumulModel}) = Pl # fully automated model generation st2 = (TT_cu=Vector(cumsum(meteo_day.TT)),) - # TODO outputs name conflict if this is just named outputs - # TODO when outputs filtering is implemented, can test it with this function mtg, mapping, outputs_mapping = PlantSimEngine.modellist_to_mapping(models, st2; nsteps=nsteps, outputs=nothing) - graphsim2 = PlantSimEngine.GraphSimulation(mtg, mapping, nsteps=nsteps, check=true, outputs=outputs_mapping) + @test to_initialize(mapping) == Dict() + + graphsim2 = PlantSimEngine.GraphSimulation(mtg, mapping, nsteps=nsteps, check=true, outputs=outputs_mapping) sim2 = run!(graphsim2, meteo_day, @@ -268,6 +268,8 @@ mtg = import_mtg_example(); mapping_without_vectors = PlantSimEngine.replace_mapping_status_vectors_with_generated_models(mapping_with_vector, "Soil", nsteps) +@test to_initialize(mapping_without_vectors) == Dict() + graph_sim_multiscale = @test_nowarn PlantSimEngine.GraphSimulation(mtg, mapping_without_vectors, nsteps=nsteps, check=true, outputs=out_multiscale) sim_multiscale = run!(graph_sim_multiscale, @@ -322,6 +324,8 @@ mapping_without_vectors = PlantSimEngine.replace_mapping_status_vectors_with_gen mapping_without_vectors_2 = PlantSimEngine.replace_mapping_status_vectors_with_generated_models(mapping_with_two_vectors, "Soil", nsteps) graph_sim_multiscale_2 = @test_nowarn PlantSimEngine.GraphSimulation(mtg, mapping_without_vectors_2, nsteps=nsteps, check=true, outputs=out_multiscale) + @test to_initialize(mapping_without_vectors_2) == Dict() + sim_multiscale_2 = run!(graph_sim_multiscale_2, meteo_day, PlantMeteo.Constants(), From 68b37d084788dd91708d3794b90b30f33b838edd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Vezy?= Date: Fri, 7 Mar 2025 17:15:46 +0100 Subject: [PATCH 094/147] Update index and readme --- README.md | 61 +++++++++++++++++++++++++++++++---------- docs/src/index.md | 69 ++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 103 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 6b77ba8d6..864091693 100644 --- a/README.md +++ b/README.md @@ -9,27 +9,54 @@ [![DOI](https://zenodo.org/badge/571659510.svg)](https://zenodo.org/badge/latestdoi/571659510) [![JOSS](https://joss.theoj.org/papers/137e3e6c2ddc349bec39e06bb04e4e09/status.svg)](https://joss.theoj.org/papers/137e3e6c2ddc349bec39e06bb04e4e09) +- [PlantSimEngine](#plantsimengine) + - [Overview](#overview) + - [Unique Features](#unique-features) + - [Automatic Model Coupling](#automatic-model-coupling) + - [Flexibility with Precision Control](#flexibility-with-precision-control) + - [Batteries included](#batteries-included) + - [Ask Questions](#ask-questions) + - [Installation](#installation) + - [Example usage](#example-usage) + - [Simple example](#simple-example) + - [Model coupling](#model-coupling) + - [Multiscale modelling](#multiscale-modelling) + - [Projects that use PlantSimEngine](#projects-that-use-plantsimengine) + - [Performance](#performance) + - [Make it yours](#make-it-yours) + ## 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. +- **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 of inputs, outputs, time-steps, objects, and dependency resolution. -- Iterative model 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. -- Flexible Model Switching: Switch between models without changing model's code, using a simple syntax to specify the model for a given process and scale. -- Achieve high-speed computations, with benchmarks showing operations in the 100th of nanoseconds range for complex models (see this [benchmark script](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/benchmark.jl)). -- 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](https://juliafolds.github.io/FLoops.jl/stable/). -- Scale: Scale easily with methods for computing over objects, time-steps, and [Multi-Scale Tree Graphs](https://github.com/VEZY/MultiScaleTreeGraph.jl). -- Compose: Use any types as inputs, including [Unitful](https://github.com/PainterQubits/Unitful.jl) for unit propagation and [MonteCarloMeasurements.jl](https://github.com/baggepinnen/MonteCarloMeasurements.jl) for measurement error propagation. +- **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](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/benchmark.jl)). +- **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](https://juliafolds.github.io/FLoops.jl/stable/). +- **Scale Effortlessly**: Methods for computing over objects, time-steps, and [Multi-Scale Tree Graphs](https://github.com/VEZY/MultiScaleTreeGraph.jl). +- **Compose Freely**: Use any types as inputs, including [Unitful](https://github.com/PainterQubits/Unitful.jl) for unit propagation and [MonteCarloMeasurements.jl](https://github.com/baggepinnen/MonteCarloMeasurements.jl) for measurement error propagation. ## Ask Questions @@ -314,8 +341,14 @@ An example output of a multiscale simulation is shown in the documentation of Pl Take a look at these projects that use PlantSimEngine: -- [PlantBiophysics.jl](https://github.com/VEZY/PlantBiophysics.jl) -- [XPalm](https://github.com/PalmStudio/XPalm.jl) +- [PlantBiophysics.jl](https://github.com/VEZY/PlantBiophysics.jl) - For the simulation of biophysical processes for plants such as photosynthesis, conductance, energy fluxes, and temperature +- [XPalm](https://github.com/PalmStudio/XPalm.jl) - An experimental crop model for oil palm + +## 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 to perform (about 688 ns per day), and 275 μs (756 ns per day) when coupled to a light interception model. 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. ## Make it yours diff --git a/docs/src/index.md b/docs/src/index.md index 94831a89f..697124245 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -49,21 +49,41 @@ Depth = 5 **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. +- **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 of inputs, outputs, time-steps, objects, and dependency resolution. -- Iterative model 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. -- Flexible Model Switching: Switch between models without changing model's code, using a simple syntax to specify the model for a given process and scale. -- Achieve high-speed computations, with benchmarks showing operations in the 100th of nanoseconds range for complex models (see this [benchmark script](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/benchmark.jl)). -- 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](https://juliafolds.github.io/FLoops.jl/stable/). -- Scale: Scale easily with methods for computing over objects, time-steps, and [Multi-Scale Tree Graphs](https://github.com/VEZY/MultiScaleTreeGraph.jl). -- Compose: Use any types as inputs, including [Unitful](https://github.com/PainterQubits/Unitful.jl) for unit propagation and [MonteCarloMeasurements.jl](https://github.com/baggepinnen/MonteCarloMeasurements.jl) for measurement error propagation. +- **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](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/examples/benchmark.jl)). +- **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](https://juliafolds.github.io/FLoops.jl/stable/). +- **Scale Effortlessly**: Methods for computing over objects, time-steps, and [Multi-Scale Tree Graphs](https://github.com/VEZY/MultiScaleTreeGraph.jl). +- **Compose Freely**: Use any types as inputs, including [Unitful](https://github.com/PainterQubits/Unitful.jl) for unit propagation and [MonteCarloMeasurements.jl](https://github.com/baggepinnen/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 @@ -178,7 +198,7 @@ lines!(ax2, out2[:TT_cu], out2[:aPPFD], color=:firebrick1) fig ``` -### Multiscale modelling +### Multi-scale modeling > See the [Multi-scale modeling](#multi-scale-modeling) section for more details. @@ -283,6 +303,29 @@ An example output of a multiscale simulation is shown in the documentation of Pl ![Plant growth simulation](www/image.png) +## 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: From 17d92b08d8e6b37420c9ba9175d83a7e959bb8cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Vezy?= Date: Fri, 7 Mar 2025 17:32:51 +0100 Subject: [PATCH 095/147] Update why_plantsimengine.md --- docs/src/introduction/why_plantsimengine.md | 48 +++++++++++++-------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/docs/src/introduction/why_plantsimengine.md b/docs/src/introduction/why_plantsimengine.md index 9f7a77681..ed8759658 100644 --- a/docs/src/introduction/why_plantsimengine.md +++ b/docs/src/introduction/why_plantsimengine.md @@ -4,41 +4,45 @@ PlantSimEngine was developed to address fundamental limitations in existing plan ## The Current Landscape of Plant Modeling -Plant modeling has evolved significantly over the years, but many existing tools face persistent challenges that limit their accessibility and efficiency. These tools generally fall into three categories: +Plant modeling has evolved significantly over the years, with different tools making different design tradeoffs to address specific research needs. These tools generally fall into three categories, each with their own strengths and limitations: ### Monolithic Systems -Systems like APSIM[^1], GroIMP[^2], AMAPStudio[^3], Helios[^4], and CPlantBox[^5] often present significant barriers to entry and adaptation. These include: +Systems like APSIM[^1], GroIMP[^2], AMAPStudio[^3], Helios[^4], and CPlantBox[^5] offer comprehensive functionality but present certain tradeoffs: -Large, complex codebases that are difficult to navigate and modify, especially for scientists without extensive programming expertise. Researchers often spend more time understanding the implementation than developing the science behind their models. +These systems provide robust, well-tested frameworks with established scientific validity, but their large, complex codebases can be challenging to navigate and modify without extensive programming expertise. -The rigid structure of these systems can limit the integration of new scientific ideas or methodologies, as they typically follow predefined frameworks that may not accommodate novel approaches. +Their comprehensive architecture offers a wealth of integrated features but may require adaptation when implementing novel approaches that don't align with their predefined frameworks. -Many of these systems struggle with seamless multi-scale simulations and model coupling, making it challenging to represent the complex interactions between different processes in the soil-plant-atmosphere continuum. +They excel at specific types of simulations but may require additional engineering effort for seamless multi-scale simulations and model coupling across the soil-plant-atmosphere continuum. + +These platforms typically require dedicated engineering resources for maintenance and extension, with research teams often needing specialized technical staff to implement new models. ### Distributed Systems -Platforms like OpenAlea[^6] and Crops in Silico[^7] have attempted to address some limitations of monolithic systems, but introduce their own challenges: +Platforms like OpenAlea[^6] and Crops in Silico[^7] offer different advantages and tradeoffs: + +These systems provide accessible interfaces (often in Python) that prioritize ease of use and flexibility, making them approachable for many researchers, though they may require performance optimization for large-scale simulations. -These systems typically use accessible interfaces (often in Python) that prioritize ease of use but suffer from computational inefficiency, making large-scale simulations time-consuming. +Their modular nature facilitates component reuse and integration, while sometimes requiring proficiency in multiple programming languages for extending computational backends. -While their computational backends may be optimized for performance, extending or modifying them typically requires proficiency in multiple programming languages, creating a barrier for many researchers. +They support diverse modeling paradigms but may involve a longer iteration cycle between design, implementation, and performance tuning compared to more specialized tools. -The iteration cycle between design, implementation, and performance tuning is often slow, hindering rapid hypothesis testing and prototyping that is essential in research contexts. +While offering flexibility, implementing complex models often requires significant developer time, especially when optimizing performance using lower-level languages. ### Architecture-Focused Tools -Tools like AMAPSim[^8] excel in specific aspects but have limitations in broader applications: +Tools like AMAPSim[^8] make specific design choices that benefit certain applications: -These systems often prioritize structural modeling of plants over functional and environmental processes, limiting their utility for integrated studies of plant physiology and environmental responses. +These systems excel in their focused domains (such as structural modeling of plants) while requiring integration with other tools for comprehensive studies of plant physiology and environmental responses. -Implementation in languages like C++ or Java optimizes performance but can deter potential users who lack expertise in these languages, especially researchers with backgrounds in plant science rather than computer science. +Their implementation in languages like C++ or Java delivers excellent performance but represents a tradeoff in terms of accessibility for researchers without expertise in these languages. -The design of these tools often makes them less suitable for rapid hypothesis testing and model prototyping, key activities in exploratory research. +They provide sophisticated functionality in their target domains but may require additional work for rapid hypothesis testing and model prototyping across diverse aspects of plant science. ## The PlantSimEngine Solution -PlantSimEngine brings together innovative ideas to overcome these limitations, offering a unique combination of features: +PlantSimEngine brings together innovative ideas to address these various tradeoffs, offering a unique combination of features: ### Automatic Model Coupling @@ -60,6 +64,16 @@ PlantSimEngine brings together innovative ideas to overcome these limitations, o **Computational Efficiency:** Julia's just-ahead-of-time compilation and native support for parallelism ensure that optimizations made during prototyping directly transfer to larger-scale applications, eliminating the need for reimplementation in a different language for performance gains. +### Developer Efficiency + +**Reduced Implementation Time:** PlantSimEngine leverages Julia's dynamic language features while maintaining the performance of statically-compiled languages. This significantly reduces the time researchers spend implementing and optimizing models. + +**Modular Building Blocks:** The component-based architecture allows models to be built as unit components that can be stacked like building blocks to create complex systems. This modularity dramatically increases code reuse and reduces redundant implementation efforts. + +**No Engineering Overhead:** Unlike monolithic systems that require dedicated engineering teams or distributed platforms that need backend optimization, PlantSimEngine enables domain scientists to independently develop high-performance models without specialized programming expertise. + +**Rapid Prototyping to Production:** The same code used for quick prototyping can transition directly to production-scale simulations without rewriting, eliminating the traditional gap between exploratory research and application. + ## Key Innovations PlantSimEngine's approach to plant modeling represents a paradigm shift in how scientists can build and use models: @@ -74,13 +88,13 @@ PlantSimEngine's approach to plant modeling represents a paradigm shift in how s - **User-Centric Design:** Emphasizing usability ensures that researchers with varied programming backgrounds can effectively engage with the system. -By addressing the key limitations of existing plant modeling tools, PlantSimEngine enables researchers to focus more on scientific questions and less on technical implementation details, accelerating the pace of discovery in plant science, agronomy, and related fields. +By offering solutions to the various tradeoffs present in existing modeling approaches, PlantSimEngine enables researchers to focus more on scientific questions and less on technical implementation details, accelerating the pace of discovery in plant science, agronomy, and related fields. [^1]: Holzworth, D. P. et al. APSIM – Evolution towards a new generation of agricultural systems simulation. Environmental Modelling & Software 62, 327-350 (2014). [^2]: Hemmerling, R., Kniemeyer, O., Lanwert, D., Kurth, W. & Buck-Sorlin, G. The rule-based language XL and the modelling environment GroIMP illustrated with simulated tree competition. Funct. Plant Biol. 35, 739 (2008). -[^3]: Griffon, S., and de Coligny, F. « AMAPstudio: An editing and simulation software suite for plants architecture modelling ». Ecological Modelling 290 (2014): 3‑10. https://doi.org/10.1016/j.ecolmodel.2013.10.037. +[^3]: Griffon, S., and de Coligny, F. AMAPstudio: An editing and simulation software suite for plants architecture modelling. Ecological Modelling 290 (2014): 3‑10. . [^4]: Bailey, R. Spatial Modeling Environment for Enhancing Conifer Crown Management. Front. For. Glob. Change 3, 106 (2020). @@ -88,6 +102,6 @@ By addressing the key limitations of existing plant modeling tools, PlantSimEngi [^6]: Pradal, C. et al. OpenAlea: A visual programming and component-based software platform for plant modeling. Funct. Plant Biol. 35, 751-760 (2008). -[^7]: Marshall-Colon, A. et al. Crops In Silico: Generating Virtual Crops Using an Integrative and Multi-Scale Modeling Platform. Frontiers in Plant Science 8 (2017). https://doi.org/10.3389/fpls.2017.00786. +[^7]: Marshall-Colon, A. et al. Crops In Silico: Generating Virtual Crops Using an Integrative and Multi-Scale Modeling Platform. Frontiers in Plant Science 8 (2017). . [^8]: Barczi, J.-F., Rey, H., Caraglio, Y., Reffye, P. de, Barthélémy, D., Dong, Q. X., & Fourcaud, T. AmapSim: A Structural Whole-plant Simulator Based on Botanical Knowledge and Designed to Host External Functional Models. Annals of botany, 101(8), 1125-1138 (2008). From 3366791b565c67aa5d425240f4dca9c54bedce0f Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 10 Mar 2025 14:40:06 +0100 Subject: [PATCH 096/147] Patch up more links, document two more types of error, add the ML image, fix type promotion example --- docs/src/prerequisites/julia_basics.md | 5 +- docs/src/step_by_step/implement_a_model.md | 3 +- .../implement_a_model_additional.md | 5 +- ...lantsimengine_and_julia_troubleshooting.md | 118 +++++++++++++++++- .../tips_and_workarounds.md | 8 +- .../working_with_data/visualising_outputs.md | 4 +- docs/src/www/l1_flux-vs-tensorflow.png | Bin 0 -> 228317 bytes 7 files changed, 132 insertions(+), 11 deletions(-) create mode 100644 docs/src/www/l1_flux-vs-tensorflow.png diff --git a/docs/src/prerequisites/julia_basics.md b/docs/src/prerequisites/julia_basics.md index 10c86ac0a..1ed1a7b60 100644 --- a/docs/src/prerequisites/julia_basics.md +++ b/docs/src/prerequisites/julia_basics.md @@ -69,4 +69,7 @@ For example : ### Function arguments and kwargs -### NamedTuples \ No newline at end of file +### NamedTuples + +## "less essential" +### Type promotion and splatting \ No newline at end of file diff --git a/docs/src/step_by_step/implement_a_model.md b/docs/src/step_by_step/implement_a_model.md index 253679dd5..f7f79b668 100644 --- a/docs/src/step_by_step/implement_a_model.md +++ b/docs/src/step_by_step/implement_a_model.md @@ -205,9 +205,8 @@ To use this model, users will have to make sure that the variables for that mode Model parameters are available from the `ModelList` that is passed via the `models` argument. Index by the process name, then the parameter name. For example, the `k` parameter of the `Beer` model is found in `models.light_interception.k`. -TODO !!! warning - You need to import all the functions you want to extend, so Julia knows your intention of adding a method to the function from PlantSimEngine, and not defining your own function. To do so, you have to prefix the said functions by the package name, or import them before *e.g.*: `import PlantSimEngine: inputs_, outputs_` + You need to import all the functions you want to extend, so Julia knows your intention of adding a method to the function from PlantSimEngine, and not defining your own function. To do so, you have to prefix the said functions by the package name, or import them before *e.g.*: `import PlantSimEngine: inputs_, outputs_`. The troubleshooting subsection [Implementing a model: forgetting to import or prefix functions](@ref) showcases output errors that can occur when you forget to prefix. ### Parallelization traits diff --git a/docs/src/step_by_step/implement_a_model_additional.md b/docs/src/step_by_step/implement_a_model_additional.md index 4e3f24ae5..b3f943669 100644 --- a/docs/src/step_by_step/implement_a_model_additional.md +++ b/docs/src/step_by_step/implement_a_model_additional.md @@ -45,10 +45,13 @@ To add type promotion to `Beer2` we would do: ```julia function Beer2(k,x) - Beer2(promote(k,x)) + Beer2(promote(k,x)...) end ``` +!!! note + `promote` returns a NamedTuple, which needs to be splatted for the constructor, see the [Julia docs](https://docs.julialang.org/en/v1/manual/conversion-and-promotion/#Promotion) for a more in-depth explanation, or our [Getting started with Julia](@ref) page for some links to other references discussing Julia concepts used in PlantSimEngine. + This would allow users to instantiate the model parameters using different types of inputs. For example users may write the following: ```julia diff --git a/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md b/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md index eee67c227..6bcff9315 100644 --- a/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md +++ b/docs/src/troubleshooting_and_testing/plantsimengine_and_julia_troubleshooting.md @@ -97,7 +97,76 @@ The syntax for an empty NamedTuple is `NamedTuple()`. If instead one types `()` ## PlantSimEngine user errors -Most of these occur exclusively in multi-scale simulations, which has a slightly more complex API, but some are common to both single- and multi-scale simulations. +Most of the following errors occur exclusively in multi-scale simulations, which has a slightly more complex API, but some are common to both single- and multi-scale simulations. + +### Implementing a model: forgetting to import or prefix functions + +When implementing a model, you need to make sure that your implementation is correctly recognised as extending `PlantSimEngine` methods and types, and not writing new independent ones. + +In the following working toy model implementation, note that the `inputs_`, `outputs_` and `run!` function are all prefixed with the module name. If there were hard dependencies to manage, the `dep` function would also be identically prefixed. + +```julia +using PlantSimEngine +@process "toy" verbose = false + +struct ToyToyModel{T} <: AbstractToyModel + internal_constant::T +end + +function PlantSimEngine.inputs_(::ToyToyModel) + (a = -Inf, b = -Inf, c = -Inf) +end + +function PlantSimEngine.outputs_(::ToyToyModel) + (d = -Inf, e = -Inf) +end + + +function PlantSimEngine.run!(m::ToyToyModel, models, status, meteo, constants=nothing, extra_args=nothing) + status.d = m.internal_constant * status.a + status.e += m.internal_constant +end + +meteo = Weather([ + Atmosphere(T=20.0, Wind=1.0, Rh=0.65, Ri_PAR_f=200.0), + Atmosphere(T=20.0, Wind=1.0, Rh=0.65, Ri_PAR_f=200.0), + Atmosphere(T=18.0, Wind=1.0, Rh=0.65, Ri_PAR_f=100.0), +]) + +model = ModelList( + ToyToyModel(1), + status = ( a = 1, b = 0, c = 0), +) +to_initialize(model) +sim = PlantSimEngine.run!(model, meteo) +``` + +If you declare these functions without importing them first, or prefixing them with the module name, they will be considered to be part of your current environment, and won't be extending PlantSimEngine methods, which means PlantSimEngine will not be able to properly make use of your functions, and simulations are likely to error, or run incorrectly. + +Forgetting to prefix the `run!` function definition gives the following error : +```julia +ERROR: MethodError: no method matching run!(::ModelList{@NamedTuple{…}, Status{…}}, ::TimeStepTable{Atmosphere{…}}) +The function `run!` exists, but no method is defined for this combination of argument types. + +Closest candidates are: + run!(::ToyToyModel, ::Any, ::Any, ::Any, ::Any, ::Any) + @ Main ~/path/to/file.jl:20 +``` + +Forgetting to prefix the `inputs_`or `outputs_` functions for your model might not always generate an error, depending on whether the variables declared in this function are present in your ModelList or mapping's corresponding Status. + +In cases where they do throw an error, you may get the following kind of output: +```julia +ERROR: type NamedTuple has no field d +Stacktrace: + [1] setproperty!(mnt::Status{(:a, :b, :c), Tuple{…}}, s::Symbol, x::Int64) + @ PlantSimEngine ~/path/to/package/PlantSimEngine/src/component_models/Status.jl:100 + [2] run!(m::ToyToyModel{…}, models::@NamedTuple{…}, status::Status{…}, meteo::PlantMeteo.TimeStepRow{…}, constants::Constants{…}, extra_args::Nothing) + ... +``` + +!!! note + There may be more we can do on our end in the future to make the issue more obvious, but in the meantime it is safest to consistently prefix the methods you need to declare and call with `PlantSimEngine.`, or to explicitely import the functions you wish to extend, *e.g.*: `import PlantSimEngine: inputs_, outputs_`. ### MultiScaleModel : forgetting a kwarg in the declaration @@ -287,8 +356,51 @@ Stacktrace: The fix is to add Process2Model() -or an other model for the same process- to the mapping. -### Status kwargs ? -TODO +### Status API ambiguity + +One current problem with PlantSimEngine's API is that declaring a simulation's Status or Statuses differs between single- and multi-scale. + +Returning to the example in [Implementing a model: forgetting to import or prefix functions](@ref), the `ModelList` status was declared like this: + +```julia +model = ModelList( + ToyToyModel(1), + status = ( a = 1, b = 0, c = 0), +) +``` +If instead you replace `status = ...`with the multi-scale declaration: `Status(...)`, you will get the following error: + +```julia +ERROR: MethodError: no method matching process(::Status{(:a, :b, :c), Tuple{Base.RefValue{Int64}, Base.RefValue{Int64}, Base.RefValue{Int64}}}) +The function `process` exists, but no method is defined for this combination of argument types. + +Closest candidates are: + process(::Pair{Symbol, A}) where A<:AbstractModel + @ PlantSimEngine ~/path/to/pkg/PlantSimEngine/src/Abstract_model_structs.jl:16 + process(::A) where A<:AbstractModel + @ PlantSimEngine ~/path/to/pkg/PlantSimEngine/src/Abstract_model_structs.jl:13 + +Stacktrace: + [1] (::PlantSimEngine.var"#5#6")(i::Status{(:a, :b, :c), Tuple{Base.RefValue{…}, Base.RefValue{…}, Base.RefValue{…}}}) + @ PlantSimEngine ./none:0 + [2] iterate +``` + +If you do the opposite in a multi-scale simulation by replacing the necessary `Status(...)` with `status = ...`, you may get an `ERROR: syntax: invalid named tuple element` error. Here's some output when tinkering with the Toy Plant tutorial's mapping: + +```julia +ERROR: syntax: invalid named tuple element "MultiScaleModel(...)" around /path/to/Pkg/PlantSimEngine/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation3.jl:196 +Stacktrace: + [1] top-level scope + @ ~/path/to/pkg/PlantSimEngine/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation3.jl:196 +``` +or +```julia +ERROR: syntax: invalid named tuple element "ToyRootGrowthModel(50, 10)" around /path/to/Pkg/PlantSimEngine/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation3.jl:196 +Stacktrace: + [1] top-level scope + @ ~/path/to/Pkg/PlantSimEngine/examples/ToyMultiScalePlantTutorial/ToyPlantSimulation3.jl:196 +``` ## Forgetting to declare a scale in the mapping but having variables point to it diff --git a/docs/src/troubleshooting_and_testing/tips_and_workarounds.md b/docs/src/troubleshooting_and_testing/tips_and_workarounds.md index 289315a45..5c4063d18 100644 --- a/docs/src/troubleshooting_and_testing/tips_and_workarounds.md +++ b/docs/src/troubleshooting_and_testing/tips_and_workarounds.md @@ -35,13 +35,15 @@ We haven't found an approach that was fully satisfactory from both a code simpli There are two workarounds : -One awkward approach is to rename one of the variables. It is not ideal, of course, as it means you might not be able to use a predefined model 'out of the box', but it does not have any of the tradeoffs and constrained mentioned above. +- One possibly awkward approach is to rename one of the variables. It is not ideal, of course, as it means you might not be able to use a predefined model 'out of the box', but it does not have any of the tradeoffs and constraints mentioned above. -In many other situations one can work with what PlantSimEngine already provides. +- In many other situations one can work with what PlantSimEngine already provides. For example, one model in [XPalm.jl](https://github.com/PalmStudio/XPalm.jl/blob/main/src/plant/phytomer/leaves/leaf_pruning.jl) handles leaf pruning, affecting biomass. A straightforward implementation would be to have a `leaf_biomass` variable as both input and output. The workaround is to instead output a variable `leaf_biomass_pruning_loss` and to have that as input in the next timestep to compute the new leaf biomass. -TODO use toy plant as example +[Part 3](../multiscale/multiscale_example_3.md) of the Toy Plant tutorial does something similar for its carbon stock. The `carbon_stock` variable indicates how much carbon is available for root and internode growth, but instead of updating it and passing it along after the root growth decision model decided whether or not roots should be added, that model computes a `carbon_stock_updated_after_roots` which is then used by the internode growth model. + +This change in design avoids model order ambiguity and also improves readability, and makes sense in terms of PlantSimEngine's philosophy. ## [Multiscale : passing in a vector in a mapping status at a specific scale](@id multiscale_vector) diff --git a/docs/src/working_with_data/visualising_outputs.md b/docs/src/working_with_data/visualising_outputs.md index 4c11167fc..a26fb30c4 100644 --- a/docs/src/working_with_data/visualising_outputs.md +++ b/docs/src/working_with_data/visualising_outputs.md @@ -21,7 +21,6 @@ sim_outputs = run!(model, meteo_day) ``` # Visualizing outputs -TODO example environment ? ## Output structure @@ -71,6 +70,9 @@ TimeStepTable{Status{(:TT_cu, :LAI...}(365 x 3): And using CairoMakie, one can plot out selected variables : +!!! note + You will need to add CairoMakie to your environment through Pkg mode first. + ```julia # Plot the results: using CairoMakie diff --git a/docs/src/www/l1_flux-vs-tensorflow.png b/docs/src/www/l1_flux-vs-tensorflow.png new file mode 100644 index 0000000000000000000000000000000000000000..85787f4b905262fefab97021024c78d0fc4efb0a GIT binary patch literal 228317 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

7% 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


M3g23cKlAJ9-P;hD8BEbV*Sfp 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%0Dr z^ro4?p5=afha&mG2Wm*B2RtG6ORtTt9K!LSn0i+MIpiK@=&TC63I%Z-iIK|r? zC)SD@r#{&q9>ebP-5a$a6jJe?`SAwnY&;6woJu19y;5!ChcwEXnxC}t_~Mml!rH)? zK$Ic071|eBTsqTXH^@V>mXj-E444zzFMpxdobvXLWbhA9_owlo#ReMB+U&0WcUL|% z>=L3ZR0X)=Y}Q(ybd0brY*5+$F(5~(r8D}|QdU{&^FoCXV@M;uOoz`_vGV|9jXq;x zBOEm#*n^{{h39er^KW7JxqxK9IanFhd%B#TorcN0h~pDThpY#d;~-U^XGmpMdiq19 zyoB1-)m7}hwFDC)HRSd5#;`W&a8i|9YyZdP7(jwv0Vps{sMc}$0*Oi znO~RR)Ly`J7>zU;mM2@rq5DU6mN|47?bJsHOY+T|ZBz1n%SYY%~i6*JC1}kZ{JFFF}0i z5!;`DN4Hv0WmJR()m;M96E!Z@F`Z;b#Z6RYoT$=#nY7LB(quT#G8B&&;&`E%y z=+*)Nu4~)03=gF`0Qg~9v<7$xu-;w9V>`aFH=WM}5CPpndR7XLl}^la2mFSH;N8K( z*UGEk{J!sWsb_oYDmfNkIsW|_byIeJJAP+;MNKgGmKXEn&|8?HWHk;wlb@g0o~zz?ZeOwMTLpQkDS1Z4PN0*YETz&Kke7LO{>ypE?g`!CShq^zeJz%E@6kJ(ri4 ziJKfDgN8T_4`5&=KR0&_{{F266u^D<$3FnlWRF)Z-jofmJNO&wqBy?mc?#ZCb|8b} zw;hY?TI7*-C5SZx&SC$Zf}-NuP~Hfvs=3KFzn+jD^lx;*%-Twx0fL$diyJY?YIR~3^0vV(k zz6*q3N=qO7mTv>FIyP8H@lY86kcpt%m(LK;_a~eoKb!_<664CJd{p*ZXFCI`)8Jh$ z0UcHV|E&1T@R(oreR~3XLsfM1F|NLgkSltO57rqC`{gDoBASQG@fIm7gE=v7|NO$J zg8SBXJ?q8U*x1CNUFTpENDs(cj&?_kA=Cr5US{4i5e7H%MEeHn#ZOuf!(^OW2n9s9 zI&Ok*36j^b&a?~c%>J}(ZEXR1$5PzDUTrm>9`A#9qJ~cF>3oc8zLvG1rI(Wvmdc|Sxb3t%MR5_!sei-2Wf^gG$iKpN$akNp=K{3lF(n(GPj z_OQh)$wXSs$e~@_hV(;e;&WyFPVW(o`Xy7Jv7{)DV(LX;D1dr6gBZ=?pQ(cb_u!n? zOIbj^5K{3l@$#0nEib{=hTY+Fv_sIib=d)WKLJzHgu1Lx(J#gjv_qGgpx>EyxbdP0f`SZ z?+m*RL9XlrfkglNtFJ>=*0ue51s%0v8ktJfbKxSx`MSLDHcn2wJ&lme(!qmkW>VpE zR&_6U+XP`0Y{M}o@u8(UEa!9;(pWRn8#j!+H(P;>^Vw*^o1G9G3!SS1-a>ru&x?|h z5)#Y*t!M*zE0VlZdI!ofQQ0#ozDbd&w)BeLN#RR5EJ6%rmTK0owWmA27j%)`7j@Sp{2sPVX9panfB4>UG ziNns$&gEC?WfgwsXX_2eT)%I2DHe@L94)88IfntHb((>`P@pwlWZZ}~McHg;BoezC z5;(AO0>UpK#Gd-uUJdDdc?3xk*!7cryVh2vnkqi+vp<>aHl@4tJvp=QmGJB34LAwR zU%ni)0@hau&>du@!&W&Ech1lD8&mzxs@)0?_3r6gKwRhI!t9OP8`rsSezzQes8KgU zKV;?79!|d|8hNs1@)6z?HlYO4Xxi`m6zC-q3FX(qlW=UEOrM_#+`oVSB%{vw>N^|7 z|9hGu-qgOcJ~eHq;*W_pmErZS%T-?Tv{oC4OfauPlLT5jes}L?SlG8IYNU9hH|uc* z4luKy2Cd3n;tJd$i-FVrTGssPP+kk;<52{}H=tUyUz9+~WzrJNXSzJhDM%L%#$)+f z+m8TA)E%wnsfz7tUYu4I5Eedy2Idop2G)Gf`HMwaCGowF-YM9=LYaQ z(7F7^e;?7A9g{hiucQV;S~ZuGnQy7+asX`d0mI%90dQ47hh7Qhbq z5C|>!c8q8853xXaSy)msMpYa;faPM+&1)_0S@{IpzgU>(;Q(0b9{QWI`roIqAppML z{>Mf4yd6yr7D~Fm(7SQ)2aC|<>$YURI#nk6GWHSqI#pD%J1Mu3?ajvN;ZiCbAm(M& z*WZXpz54l%1dcqSEbQR{#S(fg5=Y3olJ1azXj9akXM&6GJDg=j2GtKva>hkmR(JYj z8F!%o;K;tVJW@0bYY+Pj7Fk4l;Jlmbh`PBuR{WgHsIKZo`}Mq`+=IKQQdkA!`u6qY zXG{x6;QxcgcQNd$!MZ7Cqu?ZQ3TZer_vVfS2dQSLE64GeKvayXH@1$;W4Len6IS4Q z)!g-Dm67LbW>kx?JpaKHiCf6a%LBw{SoFW4GX9JB7xob&n6UI3mXj?TJw@~~?P`IL z^7WWLE#8BK4U}_`kheM7?i1c>z@!}@6~DM37$IR;8BYhy$OynOhvorz5+hc_QAaVr zn5*aw0-p%*0JTTkxQOB07TY3w)HkQri#^FskB*tS9lGzzj4Nc*on7 zEDplSO_>%=R1vfSm{v+-n}gRneei?)4rW7mu9k6nZFdPY0x%(@<_gJ~<;GvxJ@Q(w zcY_56B&YR3IS>WY-rHR!Dx~&hqyKLLk`Vrc&%5#j5!xNEqbq|nT^;Y`1|;{Lt*?L# zTV#)dY){~nn#eV2QubeYl@Cic(e`2&s>5Di;6Ki~SQ`^X60sAaw zYH510-Q640o4{Alp3$BD|CVuWg!PDL!*`19&f?CM)%!7PMPkaN;}vB!OFuSHwcQL- z?KBpcOEYF=qi}#yERu1t#aKebb1@nzHfgDGMeDt|FVz8KY@{Z3OH82N0=s=ghZ8j7 zfD8zGQx>6<#R_1J3BwEFWDFQDHYBy=r$7y62A~QNZ!sWqIE`ytO>@?@f^vXc00z|@ zlDHQFKP`YJhFpsvixuniXNL`#8gZAi`k+AL1G8T$r+vNV_-_ged&1?IsHEAK=yaz_ z7*GGd)p_ZH8{>Y{Fb26|7M^FZHG3r~m8TiuNeE1J9)f^1hX-2C{Lu;_PmX=mr z5TJi=IL?Nwy;g1h{(Mg)w(~SDX17SE&@G_5PMWWB54y#0wM(3B zMgU?7)QBc71y)k#{M+TWgZJzy%RuDM@k>kMkHS-oLk@r$Evc&L{r`v{Gf4zoO!mM=E!dR)9;?1f1{Wkm(kbo%9 z%j6gVn>`AVSnw7WC4sM_!xYEabufE-A@C?yHiBW+z=I_aNIz03nlJMI4HLZvk~87N zQWGn4KUeU}c=+RnPGtwJy%GfZ(+K&4z1+l2^A@ktcn~j7_rlQum29fB?3Td5zz_z{ z+bnqu{5Cp}5n0jEbUIEzhR}E0B%^&!x53*ZtQAtYGW>V6eGr3N>ZBzBu?(n67*3<# zf19amGYjRrsm+Q>6!QkW(q)W(psaP>?*xr%9#m80|wqy!_lgzt*Ue1!+}*O zVg(*Pi}?19C9dUvGud7dSKa*SXi&iBFhZ+oD>9;Qoq*Fum$s@GO6_u>xTF?yjjT|P zuDvT&!Vk`Zas^2?l_aX1V~W+?BM8C4yz7TRIW#wpn&TQ)Im|)=%?E8K8K{j}zv=bT z!FT^D9RgWJqxj$&ujkG(He-{_WBCSSKw{X>CA{3Isane~Ar7YEb@wN$XbGBbRn=04 zRP?HI-@E4$R1rZ#zM66Gp89PJ2A2?*kVc9LQ(D_u^ZU1t1celSCx-2(b0gz=-=qky z1$$!Gzj)^!`t|M(Ky35#^EKgF>Les2koLHz<$mn$>4Ed6QRL=lg#`#AtAa_H=_HF2 z0dL>pUU(yN6LcG`f9*Oi0Au!_e{>Xk|B_|z6hDOC)c^48v-tI)^H73>lI*KzVY zzz?L`P5ifz7s{qt4fp+l)vyGNCFZpUMQAIqNCskonxRl5L7Q(LKuz$sxwB!r2F?>T zz+i<^U$d9ygquojyuIr{Sz@sP)nS?*sUn>WsN)|K`sMCY;d7E*?a39sipavB*z>aTLs($#|>8q32q(VBa)?9w9Hl~^;!|KD}jQKUM^;**mzg^m-rMYEhrt@5O>UDyhG>|cAY zF}`0aePGB^^xi07fX2hcL@1}sLS=YpQtR&LZUD4gW0FJ+qJj?|^qVmYDGxw0p1YeS zQsR5+iB#J>^cKiV)W@nUKxg4LOt1lJm-m*@Wp5!j?5%aPcaupGcF7fIw}LF?Ysn;` z6h0_E`jUMgu*$dn{8?4jc5!%S-bqM48uQU6a-rz{|5+*5T4)O3nk`7wbOLqUoNq;< zZi*TRWZCU~k$miaSS>__lb|^l=!LF@ED1oqvB$#a&m7vrHbl;gzd;fjLru#Pl?`+* zmo-{9Rt%a0^$H3@sTeY~^1!ul?!hbr1qJ`B-Y-W4IqyO6rKzG4j!NZzL<|GzEN#mF z*nPX53x62HO4pMtvShx#CCg5X54FR8-8)f7KY5g(nk?dM4aH=ca5`NgW|N5U_BoP3 zeeL%{30gt_-Ktc>|Au_ctOwrO@YkeMNxIpK5?>*jF>2D@z8u8T-#*G>ak-~d51+@d}S zInBQ_DsoG4V3#k2+5o1A;?Dy>B{iduO#xde3s3|1OiD!+my&|<24j_Ikx3V;R`_Ya zhpaB?U|#6%VRaMsW;h5bY3WIWFbZo2a35GX+cMSRg=&VfIWH0wg?dRyk1~W2PX=YK z0qQvqX~LMB9)Vw@o#F3j8;1;GF)7Gvi4S;*Ib`$hl?=GP*Wo0_rxB(Y0j`VoBdff^ z&u_uums)u8gRFun<%gOR2tRh=G7}iEUqSdsZV+T|$0guiM{YQPO+*JGq5S(!<=9PB zcXxx!nwF{QUmyeK=H{3K@xRhoNP|)b$|k8pc7fy&2S6QAt5gMYN&qT@s?9#ItGVnE3CB>D%)#1wLkZ#>Zo-ngsr0X1Wi(9VBud8_X3a-VS^!BNMDw zAcG&px&qH`ZEd{w(X(&G*{^!0PT5tHyMO;4T-o8THDrl;_vBk~aj)9VU_xrqVv3k` z5RYw?2nL(oqp)7OZhN!&t#vVJu*r2s37`A3dTJ$OuO4WS-66w2@%Dk1oeAE|vNA&5 z#UHbzBcw6+wDJtt(Bpaf$Z%Uj28@bT+t8Xo7lq>Za<&S3laC?TqScObGUFA6MXL-?^z`&hO~0W`qN1V%*_REloBH@R z0)cM=Ri(t&Xj=Jb;7tHTHKIiH!v%^H35%?4Km`$2Tt|;^I6ZQ4vpc&(_JM7zx6v$j zZJ#%IC}QW|4*Iw-=}fl;VY1H8>FJ^~ z_9n2vB+nB-OE@(@2ZzB2*RB#=DB%o|M*nf<5jq6|0$BDUaFa7@H-bew8NKUV1nlWsCq6Hk`g6>5@?#-sg; z*x9xZ>wf1?fInx(WrF>kkN@ob&Nx-`Cl^66C(Fx=-RG#qSsetveUbj~lqHwqo z%vN@|ZSO1QgNbf>^-w=w{r#)sDQQ&+@s)V?($F_XXQIPPW)NLfnx|q3z#}zwc z0@^Czm$FgNVM0x0IwmCrDrneGLV{lC+I=Z}pnSpQSt_X6Wu+^P7N`|qd_bx-=N&g> z1qUr!+t}w>Rud@JhD;qd#pk!N9kK-jJPB{08*p)Hsg~|C^FIJk8a<;Rzf)wr0j9Oy zf5K=5K+yg4cK*O~2*5NZ9wMmHks@OY-Qfi}m>3M{-;mE^zw>9K>b^4>ithjShDPb?xg9v;umT{#5uZ@%A+454^e{B_&AKTAV** zo`!aP%M>I<648E%c=`2F5NLnSe|$yj593|B%&+u?_`S;=oIBOVek(O8gG-S2Crz!Qe z_m#r!=udJ#*D5jQIy>FgJrZ>$CM8GXsb_cZ3>|xVb;SucS(P(L_;3jnZ5$h)pZWA% zD9PT+d;86_D^+o1!^6OMdgI*ZAoyvodV?YO*kemdFs-&|yTk0@J5xWa^U#W^!`)hyqoiR={^09{dj>EMvbUx}?u@+o?){$qZU+mc_wojaSFcfMzMetmkI zf`9;D(Zw@#C^r)I_b>O$g?X-{lbejGA7UPy8&oVV=eU2C8g5jL_<@+-rYw5-a)DE~ zfnTuK?`dPoGkV9)59PW-)#^D1UD~N@gI|@)jrn21q4P*TDdvaGh415|N^<_0y`=@d zPp?@ns{6b4eQCkb>vEx24RGKj2er1+!1qZzSP)S%viPGm{IO=X{Q9GF#8RQHQUl-T zBrFpubYRj12nfe)`0I^~A>irh!Ei)WsmrHKIsjah#@=5wwbaL+Ql_h0Ldg$Qs7V4& zCF5ww9`7f1xoDSwXA#hAhI=uaySwyap0DX!du;&j%Vs&(UweQm?d$7PuE%?wr&aLw z?OTKHFgbbooL1Tw&m2A4vmY@9+D%l=ZXvHSr)%Cd3(F+EA8gWHO1>4CA=dJXd8$m3h zx#UvW-!-$etTpd`HsA^%rl6o$SeQRJI!2i}UVz37}8>y{mMe@_;0Qs@(53M!(BAi zY_Q0~Ql20%)Gkpg%Xkwm!VYX|= z?S88=f`bzp15w(KnGq7k>AX~APVrOE=_EXtVctZY*fZkh-~T4=iR>kdI*b3977$@y zuZ69678>!(Z{YXuZZQToEmksD%-|X7(EcK#?HcV41}3FyjaN+KfQid+bNNo7NNHT;KBj}2Lsi@iVaRDE`^u%+JK z2E@-=)!n;mn}1Iv4BrwDKdAsKFW|MX6_vtJ2M%|jw;{7wj=R4aD3c}xLVFsv?U71O$+olPR1- z|A6SL;pJpPaKC+PqT|C8udLeRa~E@xqxCh4xVbh*4EOHK54y4B1g#`r3sjONcIT7W zN6xE_*ev}M-v&K-Qq*vk`IKdMXGao$etuE@R`emGACK!!M{zMvG%J^3L#YR9@7a05 zeX1QeFHE9H1d0qjT`-A>BSksquO3~TlvP!#sbBAP^pjw;m0!tc&-$Ij-|+8_Dv{Y^ z9k~$|^rFw^#S1@7%g|Gd$HPf?6MtwHn-92NxS=I-@3Vrhp5XMh(d9blc5qz(?r|Pt z&tbCfL};&&YX?9cdZ?V?+TwNaXu%|7c* z(65_{hfhe`58=p&Pn2F6%J%UhZg!wYb#UN8e96^yhB~!!8x$dmbjpu5np%9%d@tZkbKgE$ zVr;+;F#Dbplk4)u8QeQg+CS92WvN!m9ie3lr&oKb>Afp+1M%f=)@P2y%5`PMo~`}u z;T6K|DW3wrbvrZFJM*RC=x@lvv0~SO>k`JPp~#~46hU5_r_-f4OWa0K8Z?dCc;7ik z7lC_l{EQHVAB>~uIW+M~&#d99^5Hm&-Wq;+#w zCg48A^8lr~vCk!)TLeUQzFdbo3NQNgKCqpt ztHw0zl$nQhnLCTia3KYPsq@2(!4g5lekD6Qn{D#Pjl+tolnK`z??<6Y;gJIjiwJFwMg3qMoEM}tiUDi)Z^u~h*goNaAl0% zf{je<VKl#pmwh&{?7YHHt-59os*Hlo`^fj6duMa^<tbSa@3jh8J}EqTF8L!y zRY~N#{SqEy12Wb4dF8l=sJQCw^S^8M&@Fe2_v=^Z@yWPx=pYSftV;jU7Kt7op~}b} z#P{i-J!XTRtQsD_fEf2-Xl%(`^$f>^@ZZ0;W) zd*B^S8&_7iJdTrN>a|jI-nwgf^KdPdW9#Y_90a%6c2$vE>t#NGUvbhgJ;9+;_*k&8`yEA8sy9fan@{gpot+jBng6ly(dG;Ucx@Pvn(+_U3X8J)gcB8ds zza#Iz2oBFFlEXpFEEddn=~sFk;}yRfS|hx`;Mdb_8x{3g^X^9mKGVbgTN+tCpX6-l zuH*6S_*oAVGV=f2`JP_{bK3|CV=My=(-{<~->D4&quD0vB6X8^Ze2GzdE~+@~r%|J{$gb^P2T5Kg>9KkUgRKC=)Yk z&_GjvH><}8+Lk=)zeeaJu)5nnCBf_Q35WK(HE)x?7k9Ygf6f2>yDv?K>-0E6LDqMB=%{sEZ_3NO?2^>&&+ncrTq%Q)tOG+`w8j~&E<4P- zPt2xeF|l6WL5o}ck=&pA$#c!xQqQxQ9zW3k^)V|GMZ9D4DObW50$TLqx17ik_1KG6 zn(68*Py#<3)KK;Ck)b;XNBN}>bR)&QG$tH zzfEn+f#Zwd>(t-v_xV3?U;NQ<{-deCU%h>d3aE#X>9Y<6)yw`D{S6B>fobjO>N>TX z)kZKO@DYC=UBrX`Wqg4C7BtP(ZykzgbyEs`xboUstH|(hdev~e5*H5-M^@nEZuA@E zjl#O!y<b4`4_+Wt+r#i17FDan`SDGrV& z>#J3WSsx(+p9V?1K&y#}v6Ho{unT@BQP@hU2l$nMh;hP#ZzROJdruWrtE3x<9Vf&II74m1 z9J^{RKZ?6&XFiT|0kPjuQL~@A@6^tT^2`w}Vl@(T-c*{#_%M!Ds4xYF z|74hR@ZgkUD;q_PA0I9muofG|pMIPAvcI*Hj(*s}GZuYAwwYdvijGcGT@NA;??Aui zFNN5@(42PGL;~4=UxEy4u8zs+%6Y?qX;apN&-QYUtzq`S`0nL2&7Apd54?SJ%JFb* z6bT7S&fxun*>waZw;=<+)5i)Oda4^z(Sdqtpy4ByWQbT_Us6<~OE%gcAe!#Kb_yUp z*Q@ZCKyCJ~_qkd zr{ro3V;zvF3`i6Fi?ihneZA##!oOPPJZ2@EIzNhu`nd|jz(*=G(5tL592+;=`*K|ED++#I|U^G#n?h?t+(^W0odj^^ERy}3F4u=sHGoaJo~d!m2ibF*2wLk))ZJmxPK9ppaTh(>=~m(|Z7 zw)(CY`{c&^j&{^Q=cQaZRs?Z&`7Pn(sg^m6Y)EGM<1B%#Y6F~9ZQqR|zXDBb1tuJ0 z4_|YX?iTd-=9}wzHz%|E<1VDD&*4BOuOFWoi=ZaNgyTMJwfBFLt(UKsCKCoCQLs@8 z%L>@T8oe-z+wfGm_dpHUpvr-nEt5ab7c@f9nrV1_4Mc&ZrS@4mm%yF)-_&D9W8>+? zD2M%xjYv6GE>et4T1k_(^($gv61oCwk(8LN_a-`%H4Y1Pt>%t9j|(5zK0B12J}8WT^wJx4*k%=qOV&J zdvfyE?!F;t)tOjJ%gVro{w5i|nfje4!DoBh_A%@#RK1O{s#{0*YXA1IOCrqFI|EBT z=W87mpER51b;(43lH%Il(R{|IbbH3j%foz$y!hR{;a9&TFkjW31P3!X1b#7a-Wi&# za|vORlVDum(tgxFjiLPy3WUp`ZbKm6GFdr<{akS3qv?Y3!Ro!) zl(@L`XL-^sEjqCowiRzK9#v1goJC%>a^>_hX7ro>{UTK2A zh8)3!`PbSatD0f{@^kE}4(?CWuL{iTgAH9(QPOpHy;AWGj^96N2Jd#-N;{&BDzA$( zQq6p|lZy*0q!c#{_?_09(%My1%}7L?di|L%dTK8%m{J9oKMuRaIP&z%QzfTB*B_EI zGP*2MN=90fctd2pu9>%cx=J{o%QbvgX=x&sf+z|Tr$Fo)@&0|39BW}^jx4AGzkdA+ zT!*u`Qqc(X*A%#tVgSa})iTCq{Ru@;lgeTOROL8G43@#t=gtn@4 ztTpoq0d7dP?-ON+=xMKS<++xpFYgxZPG`g?9@clNg2Bi6DjE5jJ*V)k*E=nJ_1pnC z^$`9;&zJi%G{W3m)-?`{MjHlbMio-^577IOE##q3h~NH8stczd^ATQ8o2vKRI;@)? zck?}2s60Q@mv+4URnook=}J>QouK={;HEUfK|Ll0@qE$m=n!wc-f;Md&tYTU)baC_ zR#TEI2Z%59B5q;|&B8#vmIHQqPTiOfrYe_2D#lVnv8V ztx!8R-aM(fSIcG4aGjY-!t>RB;qDkyh6!_Ee}DgD7PYRsH|VZrXQrnQdOm;FM|+JG zz<7R_IkMv=j1$bMlonw^iPQzdZS z=`*ZOdCjyppIDZPo=j5=65ClG>l|-M`%>kO&P*ox+H~eC-mcQh7$I_%$%dX9gLV;+ zO$8sxo?nE-k`nesLfoRyXt}Y0B_3NwzK&FPzYu$=w?G_`UqQul?Uje}#k-O+7ZX#_ zi_b4OQGGoQ^D4H2mgPwhhd^J(1PIgHM*H3}y#oSl8hVXAZrTL)LhmGWk!b2!yS;)Y zJ&aSerm8Bo3uJhhBvAkwZ%PxZrOP@Cw+_8Q^x*{buWdXz1}L~>ld$bHf|g=)8I5lZ z$(}su6&#$|(zAe$Z{O z7YEY5#N)oTu!midteta?RVwGbR}U}v>qIl{9O1E9htu_+KmYeWlwiaaE#fW0H)&or zEw4PX5+;qehnZfo(`XmTq2?H|yM<51aO2=4e`P^nWQFh=>UM(So8}f`DbTP-IN{9v zk-ir^Zvf5#a~FTwRiYLiWPtm%y6qdp#PN`m02=sqk_*I=_Cyg2c>=)22BGFYw4Q9h z5ov53Gx{kyHa<~*W^TdqzP|X`$+5k}d2xGyo6r1%&O!MX4aXjx=)s)`iSsGcz)hiB z1?i93XSOY3^=}{*@>k)8hilgTT5=1?6;2L>~9 z9hbj_^R0{`j-`*&2nep4>86j+ipTGNU6RhTI{_LSfV%)qSa_=@$8w)ormULK%?<1e7)Z^$E2&v`vX(TK` zF68zRss|r4#9~n6?K2r`co@`*V5(LUekr+5@Zzp+*Nr@j)>gO1tYS{o()O*t!N(WY z_KIHTs1PBbN}x>|qu}ECmPw@2p7+Jr3-tJ74Kf16Q)@-nb&*o@0hJQD+a#gBM?Rx} z87ZX1B#B~h0HOa{VF1!6R~j2gI*@&Ol4roDGZJKpiM`xn$+PZB~(^6nfsDW1iC zTGq;+e0{oTn>f@q`&6x?7}Qwv5C;LPw1AB znAM6jMr`b{vu@Q~%gT_%KRh~uLVE>d>+o+ve0==t*Iny%=e}s(g_@7&!G_5aX?>UZ zHYkj&IA$A6S}r5Q=7OYVV^k}?4COII0d2%Ss}lXPtU9D=X?xP+-aX#g>k}Mk7!~R+ zy7U4>Fbv5$I@`F^xc+9kCB+n|--a`r?g&YL;y_|3+td@hEUb#j&O7*;93TJ0K@guA z{5q&dt#+~%JimCkXM+`ajn%H+*Qd)WewG1HBBN1ei-BJf?@@9aQM_*re<6J!r&uTV z_}l#!JnCyQ;;C69v{ya=kJcHM*r{DK4R1lCEyDXOXQgoe0s`5>jDLY?#``WSG*D6_ zm@a&t5Z&9_Iq;Zex8OGD{MC62pM{gPSmXHENAAq9c40M)^(||r3YEql43N#D^gh07 zn?)iO$wYjClFJa)#o@gh6h9qy2g>KzthLZdw+$K$C;chB3rWzKGBk*(h!?~w2D zu65oCINEP;I?Wt1eRlh0eXScgK4yyXSH~~i22WB1)9qfrZbi4m&QG6Kr{d3tb`v{s zaR}siI2Gz3&(Ql@H0`ZrndJ`Kk0QI3eS#o*V=F)^Dv`kLWjW8C_eoqntHOmc;m;^p zDjUfo8N{F7EkZdY@cQ|k1FVJ*sE2?84wCw+o}-z|=0T6`(*i!U#=wbJNa(OTSp+NR zau);-NKla>jlV%1hM1Um<6XOzklyn-vS`-H90z;LtPIEm9S_Ht1G_`SexhTbDI{j=Rz?P7b_h-i z#i!4|)P2x9ey2r}-*pAywDrEhd@{JIiZ^FvMM$XnazK=sZsMfOI2wWACL*FJ0bl1E zav6Usmqd;CQe8!oW24Gl_Rqi~(DC+a(SmiC`Iw+hTIE$oG|@dQ(3%8=({x4UOl*FY;;wqX1)hu5>4$bXX$U_A}le*ns&dpw=I>0D9pv_Aw75Gf$8u!q8(@H9n<@--%%)3)j z+J1XYv>2HPsAA;L28uj#$3uLj%Dy^DqdLAqi~KPt9G)BXf{JZ7^SI?JQh=<=$f-EP z(ctk7TxN<;A-ZtTNuw5k{m=FC_VIyIIn=BaGE7UVs2?tathTbK$X-t`VcQx5{iO6F zuGqfO2=^-JGNK?S7m|p5dYcYv?Kc^+pD^QB=JxBvx|D4z&4myJBdf4B$2OhV&z-pjva(z;7p{*uew&eQ7bf-FE3dXUu5l+Q&X;5fCLXhKO)x>2 z;)H%;V8DG;d`-Wi(Asv^A-m&UFVVF36SgRuNe?F0+t1euFP?h2t@jp|ZxcR|!jV@4 z)^qCl!}yHoyX*p=R(Eh&U$t>L)sBtXRi1x$k5A657R8J^r*NPa8O5lLbPnt5|gr@hvS)z<$c0>>F-EriZ9eBL8lsQ;rf_2Kr$j zE)s?6>IA*P@?eQ{J^g9Q>aN4mwmT>40vditNhmx=|G6U7V+#1F)4)QGPHi;k2;`TlK6BCRfa%ZJleUy-`pQ2@w&byCg)qLl9ItM7lw`q(kXW0cjDDZt0W|5b17^lJ2f|a_&Fw zefRO3N7%5}Z>_oJ9OD^J))WB&wA7uv<}xWp7epZyMTQEM;juP!sF(LZ9Ff}NnE9o= z1H?Vxf>tlq^W9VCBJksJJ%nCsxDyUAmyFrslfrlQ_pb-G=|mYBb9*OLQq@UZ@3Boe z(NqA4)y-QBBEhbjdtD*I;NJN(Xjw5L3nY&+gE~EzvS3q3#>LpoUu$VS)cbOnYaV*& zqFMBP-@{C%8jP<_(HV=$&jkTwz?2pe?sz0jz0a9{X@#+%Pc#Xo6UhKGyi;*xcK zanVBYB(-NA9r?uh{3xlaVj3 zR?W|5nCmXdDw6A(vwme1+_K?o@j7oT>0{Cj@}wv4+}7H+OWN*7TT&uKy>Ae~Jm!6P zg>lcPZk{R>81%T*i733AZlGUi1$=dGi1CcIS{DO(f)P%tD0j@=ch*;Dz8OnURm3*F4#)VH1dXK>N>l+ z0%|KiE!~9en@n=`wRCc&GbAJg&JoNM`o42vT2?Y8(XgpaocgC*jw~vnO z*xhF8^zE$Wun`Dw$J^R(-v}sHW31|B!w=<67NNosJhxe2Nz=P`1F`KiIa%u#JoKy3 zd@gkV_bclL@zI+#C#G|bywbX=2H!gj^zaWmQBuS31=+c|B^e^QPgAb82MS5u*o7Kr zutel)9&8$t`Q%eJZ+-o0Q$-Y7-m-W+ix85H0Z~dBbSlSAbjd|xFgrV0+8ODnq!bwL z`TJJKNYuJ<*z6s;Q%RMFT5_xf-X5mzCk%m<@5Y&mbMFsG9z>XJ25f70yO`#fqdR_v z5bOaq1y+o@Hsqy%EdU10r{+>4>8=$Z&M`DH>bTxai>z1??>opT4*~kPw`}+H-QIG5beRmLSk0#}d|-qe9244b|+>4H(Vp!$TDL_!u@FT$z6mnmD_B6m?Rw;Y5<~0)zolq^5-J> zUrfNbWocP(_6`kM1QY|%ajf;cI!&~>frxlP4Z=nSWiwQ-h|a~4LJjTOV;0(Zm}Y^< zaH*;33$TKcLiiMZ$HTZbgtK5yNkH?pDi zI)B1t<5U_Pn&E`<9`riUHZrOhTNg!2ZLYg=rkLHUB*H}cFlL^8xz-3H9zSS@BM>0t zPs@wyZ2Q&s{i1&bmWRBUg!jd{%~4zQT%S*cI`N+(JUy`eX)uqR;DhD({{7O@WK9lU z@Wj218><5+gSf=-%(zLHcY4!QawMqWyVA2DE2GM8?|sj5#cv4njUfaaUXF!JAA6RAeMD=fFcCP6xBzc= zaoQU)8{R{?p)}T%Q|EGs9n|Vj6DnqdEjr_N)ZCoNa=Np;_PVg+s{%sUx4{uS?uA^| z+(fsYAdBp;jnp)ivrBIYIjJ(l#3mZc!LIx>xz1^K{{%um;dJ67h>waTU)Tf1whcGGl&c| z2rfZ*oi|GnOe&0>)5*HCGNaPy;cX!2OOd2vc>LHt-p(kRu=*h zqKRFYi0%D@OW63%u!wT(4Rm$;2(9WFaf9pX1Ws-Uc0`AGny*umHqg9|dXmhiYQYIL z?Cw5K@(;P8qUJW4H^1?Mhn5yIcK59#PbFW-Xx2E>QgsYR>h}Epq%ijeQ{1MKsC4lI z&u5jyP+fdH1W5xyedzV&;eQUFJHdWzzGw6D@+lYNC)L53PxM~~Sr~^^^5Ks;&n5qu zxl5$rrGfpa zUpMQtu6{heK;0;ZU{E_Qi+J7UTTzlKQHA?wj3^HnW}YunXRN$ZIJu2PyU`J zZ&`GwArYqWPQS7;#zfVTb<+?bPIiEp4X@y6qrlG6Mni1jSn(0|g$~EhA`y4*o97k| zi85Bl`fsTh_tYKM8$6!Ap~H=!;;~t0jY|VzuHkZuH@4R&X5y4j8d7IgFk(z=PpZ9b zq`S2AVDxQGM4MygW+gp%qlbW_MiwavGY<$nTHT!}ZgBzl6?my2IvY30)J>p}$@?}& z(!u%w4*@+g6_wtT;NECip|km(2IWs2CHf;N=#=bMtV$=~vK%9G~Q) z4qg01|Cy^|H4A>`WzaNw9{x#jqZlZxf6X3sj4n zoVCppzJ(L>9qtcs#81{i<2EV^XIcN_i^VMYbkm|eJedpyDOiPUnSlI!+!ek9aLfibn#V!L9SoUJaO16t8G3}5o1IUqtQCQ!;O1e z^gZ_Hw>3jXLmy2L2t^6m%Y_ZuI)Pe3hCv&BiH75T^sC2jh=`^WI_GcsBNwXG9nQ{; zY6;Xl7Z+Ecdbe?UFtB4B9313*<0gGjiR$Bw)ml=IOAh|W-_Jcxk5)=I8#u6UAkxAM zd!S$Ga5(7;GazZ2y|KMzmXroMoM6c)688CA8NQ_Ik|riuad9Kxy3}93tM0h}a}VVl zvOqxj(VEIjezZ-4`e%B*dG(@-3YGiqWPA8SH&Q$te9z0>+Mb63JO{<)eMhwP&@qMX zN{MABbCpt*w%lNq*o2^f{;!le+lwXFy@#O1C>=^jiQoIVZu~1HO0q@&Ip)dRo&Mg4 z&-%3U$}d_=pJwiE#l&w3V%LDk%U2^dI#I}6uHkQ%O7}c_{O<;)laX##P;ceGPRX@@ zrIcGVI&ZAd&(^J7J)ZyaHsZaO6~7(x2i?(GGS8~-E}`9t5zu$L1O&ykRTLa%l2IQ! z41i)yCDm5F218q;(zj)5fHMc*HP|Kdkw#Qc=W47DH~2m!Bs{_+*_=!3Upd&SJ^KN| z7J$Lild%eC4p38R@DO{(Hm7;rxQT?Qs&W?1ww-%@l^g0EHx-$4%oi1>eSFxa{mpQx zm)(8iXMWFeoigW)C>2h366a;ExTg>DNpb++)#8_#)Wj!4Jx9~Vk3{*7NKaf_d681B z*yG*YYk!6k{oxNV%;lP{PX1N$H=ZDR;+b;4M7D)JW2B^aShZluh zzYL<_lI_p+0}lHS)*#`q%uLz$z2E*tPMtLw|D zTy@CsGRFVe<+m03MoTND6GeyI2SdSs2>v^v@vI>{haI>5#P#gn%}!TH-Cve6Q< z(ett!Bq~vnA)@RlKx5L|I#Lf3`e0Qw}6j>X?(q?rRVlW;u zF$wr#GFh|LSiZrBB+M_UO%nrZ88Or$Q|WBptvW10kF(Gb#&v_!@AN{KyJyLK^l527 zaNl2EdO+il*JBq~JW7K^mhN6S2?qrS2NM^|)@I%I?fYA@T~2yuVVde(`RJ&j3vx+~ zjqgD>k3m>L=Hmmh3_Wn_Tb(&rj95)l;RKR)$2D*oAEsT@0Ax&qdGGL0(0aDF+ky`b z)8Ejfv1Cr(qDfQ?$7=eEsSBaU-}lq6X~Q)uoQ5xok?`bq&#Mf2+Bp(ypib69rQR>&jOfN@mR}5>LS& zJtMC3!&6*QQLZiae{8u&EUBPGQKoqix4MsXS#PK1^2;zHIPNkoB?cW7y8{DWHyF3F zc(SZ|$`T!5WZnAmDsivr)d%4sqvpdVJq{6UXnE$0b-ZPOi#K?4oq*;8a6c+q+PE$< zDpXNmi74d&(KGZBM-H&RFbFeWWK7n$o>c}a!aUKtxtU52TI#P}c2;^^&{$N6MPZ{N zPiv*XL!~}J! zP}`X;Pm7WNK%$jNu1WbsG?49X9nmnYQ|ThGa@u780>T0Nx7i-k4-1{Mk~B-S>LS0} ztXZOp_?PD!3&@}7y%y9;(d7L;<#>F&Qu^?9{>*T$`m#?h7g(2!-js_?{8gDF54XJq z?ePSRB+7t5sS8~U4ZvjcM~cPRI^8M&9Y zH~9Ymb0oDaP>+s|?zl0oJmupG?!3;WEq)R!z$*=nGcA$Dby9C0h}XHFIqa!YJ*A{< zHhk)Wg@whX9?vBtC@8r3`x?dwkJUd9gR1j|%l^hldf~Tcc6*0izms0Y8lv5}b$m;r zBIr<#Gq##QFtx#OKEZJs>vYFo8=<#I!cZ)^zP9Eap?^=wpmYp9I4Ferk?hQSQjgQc z4KBtNi+s8EIOaF}mQ5cA7pSWIxx(9G0;t_cw5?b_y$GK6*lK=l+<-rjZWZ{=r+I$7 zD+6ohrB=o13@!O&i4D__Xh~`uO#dj!R=i;4Oy($hf!K?S=jUT~!JcOwbSi3h6aMI> z?s^iD67spsIR*>5Tv5KWDb`4fc#)B(YEY~i;=5_I=Mx{n`6288OsCn(1nLroLi9266caDx$KDt-774hCBIR`k$^hS8chMZ0`;|19`KHQTIWM0bVghnj94lE?%}W!{SJ4{aUtd-=0fb zO7m6z%+Ezzf6Vo#HtPX7ekW%qwX9~RM-Auhv`q8!EO<6NM0Gz*O2^wm@Y(HkQXeJ7 z(qw8kz|uZoWM;0nTb9w)B^M3&LI(|b=)-fp}equ`gKoo!L|xe zX>Lx@|DTYvhMW!vJ-2nrnPcBLw)~Nx|*V1@u#lXpoSE|=@cp{yoY8X zM$s?GU;i?EaO=10kuphKse6w|ny?OQ0w#&T+hR2-FL-F6=TelVnxWt(xV4e;BsmE= zLR)Ts<+TuAc9|B*FGnrUA03lj4~+#p1fK9Pa#@Y#U?e)O53C%h1WRWq)$aeE=WsuO zc2O6GOyvxUuO8bMrsz$}CgM?xL&Y=a@GhU)TZvmLJrVVT4m#-8HS|x@V1C6n@>Wvy zzIfDvez<@GT_LoK-@CrOiacCM!D%oL2}|*%P((inEADx zYcEp*84IM}l7kC1k${Ef^Gv^i0JrKR>rK06+oyA3*iC4T;IIu+C|DoJmrb<{XnbS`WF!+WCQsFQ_%CP&#x+4b*CVb9n=Tf_P3 z1Rxn#AbVu0035m$O(||}r;{UEeJ1S0eBX`nWB3nDE9=yfd( z>^$5J3PSVxjM`*W8tl^)DRw5rr=L(5p1IeUr=ouD>r;?C(UH+##nNLd=+r-YkDu8Q zc1T~}*1Mufg9=C$n~<8*P^Xn{&>`IdYNG)KmhT1?{QVaLF9jnDaSvpWO6b zHx8$C4@{hopM0ghE;F4KYwO%%L^JVcN|X5kLbxn6jbeOQlV5cJg`9|0=RJ%*R8(U3 zA_0R35^#VF*xL2Lumiv><$F(X#RY(X*7do&itHsNK-(NX&Ifq+b3ekFxMClg>45=)kK&b?LfI=1cd*zLXf{N~z1AsvQ$-Y} z#BgwUxH+Do=!@)_q4r$7K&vWGHNU&Ft5h|AT1yMQbXX?toyB6x+Zvm5J8u<9QaQ!* z`+$;xj_if-6!NNc?bzS{O(5HoG-ohD?tL2*Y_OGphR>zPZ0i|$PKM2$;^Hu@*wbx0 z71NplZ3T-j6tDoaMAD{pz7KqtB$K0mAX9As-B2!V=W_ccrTDMEw7g)Wh*>t~7j$0F zpU8GeM?$m&XX>NB^qH9W-9r;sh^OLz%w7@`oN~#ry^8)cnV(Pp)CX`*nZk#G z&o1jR%5iRiKW*OU{^pTQH5L_rEkVWo&33)=&6(dDTsT3Ao~MYFm%k@2?HX#67ZDyx z6|st!;mUEUkFRl4fY(oY_#*MuH4#-BDA-+p;3gLahzo8~%IFU8YO92*7T<;vRUX%{ zVj>v}TF*2z4$uXcnwgN4mIlbH9Zwz%+L_8T?)tE@vF*Yrm6=umJ|1X=*TDVn;6Ch| zs`nRRtm)L-_8@G){0t;@4m(bh5ue|iTKu7! z(2^jJJ6KnVUa8v59Pf;vBEb8T=Y?;^4~UWUFBF>Lxvmsxy`W9f{Q-YCMjN7)Je1vP z1>9>jUm;x==kYo2HoZxaRGD_Nh+G?)Pv6ytEv|;&2!{q-*w$W5K7n2w(p=HaOB|IVT->(jh zanK@*0Ilz5vh&@$%^LM?V~ZysNBNQ$HChb~C3`l{!YY`ueOzpXz z<*~sDH!(4hOd|w}PN~=I#s&sdv^AH1VMS=Z2_KK82b1`s`2&9#pRBL1SBL!vF9Jsz zwoK*gmPFQQC8a_P0=t#37F(yhL76|s3X@CCHRbcAE2nNN=8q4_^Q)GMU_XN*WAe?1 zJ($T|6F~%N05wEU-(jfb0XQf(K`pged0&73nrLg|H;14hyW2b{Ci#vWUewgBi$$TQji=-kS>|R`an-(&SHa zj`AG19~1R2_sryyPu-iE69*ihP5Jt8(6Ysw0}}{fXG5#^X=S)c?ZBW8{LW?lXB2$^ z?n>ByloI+3ei2hE-aRGju=F%!_)L=U{Q|?o9hWUd0DPt9)Raus0RXJZfuB?6oB;l< zU77ZVW&Ll8Nt|_49nNH*B5Gi*T4GV_N$jE!nb`eJm2*}D&POBXEel3UW{fh0X-a03 zB{%J$3kodYBtC-e&N!Ckg$1R9$IYx@CxwibZbyh;z~)K}*YAeR2@Y=V(d0;%x=TO; zf}>Rvz$TTpi$R^bmzS6D;T~Yi>Pti!0Lo-a3g*AS$b1JhplZzP+3UalJh}s)o|pMZ z%!s?(ySR7h(S?7nj_<^WzQa+x5X*bFb?Yqf=Fm9mDYEr#A(iL%bh{LsoGSUGhmsv* zssLzq?fp*={M_RrJIk3u2GzL^?ErVAA_Y7&PPe$ckM!lr&rV8=igWn zKV6I)w3Cd_olbVz1krR3y0Ls3D_y-Ux`aLla%Ghz^O&E`znqmgG35!EsAlxcTKi4! z7qe_40Rjfiuv*|z0^5hgW73=xIGlZpOYkNv=b(CuJJU2=cQ}JRUA6oXx;}d){PHv# zJf;>|Nl8L3kQ&_s#QS57%VXoT{`9afO$kq`Gz1}3Ag&o;whS8#cJReP zr^!778jEY<=uvdGxNQ|b4b9K4@+pWZf%vKXwFuLfP#|3IW){{dPJhrG@xm?8$b0~O zu0@*VDj;aWi3IO1%=u2e*7SRP$z5R3bkA?E{MZ{X*Fj*@>Xm^8hfM;v4aFv{;uXv)|5YRV(z0@ZLfU9$_4Bd;3=sHj{R4&&hmIo{;+>?Yuf#2SwNBiP96+&u*K_)Ds&aWq_4Xs>w#DrjLz2dvxxp{ZKG z8jw?s)dU`zhDH(}5Wh(#!Dm*DncXqB;{=>mGO7NqoqxHm5}p-8ZoL+(MZnw&rbDoZJx|4v-*`5X35uQ4=C_#Pi1FioxWlI zX;LGyvf2XML={nNd^`++rP>+_wdxYUYv{%=f(??B=FsvrRG)1i#Mw2pUjXO(I&^3) z%XA4o)~oY9*i}y4F40+P$?$^Lc)xzjf+YadBOA|iP&EQxA-p>Hf1RhR1n^FMx!mq} z!Zmg}iQ`d7@WW46YLc{?1$CcE9;hc-jn!&Y=x|0mVU#@1vTVS3wynn-q?M#d`$Ly0 z9HvV=wqN-|wPF7?E>=Y#fTs_%>h9Z+3+LnKH!KsNYIvPZ3X~|W+mt@niCu6k@M2`& zg0vSvQa>*pTkoEQc5u0c`h|)<(@m+U9-*t+#AFaeuuB)dDgD`1fAasW(HdkHci{tk zrM-=NMMR?p$GaRZ6TDxU^Xe*1@$Qx+3+kQQq&P3i_G+b9yP`%oqFz;DrTxw=_ik1z z0Ny!NOjShJPcQt^ZXh&bD{dlyPUuHyF;WJ|kD-fi0;p-g-*o}P<-11;Wd_E^o&b4; z))Dh_3LnT*0!$f97*@%PXc&N&l}t@QMCL;iy#nY+fUY|xOZ@Lm=_c*ul!U^cD)g)@ z%T+f`CiXYVJpNOhsw6=Xv_x= z(fEo!0diGbGY~IrZEZzGn;@hsIUM@t;D|u}AoO=t1?-IgJO6uG@?9nO0O$k!_J8G) zB^JLy)dYm~W#E%--u*^r3XJQ`Nj%dI$tX7f3-a^xPtH6m9PtFSC4hUgYO?;lY~h;X zG0U&SA)GCGycB0(-9ov3^In9QcAbgFe%eZ=^YBIDnAfCQaL>tye7uS5i=Wy;j3+Z1 z^V8BpmSl>U=AmGWmXPh%lYo8Dq}NRJCBN=3i)c z??TQInP_+HwlDTI#r1e+o_U}KOifMsLaYA%{%sJ>qMcs~^!pjW)s&2ny&3o3+<6Z<1MCT9w0miGOb+|r~S2&8VRO10CwHTbihH4ld((gfbcb$Zi2 z!wAe~B%bmAa>LDY0ZJJcY!^UkQ8vrf_Bf%28xN4~dNCEKG!>J^bZc|Yf zr_a<2LYtAdI$dZY*w^%S_!+k}1QP?$jj zA&k$HfPerr_V!oQ|NS|ch-4~9kcX-f!`1wr8IeT%&PkC0%jqW5ShY9CX0q_3(M|xx{ zw~m$JhH{4h-5ArE`Z{pdIXE0rZW^x+W>?}@rF6wVy~ZKJ?$y0a_t78wI%Uk%bQO@7 z0sxfa#psy>1RYezfHy6h*w{3dha%*(<-Y77{NS>3a=q8&W+T2DEHsJ<)95P8Kr~3?6bgv`tV7-`Y+oo@yyCFzw>x2+BC`BYb7|Uxn zc;>>?pEZWVk;|mFN6I06!sZPJ-eBm$eB-@oJ1PX31p$jUY6_LGzVG+z&&lsirlo`hlz{^t^6ms-HS#GAaPj^KIb}#r_dq3(Fp_CAt|2i`iQQot>K)e=ZRZ%<)QfWi$98!%|8`s;aQ0JD<+bOp2rvN{p@;AVI$V&Rrc7n$_+$N@ZEC8M1AMx=HNlhKFDG{ z3MF_7l!YFM6tS#$?WpcW1{j*_wiFRh$-A{-kn(^2OxA`|Szlkz7XSNX2}Yp62!tx_ zIt^D^x(>ppf9Pb5<-$Tx{cdtki|(LLiYf}8-$MZYUSB}QNK6u#85nroU@bKJ3kV1R zsuf<)U58g~T^)$RRUa?yIJpU?lEEkqfKIJ?eWqm)CYs&{RE5Q*C1}Vm{}VOa$G&ly zc25*?4&YAM#FX>jbJh{=of~9lJvj8^>RcBVvfP@T8-|Zi=N?iyxiDAs1o&Yho2BtO ztpDBE&;%{Lw*?&Y0WYHnCxPuq7nC66S>Iuhvm?Xy&kKPNP3s*Qc@1!@m>0gHZ@oP$Z^3p3Gqi&8f_Sa*SM<0e`2tBlQBUHd&y7QM4zXjqyt!M>ZAqLElIlIu{4?e54sJ&d0-GoF)`Hn2u>H{mvcfHf=$ z7kjuDUIVuug)ZH5G-Qj1XC^7!W*Yx1-2C9g<}U2K2O^9RnL|AlnDYp0mcB;}8UK=sYsUa4q7zx>L3V6gTr)m4+yixPQ@ z1-JJ6Nq4zEMsV}op!tUu7)#CZzny;qpTBr8-aDu>NWu>Ojyd>Xi_61h@D$>Nc51Si z!FUR}p0LwrmRp7nhJXBso8C9tEC1i4O!_7qWr8wsf`7OgqPjdt+XV=DV@==dW$P?y z?Dz}qo`(DKdx-ga>yj}rT=SiW#*xX{fiN8@sq~y&m!r4BY6cEe4;TuAzx;*#Ae5NA z@L%r7hhTokQvN576Q5a8zr7N_HryU|HWlmGV(Skf!fe*P8wh)=Pf#!t|$z4k4N zPEMV<&V|VP2iC{#ZQBwu8DDtW?b-1iQh`XrVLAw{gUKzFdqhN&P!jNl-pT#Y*w_e! z&gajb0hy5nFE}HuIa>oB%InZBNDJezCu3>?^$rYb;p))?-ns$ysZUlv$$X%y9=--O zV7H0bdFk9_;qqSOH;S*9#sF#6YX&|E`@O z5TGC25M_8AbSL=FyOrk2YF$Ckh(JCsGQ?-3T|OnH+m;RTN;L(~bnq8;SA`IF?Naqu zl(_D1Lcwi0^@!zgViYztxK}wq+%Ge;<>IdxIJ+?(biftY2r%PmVz=X84a+?N!79A( z>m(0o7lQ;@tIF1}`IcCe0obdd14sZ5H|VbZPa%2_Ry89&z9@=cK+Ta+ic@HPv?S?_ zMFS4jYV|Dfjc3u%gs}@}uw_O(eBwyw<`mof&3CayEgcdpzhh)4hJxD)I^oxSrxRAx zIoM&}spKQ}~>MB+6lZ6u#Q&@o_+F*_eKprK4GgeXOC`&gU}F#GRM7 z6vZyJWLH9TAG&zch*mN&3Ep_`oKnyDJ3@eKpkrdXtc8wH;YeNYJ#fKL?L^t(%J_*O zIB3s$PEBq9f+E>AxvNueuXL~GMJ+6&gH?npBfv5 z)8tgGJ`53;jzPoC4K6^?qDU6<6ciAsQ`w~6cB%znygk%qP{cuN8QfT*+tPX!6R;P6 zA2NCr-*0DkSAh=qfIzjmf5jf=Pe3!}gRm>GdQWNdj}b#b0wAJIfL%k-q6RZ0+>}+? z-3C@G6nfw@*>h5!UX{2`(#cG5$$?s~jHHpW-?R)Au_yUtKG+ax2_3)PM^EnN#y2%l zBy07sh6pzv?eF7mpD=S3tL1OGbF*-8XfoVeURtWVJk5=!4J#ZL!6FSL<~e=x(+K5# zSb(Xg^-OnfZ^Wbe_bx}xvJLfThlGe-L`fJ4mrdu-cWxd}PfQr-nTE2;9BSZ)0uniM z+(7R=4gz**MO@}5Et>-?`v8G7s(9I=m+_**s^lT7(B=6=O5N?+IstT$1!EBG&z)_t z+dorQFnbXqao@jlXIGB1iA4`z#SymINDNI-E-e@Y}fXQMX61?&d10Ft)cp6GuTU#>Q?OxQ~n?^5Jt@bI$L}oBj@4Q;rAY z7@-nooQbNji#tww#oX-NPVEsQ_kV^T>>L)Ds(b(FEjH^JTp%Q>|7}UHx!u|%yX>i& zpDrY*KkQd_y#YI1mv9lWtw$JHHgzxWfoBYK$8|zx&1Mt-! zTY5hcO8@<%ox{8zGL}?R;AY!UvCSDX)iv>Jo|9~8aTOB3-;&Q$!;?peAM&lLML@$mPn7FL94m?wgz znqv%_IFt3!B^GDh(V2frO6*{nNKZh4nA(GN=PuDx)0Xzg<{Rf1x#JE1j)wg{hH(Lb zfWWW{*)4fW{~9~FGPSye)Cagy*i>&z$9RZV<1>w&T!t4kFN&lTQ%B341)h^tB)*gg zPp&i`8CDy!U;=Xw-yspN<4%y8x?btHWYEU|aqs}Og_GbBsV8Fy<&1+<2t zV3S+h`wGV}2V{*9@|IZ*P<~VNZFAfR7w}vu(XzrpLm{51i08KCUR)jLPwikt^G zDnGl)VDE>`+abD!+ZWRkw?yf%kv_EV%!wSMcynqZq;IxR_#irO#3G@>e5&13%H7B zDAy{i7&8ZY2x!(Hi|pZ7gB0$Y+wI0HFSzUBRgBI(4RwkJuSKJA|pTDAQoK|!Fl z>M@7EjKuX3&K!XqNwdo3-QG_OZO{9M-RgF^FTBK1>+0%2MHKTwTp~z{BHJPS@dsg4 z#0}IUR3u@p6xkwhy+rjZVEXHqj@8xi-@M8AW-y_74)E4DL=&el$<0?s5L=$?S3T4( zG1M?*sh0Lmh%iyep_>Quq>Rout^)Ok;|#kOj?)PjyXY9iZ}Z2B-u2`hsK0oLagBZZ z`8R8Ol?D!h&OaHsSfm1m#cK4JAQ&L`K_FrJ^SZEkl)0TP?4+k82&VO0uxLx4udk)v z8F~~w)>M+GN;T_V05jDR?`*-pUr$s`MW1(NzP63KGGXPpz$D8;IB!4dNRS{wOd{)X# z-#E-2DX^L;8582(-{8{rJnI@^oN^mlEh8McxXn$0iYOuUZdRYEw()JV)BIHmK5S>w zy4!SMpBoEBSo_~4QB_BC1l_uID;O){-+X^Dq^gp^ps1>JzOx)`Ns}qrBI>hQF8f%; z?0)nn5tvf*U#F7t7KJC)@VZ{^ewn#M!`!|Zfx!B%`cp zHPTCN>xIf>qu+Swshyz$HZ+WQO;)OgA~(nRD%+nGYLDT2?R?#pU>Dulic(_Msn zrJ}Grf_9)dw#|f{M95<@dGjaayX_QI&!U@E`-tkkX$GBbBNPyT4MXT`k#By$=GC@A zGP@-i^>{`fZWBa^#*(~rw0iIP1JTcwm4Cac?4m8Fw9ePUmCuJ2)5 z>@6$47ErN#Eii7)V>Pfa^{oN4vMm&!0(Zx8okv6U>*c$SU+n=!po5Je>C<7os5U@} z>$G@4a(eNQ^p|0%iTh_>U-NRWo`&86wZISw(QGPTQ3`H+;lH0IjeB}Z^Qz=aY|_og zm9}~WDG9$5f9W!EJ@g$jyHY1V+DEq-dW3*Lkx&Ur>8Hp@)HvDxw;z&!?K7EiHrrgO ze7ZK0?^1a?(%=6M_WA5*VFaS{OUTFX0LQrVkzvJD--e?#U+pXtlmI{rnrz%ChMgT;131)ts#p-XbyJ1&k}f4Zl! zMI8t&(%(7>QLCs-dmND>i#WMv(*-q`nEPUy6IN_YyRxb1!X1_Mj#;%UK%%p0gk?>Cut(UDq_aBl}t_Zp#` z9TrJ(^|kFulBV7Yc4+IKy8lxaw*wbq1Q3X1Ep3gjhlhtDs95QAFVq#HQc^^4zJ?c8 z04)9$&6U{>O9pj@+3Uir14+GV2Od&S(}CE0ipalk6tbthd;T^-;x4QT;k#X|F=6os zgCPM$MbJyU?d$W~ZFk2dtO(rxB#GPyqmeg7=JM&IzE5oMR8`%mhw=dMZ$JPLoV04$;`KRGg?m;D+KUe*(e$P z+vdAJv3dORk%x<4Z$wZ1Wwui0OxaaC*!m0i0~)SM8Te>Pk+yeBWslD|Cut=wzM$V> zGwx<@ZQJwmP79ZP<0vP zcU}26*P|BAz!s=VcYc`J06?}FzEag&2V3-UFU3WFTx^{^*xf+2u1wl+#avo^2pxLC zt4mFcV?vs1EoT9TD6`z`VG_a#nFloYF+aGl`9S5!RoVddhIg)pA8zjadg z+n!fgb}ib%)M#EN^<*Qw;m@CLklCtkRq<*?yctd zW4o7eS2fO^4lFFPk&+9flcr-)t)Mkb=1E_f(q~nF49f0u%RS}~?|bHKsVVxR6-oI# zR?Qu>%HpINlA}CW4-G&VTCQ~TJ6bU%=~Q)0QKP_Iry)g?uflULoh2w}(2c|qm~anp zHn}XP%KHw%_!#{&+@2q2pvoP<0!O5RDNzZ!W#c59q_+H@1FPAe#@_bOHI<`{ zf#n09(%f;{;E1@2k-5=y`s}xJx@j_DKmal-{UPb>JT&zs@NeHY_Mu2%Sf>Ux{M}ks z%u^A%8k{$52tgJBcr7h!|CDdynFKW4B!cBqZ# z@&mTsheDS2F5N3t4d*F)&9m8gS=*;iK_-eoI6I$!X{g50{Bn1%r`mp#3VT~B>MrVb zs-VTM;^s4Lr_K1hb#p)6DvbiAmA#$Xp8o!)lR^;^=|d)EWrVdqblQLaW?*10_)%^I zsq|bdw6r5Stkp!XB5{LN5!lT|8km{4{BxP^HZLqcG+15$S-#rL$pK-Ywp*3lBC({f zIL|?PyZu@9_$Gi>(+CbP1}gYUQuej0}vJj?Wo=Glj&X z9(OJo7a6gb_WNY!*X-6BR}57KTREpDlRLBI*iyobmKQQ4H?jH97L~YRYj8 zfxLn8EbHceX;9&)^ceI)1Bo))_zIAy#@}qo!#EPi+AS_2vtxQ`M$L8;x1vN zVpDu7@1S6`7gk4v`P_=teq(TMPWxw8KoL}%!?7QoTo}TWgL*m`vtqn2i=5uP&FjIS{vmQ{8~@ShQ|&L0Ud9n~nHMs;s)rrf;o{FZM;Gqq zIX6B4b_Rz8bU|U-_il_18o`{zy!KhC5&v{u%@$-*p4nuMz8CfqKMEdWl;5O}MY%-U zKIZ|b5}&zhoeCv2?aXw!duD|vqO(LIJ(4O=bAl6QbenTPFSsZ*uo2OnD-duM6BAWt zFdW}#iOE&D+8S_H-k2=D6V_#Ubx_fI8>=;E$P|$#N*<1ip8RBf$HSfBP0{9h)!q!# zsnlbCSpw{$^ z=h=gblBHQv&)o*~il{}lc!}JN-uer#9)^_B(8P-M$ue3-zUP{G-`@Ug7Ryj#fKJf) zLi7M}u|PHWOF7oc!0?3Kw7(v*oSJyBavsh=eA0Ke^))l|s?=Ou(z+-v&iKzzXYuoJ zX?$&|FIB(mFK%nQ&Alm}?37bDK7U!rtXWc7J@)|b0nkiKVLCgu5-=ROW79^m?kS8 zJEyPa_>VO=KGcsR5Poyw&+x;D`tnrM+{zD)7r2XEx~k8U@_JR|5uZZ_L(uNGU7a^O z#)Kv9{d&2wLJ*91fscciBiBB#IXPD6IJ4I?U0CaPG_crS?DbLd+i1Q1$k|rCw0umQ z(AH=bo%jb*K^Yh9%*quh7TK zW_tR)>J!k|0QI|9*-@GD<0@`w6;g*1ue<%$()ZEFHN=UV=Ldb38LzramvdI zJvq5|vzax?x{WojuXxNHM!>e2Os>#f_kN@oMDdU2AQYq1%iZ-&8qC&(4GGBG6tXe@ z{wqpRHP+Bk-i`KFAeJxO$_jCD=ND$48VTPCbvgKF00rZ=e3l*Lg|2 z&VTx;gF9KQHK+R*uez`7MMNw~Iu`FB-vHntS$kJkjrH6g;QC!2?`>_>pO}shN*T_F zCv%rs47dp2alddwzJb(S1PwvTr)y2EkT5TtI9_h19&0T`;ONrR&Ga7U#`wpm{`B?V zaAK{G7ZX@NYqUgq@4OrMR-oE37hAPIUmb>}xAC7Y1VwcHoxftNZzGZyYvpeQ;mirM-fQ|opy-o46pZ4 z$d;jvkzA@f8;9h$Em9P(8iMZC^-Dlq$fmO6{XgE`!Y!&bYU3V71%psgP|83$q#Gm^ z5RmRJ>24T66cj`nBnCvfyJM7wA*8#zh8SR|nfV^h`@Qe?AAI{d*YS9m*_)ZYpIB?% z>vun2NY8ivrU*D*71NBLGga!eeErkSLNggcgCIsUJ7NHTD>+Om9kqfd5F`0kshwI( z;SGPO;ec}Mt$@eZpZ5)!fBpcGz4Lrbd6Edp`;Uz;e@1Mf>D)slYp77`x&NipZ)*Bi zKye<;Oj>aj*2l@U*W87>aspl9+=WZ`{HBNfbrndY+j_CcD(E?R7EQbBE~GHb6t}gM zg(w4JR2m&GMK9FYE+t!nxpo6=?VvVytD4h23AznK0|~GdFL|v*`WuFY50sxj*xS#w zhb6e%&6ln0Wx=tBS8RD^TTk?k=0Vuu*0uxo?1CL6$KtR3;e`%Wjwwg2;$NPPi>0xh z{;n;JTsr(LaV-R)PFCPBCnxH?vN128Fp$E5DQSm~Ed(vC6$wp6t9+B9q$K}WB!3~- z`BmyuKh+dc)bWPI&m@g-FTnAxG^K(3%2eaC+Ic57k1yK97pu3Pb z3F&&Ep z5L&w5LLQMS*Ocqd5%^m(i1kT?NSM~Mm`_Kpp2v4Wzvbm@--(wFA);A4^kk8+sF-X; z*qq46mGo)Rm8-W+48GJXHh4%%P8-wBX4vG8y!?WkHKC?vjO}^oKxa2szdKY{!Q>#8 zG5E1!GE1dQk1d2G>3s)HGp@s;26sOE`%o7^v74pxRG4khAs9GOz`Xj`A4f79)Y#sr` zy-bEW87TxJ<(vqI(?~0W&E;^8h`8jaGJu<~ zr^GH-h3VQWz~BVwm_ybC7caa19m3>lV3tG_5|fzZzH{0g@(>AJyl)YFmXgwC(TM5h zULQ?Bq}C_8+Dvpg2_z;yH#UZRYH5*BaN-Dv{Ka5sNJAZw0E^*r?)_|+JRad+Xd+Xn z14uS-=hb|byZ{oQ&43>shBYgWFW$R-8@m;D6O?B)yH=B;qd{Gge_jmSQ`uKCmsC&`t-dxt?Z1;oNZBv~p`_H<7osN=p<>(i87cRb#z`pm4U&h+T>uiRe?-{`a7 zR);!r8az+%$E>b9>)m#Co^O}JyNX; z@$f9rkjdZ>g99`Oxy*}%i!UMCMPqRYTz7AeXsKmqtN+cE23XQHNa>{QHK^#PeAE@W z=iR3VcWS#CHvuoFyN8nPm0pw6iNq%@TGGLb;7_@RbGy1nGJpqWJa=$dcIJo*-u%^o1 zPkIOQsm1@|tj1U}_KOm=lKmebCsCxSi=whax|}sX^_wThPAGT zYAL{12o85J5Is0#EV?d$CpG8Rg7sxLk0dzxJRjD4ulMrVd;q3=+j0FAD=N=$y_A;N|y9Mk} zf{pJZPIiZ^#HhRGTcUoPoKOYsCrcohSm|ffd3UUEyEj@2Olf5 z0N-(I<^VY#Hf+hh<$4QPU)}e{J-m=;sj_Q0ktAqvx4meKb>aa%c|9OY0SW^N03i>2 zcIqf)NTdKbbH&+f_U(USFs+A?QGpCw2GTy-^ofV8=1dG|eu;oiyeFG1kht;ZV}f?F zvQ~LNI#;hj>|`Vp6scw>^FgEsI{1yrLw`el$;-?4h=n0@-~oLWpc?J~KRZu$SwFqF zz=QO|;HQI)Ny7wu`7ao$F3=FLf^e1U+yz4LD$+_G7t5)xu6etPa zy&Lw}_oN$tuX%MNtN(Q`I~>D-tGG!k>*eE}HP?JRE{15<+h>Fr^)s^%c&(a4{!ARi zfK&wP;Dm{ZDT(`_tR0SB*%B7n50!~{TIMle0QseNMY<9Y66%cl=kf0&_d1(itlTSt zYsuZOEsiixyI;vCzyzV(v@eD%D!{NBxY)o9;Lx&@n^P4YIMsp1UxQaIzAE;wpqM_i zJod6r-AEnq5hGhowavw{xHF^?WN&mE)Nep9~(gEjK8V0WNG}U`BQdD2O+4)vm0clwbx**=tG_N?d=#KV$s6yFR4taj#`5Uwg(%f(Q?`)L{ zl?V481`qO?6bJNSEWk@YG*%@S=IO3IZ@vdifu zu{5Z)Oo-eTrU2A(ZxigYY^OU{{|S1>vg9nl-S;fI6hv-&@K1LWic^~$wb|mzB;1E2 zS9c)+{}uwO2ctkric0V?8P#aMVTpZ0vEF?3#WTd~#iEwq8OVO0*Sdvbn6mHUg;Ffu z%W>ZNwva1zFxW(PyaJm@AiQM@xq0IT&}p@<{*Qc3hal$un~kp01Rv7$2G(?{$+Yqp z=nJH%JjoxkLik7EAzGa&#T0iv0AfTXGNvCbmnD}aku$7HdXqr0OhfsHOHB><;W>7IURe^4cvTqFCRS)dEq`ITjO_@`^Nn z?4*(?_Lr@0H0?YT1;l%Mm&ctYkuR?Y7*x1pa!_JR@iyjfNTjqS2CUe{)vO+P&P*s9 z`lDczwreO)?aj@%CJ}w!-ZGOFPL}<^tm4+`MRKnNF%A4+xnoA?o6&YPj53g1;wLOl z1T<=}KNkL{=GVIsbr-Jl`XX*z`bB!nF#5yJ;O`|9CE|$uR+sGD+^^4WLm+lF%%pb` zDMVEArC-F)=x;?Edp63&hr7wuH%*(LA=1_7#sL z)1zrCof5tWw2OHvAXT>AZM7^dvDlhi=|I8bzOvw!>~2u&dkpgTE6xH>v;nufOcULo z5klzty_=cg(&kJjI#v6aG=0=UmGRL7r*=iTzFP&%tT--4c=2u!P0cFv z^d6S@I^@BP0_LI|t!n_7S~_hA zMGVPGiT zVNCyUI!cBM3m35{)A7_xl_>(dHYmd)QQ-2WBmsoq2YDI0$@L0|bJJ^MTwO+LJI6}2 zwHLpJ5OvPgrjnDB8&&C81!FxXqswiD0HKo<+i+O5Gi z$v#`^=3yXiESs15?5nnt;u;EhtL}79JyF|>u-ELfeneKlB#3h}gYi}To`I(t3f7kb#)nJM=51yX)jXxT{4G=YH zxV@NfF*(EBed!Faen(EEYx`#(^OK#~0j)k#>D84LAYQlb&6TshEr#818Z@OZyZ17_ zKrtmW!z%dqJIJ5rApAZVfJ3k<(^_#p3)zFC_N&1@F}}|}p*xco*l@*76pkI<2JV1V zyHn3^uoXYpOAWw>{sgi@poX2T^s@Qz;aR}`X?F|=zJX-?x796@(%RgQOKk_{0wte6 zgBBLU@VVFS3jm9&u&*EqSvs8pvzGrh?d#e%TrU%vvDKCF(# zHO`+V8Ec6~px>l7X~@4>LaO>UZ=}=6uT6sPEozO z^0@TtqdTYiLRJ1}ll0UEe|`xtT#&joN;CQPzPRz61(&F^SS%~zU;mh_Kszaz4g*{Awu5CzZNtco`UDF0 zy+my;7$tMPTWn5ELcJC!hCUPjsgNxf@;vzVoqJvV{ZIIenPeXTAWpYzHwK@0uHI=@ zlJ(=CISl@NQ~Gy5VY>S(M^>0~jiLIjAoP4H=do;-3S(sc4=S<6!<1O#`@f$g1&}=Z zv&fI&2DL%9h!DABZp2ds3pBB^!R6U!R6=pS+8vD#vgi}e!KnL&{r>9%<^`(56>M~R z55Gx055xip=}?8PWoJNb1(^rU*Dp7ACaZ=4@Qjl9slpl@r|*FsQjeLGc(bv+4P0}- ziI@nEvZxid{|{U!3UoyPy>UGy9SOlz^Skr^bgQw3XgSxkyS9J#IrBb|W186$X}un$ ztG-qxNlVI~FLj&Xntbw%yx>n3kOF?M4E)A83bk0E9|>+iKFU`PXTN_yz$EJjI4!@_ z%?f_En0!M$_sX zTAxM>*zh-e17B7VKJP^gu=}_#ud%X=;`H9UTb1X@598StsuXhJtY&#h_c#nNwckX8 zUX|o_TPLgv?9}!z87Nnd%n$6%35e6masrpP^c#><{8JiH=cBGcQx?Qr!CM*Ys%)2> zA7vpYhn~WaKZ`Hl=7^oe>e1yO&HLU(n~?|8g6W#z`9JHbWje)|ve8W*I6w>!muc`A zANw10YlQ#sf;!!lFaiPNclQ)GF8e^s{q+0IFYPy_w<>!Jh4K<@c#-7)YSwj2MCoHR zF4gG?mW~rF!)ZiVUWq(>zOueOTCeUoS)uC=W?si968jTf=}mh)iQj~L4r18-D<^k> zJ>(17qH0`U`I0sm&(1R=j>`V`)NIc}nC$O!4h3}~&q`^Rm(b3+g?+R-o zJjBHp3$?k-Ir9uk;6CWCaI}lPeJb!qd7^MD1GL}F$jVk}+X+(-I3;9AoHXvVDy}X% zF)kj)D2ad+{8Q46wB#5e>)x(D3Jpyi)6_Uv-+cneCtB%& zQ`CQTy4u56)=PkqO*$XBl!S5TNzs?|*V@GFNrX`9w&r!AP zzcLeukRN^z%)UbS9Q--7BD#O$Cxwq084msa8JA3c-K0HB-cEQDnvcrg6o+%N0 zlbRhth?eWZ{_U9V%VWzuWwTcWJU4Eh-@JLQ?)h4T%cp&-k5u*?knav#X6x>I2RU^g zR^4m8aeCHxdgMw>yqZ3t8u9}cDwb9XOp6nT=|^6lcZ7~-6He7qN75Q;L#RKJsjgv( z`;_OPB_Toqkvvb&28*^?bqr}7h(dx$o;|awngpdHovDM_cnz4K`^I?50BC!Qqo%$< z(+qv&mdknoDK$ntwsSIe)Vd)~qDrR#flTbWulwA9?1(vj1POV*nCNmBXE(5^;k?&c z&SFzDOie8(nl?}xhtm=0uYOP0;K}h8~%LU&+?1OXyh7K8)=%j4u2kT;$+ zsebd$kwK2h&0~EP77CAaJgyH>>3*@2+1PYXS{eAxXfv1}h!q^(4JEl=k>=Mj0PM(; z>>7wuMSYanVu+~zS)N8hL3wRRodR`u7pGH}4P1kH?vmRi{!et&V#^DjuZpFFm7cb? zw$n59#-|-u0aD?su+ZzI_^(-L7lO2vA=K{(XZxz>ZFWFsPJsDy@!z>)#|e&_Oe@Y` zgxT4Fb%ctq@(}CzO~9@dVq_F-`rE0yu@Uq#PGj`zErHD)09Ub!zXfb$f3n7@kBNCc z-EnWw$#5bSk|Y3+Tv=x2Ozj0daE$}t3A z39$ybS)jS0c>KVYFZl=agJ>H5Gbw`mK;&pw86JG2vF7&OkIb#5JYjYBuA55_R>@?k31iyKh~u_u zUNHw<$1E+eQ;IlwPyB+~+hY;Y{)D$)%06&tJhas|-l>zpznDf^Y+K`kkH1NB$68f9m`zGBoe;<2$URArk4ek31NW_QWTx zRG|$GWOqpK#4?S@=QVUbRbq2*MqMl;Pb^m1WMz}9edkKn1I-*f+%4Q4B7EKLd0SRX z5#=nhDyBn4jiPDuG_MmCzH@YHC`cS1qv8Bi{pT_RlR2=+w1kRBuSf=C#Vm0gc695$ zZTomi!tQ2f#?MGnI4lF(wBVxc9<$DFX2$Y1(*+LxD$QBe$WPLp--WrIq99BJGuMpX zDuWUM@X>JBlvh%s5uGt=765ttQ+5jBpM>Op#tPRaC-a|7y~KqoakrY{o&RR%pi?%b z+8a(Fd{Rz@hwkkK@Cm>W$jr{J9iFMM7(jOMp>vdQ%p?S_J3jr5WNjDugO1D_JjPOLhKBxaUp<49V_>rtmTA8YnwCb%w#NSbtDXK6 zXi_{=@s_w^_Ayb&6?c?0iN_L|t9)=~cTZh&d}MT7mo@$a|BkDm_sPb6Z3DfT3?To%Nr1B^0Bi-s#UueZ7>76e-yt*YzsZz9^b9ciDQ$bHTgNp``Vh}%V%$eThO;m^q zP3~Q*)^M=1W3|+a)_P68Gg@H@aPY55ss1pBtjZ{<>gZ_p_OuDUKF>xe3ZHy^;)2Cm z9&i@4pQBsMsqW3d$%U|KUl0Nb1A@n&F(HJ?@qlLDzm7(;A)VUxPZR8kX`p|iR|~4u z)Z$~~iFf96wx|F3QC=XoCmi6_+7((6soxQ#{vVmv2mkJn3ONf44*0CdwP@vLcT{&$ z7q>Q;6sP}SL{1J{R5>Rmv#vS5=C&D231b%)JEJ>3CX7>8=PlqNB@)_kKRC+@9+EpaH@d+???%ewHp0oClfe%9~lkQksUEFWWX=!0GVf(nD z720>qM?CGr?#A4;>6%?Pn`OQ#H8M0*4+!JOp~cVUNWO~^w6CKkZO6LE;shCln7PRB zCH1X^C(FkaYN%)|65ra5UU3$JDZcGQMEn#rMMj%5SCn(w@@^;Qr2L!gdueUGJ%7I7 z63r;Yyu4$k`yqmMCa_Aeq zyetiBti9I)^;MEMQPfS)R3#Kq<%H=dX@26fx|%>YnOR+Jf5{HFurMQ0c9BOQgKiOt zNeQ(Y(9T5_j=9;4Q#>9TnyQDk%7nD`3vhfhH_Noq_=*lwp);M-*2caGaLLb~a2_Tc zpYz|t3C<0XCNPuUdl1#SI8kb>+R4YA>3`2i%?CwG0>RTD#G-?MD-NtWGiNTh0V=&iQ2 zI^bK}W*evAjX%W;G-9a4iAt&ndJQh5G-HfEAvSXhZ1~YfF(_vtC$Tq29X@G5r0ev1 zyQGm{^VD8d)g^3x&qELfxL0r|GTsQ^1XSMUbIONY+1GEbt{3ANn@j$=qZWS%Kd2vPoK7K zdYF-iAmA3z%H~-iJJWotv$9@wpA(CntOQqW4-L&kn^_l^2>Q0A*L#`%zGkiCJ+=3c z{tR=gVnZP4okB!!+V=H$Yf=>>XgbCC;_KH#6iQ-_SJuQm!Boc9(-Z%AOP%st zu!^cG8LLiX$A*7bwh6f*|YiY7-+}Qrqs#-_nFSt z#c{Y;=$L(lmHVzEzc=Y4r~YrA{3P#{l;i=(Lss_w_VR-*Q{Lnmd-eN@AplVNjuVie z@q@&35Q8~N>>;hig(TXOv`^lDpR6tNI4Jqy~I)G!f zJ#H?E1x03v`&CucXUjY9yqlHu_QNd+V-vHA>9%)+_BDwLbS9aJzx>A|y)T`fkfrPy zRtWNPHJsRWsJYZWyw4$fr%*rH`||b^GW}XA8#(u6yEfyHYOkk1+h&ll3fto%*cC3> zu{3V4o)Y-`$e&<)ds!3?Opmt46UA-5^^0uZ3+uCwrhVa#D)LZF;T8Y}TKVONT`wcZ zx!+w75Ml&GY)&Yn%?tW;=KlexLK={UT?XKtJRCkzNao|?;gPlo;2>qJ$j|p~Gj83h zUmgpgZgiN(JDIPw;<^uR^<}36T=a6|VUmI~$_3BPE}AxR_XKXy`)-I{*Ag9wnaRuJ zEihkR8_GgxQWQVD69POBqL!S4KczP}g%OU+n)IYq#24%BI!+7^&sf67usgfKP>kt$ zAhQuJ&9;-}Kto&AU;#P} ze(*1SwKI4QhQow_zF#=^M-L617Sou!oonP8ZNW8M9po8@&T|D3uCs)*`<8>)3#7|L zdMiRs{*^31elxMN3$1>4Nq%UC9DtwpBs;)WM=I#o8?&m~ZI$ZwKcODPfrqWcum%|z zLOsf|o+1W5I9QiW%07Qe9~l{uBo>jX2sNJZn&EiMRUrT{v+FDGVZ4|KS=FQMLj4Bp z-w+yqYqp(3CrWlDM9VgI^%)Fh3ghnl6gh zQ~hP5)k5CKw+>FzMXRZ+o6cCsw0Zi{-F=w@^Db{UCjKIELW&-E2*s>au+x5XQhXeO ziH%VjDr}r@ZVlcMsIAT(v#9WDY9=%Enl0^3F!WpgmXqdpu&TM&giH6wYR7&Yr*c|6 zTbEk9z9i*!^Gr!ZWNo@^`QEr*x!EL@$81b0EGB-9u0zjCfIe-czwu5{QPF!7sqgQj z=jX9Ol*dWQ$pZ8pKV_p^(n~vs)ro&|w@f95NybJY=Ss`W`s%}DvawYx;vHmVW@k)c zlOtQl=9|qE>F9%mc~BSJOYom?SgJSYPuXcDcKUROj~r!k)|&D@UP;u zYJA=s*BU`M4nQiyqvNwX&JA3gL#&Sr`r^DgHgmSdIBS`HkUMQ$!{5=>RZUn^Ocr+2 zvC>W)wn~spcj*_*c*$dHZOuDfSk7|cJC`cry)kgg-h7#zU*2P1L+{UZ+BmmFJ$R9Q zwImI1<`xxkCH8tri_m!Obj!x%gfi3k^eVUgWI@z<)xohRohfywqnm8H&4=L8@xx|m zQu`lnCK&o;yd2O}J=z@2&>Z+Z#_Pb(g5$NSUlioB7y!DppjLY&Stlg(`{9xM#@f^u z%b(Q}3bL!QwHY!i?iTeF(&}z~(=J#r}V*W)bgOZ4^y;QZD z_`lvpsYewQ)5GA7f|4ey!s-nKg$Fm6?#Bx2ptSJ3e z8p)AgSUl&mm+nKz(m&wJT?YZ_oSjX`qK#d>XRH=x3~yw-52q5mj5PMy)1UtrnVfKeVu%00 zc}YMGpnj&Uks>GjOOCtW=fF4`$$jWo6#iW~B2}dM?BpVZMwyVz&fcnSF?-s+=`0x6 zfNq|4Xgb@Zr#U`<(4RbiKO};GC*P)IPNh7R!?{im1jpsfC1>yu>hRt=%E5^ZFHDg~ zgwST})vl-TUPo@W$*?q8DxW8Mc8a-KzWL96zicZ_(m1W9L;s}Ln`M@URA|)RQcGd) zcf6BXgb61wa%?=nT87_7-uRAl8)}4;Wo6X~tosmCr8M3F73zznWSTxY)Pgu38+^kS za|by>n%t+Mto=%qvb^wjc~(*1s%vIeHR^RREOx~mdx|aIb>5(nZ46r zDbjuOX~K>c*!d7`9%ImJpFZsxj`{`f`k@HN)#DOgcTf`f=pY0_*8 zs;Z7NlthJ=a+eW66@)^OmxV@$gBj~k9DgHi&)QLuv!kCMqvjyb6_padR;^uhOc);I zKJOQMf`mC4!Q$75&M?um=H?dti9&lozw+1+b2>bb>TU7f0GgUvhsiAo?#|TnI!Eta z^y`-JjA~wBL=zC3u7d1MOV+C|1HLK>BPW_M=cA*elXko>p88Fyqt@6-9#6J9CZDrNh^Dd3kZ>_NIE;oBkbyXAkU_6hpc&a`a zgP!#<5*y7RVk)pU@Y>E;H-K7>K3!%U#QqdCZQnZPP3Czivb&>N<9u?GFn?K^J}4>0 zXTT*W0y>KhDo03jTF1uLSeZ-I9(xNbJc=82Zy4R34*{3z3p_gSL$a;)MMP148<^~WFlxHCZnv9OXFyr~59JQj>2M33GaM!)J_aTQF3y7z(zm7LosV0>M-(M;CPP9I=Dzjp1RLj2~0 z92(k<4s9MMiilSMhg?zAifPXGr=vsB@d*hH=JKnuIVoP}RvF-Sdaxl7nuVw4Ee-}H z4u6o6+uIad`S}KZXI`%y6KFRFLumNV!S*3JdDh;;-o76va@0i=Lr=r&TT3g8S{q0M zv$BkL{qW|Digsd@s^e%}f{k@-S{ls}Z_;~dqK2lry44}(;yI6bzsn*!O#{C#hb&U& zQ;D=((Ldj3Kl~x)K3AyiRaYmP#!h}ukn%^o%)mh>l_|eND{vpX7#*Ky)2YnF1z^w4 z>LP?s+K%8Blwji42+OL^cArY~?@va1IL(Vc4)elnt!nD~WI*LG{ZCQ75B=Y#R65O` zH{+dyc)Pl?QutsZ#t?ekm+G^l43LK-Befq7%Hx zr^(-6{B9AwmzS^5v11oTFHMv!2j82_c&mJsNCR8H1Qz|Iq@$e%#G)zU@~$Qwd2tmJoyC;UFz z+1Z%}tphib?FGCCq;DgHkEUt-XG`;Fd3elw3-;zVh$#h@7P{mM#_fDp`-lsyw}xi& zQgjByB#%?J#>T2cQoWt9V)CqyL3hes10UofwRFBN&tIdBi!`Xbg45{8SepMBqkw?+ zh?kh+Pj1ux>m3u{Iw?h$7VwUkNzn`}NM$X3RX(rtLKj6bj>Ey%#!Ho3Njkv8*9IGV~JP3bGk1)UW(l z#)1I;!eHI@fJ47Wjc?DY)E_Lg%0O{_WEK^RW|M5g>pm07hPJ-M2wE|Q1IHu z(gJ#77QnVi^==<)#M{*#tW7@{kTlu<3Rk92^Tgp1iyln0{xvd&LJf3vr@@NdAN2eM zn|4uMod9T!11CwXXB`klFNx#lfBK#wLwx``C@#Tdf*tl9i_Ng!S373Ue@-~qGw2Koq*ztU5uFS?lj=d*rPuiLiv{qQJSoVQyEHz35d}G z2bzdZ)J2=vB{pEvR!%{I5vgv=n~VuG0!|TUB0d;SsGcJJnmWOk7K4(3b(Hc1**)97 zSk4p#C_SyC00E`Tg{ujL(6hIWnvTyAX+Ep4!rr6& zr1~5~&k9Rr@e=Br?jae?e=7h!w7B@Wh)7X&^K*KxWq&?HnD4*+i!{Fq>pt%o+4_YY zRd&ZrZHu%%h#jR@>?uL4TtODJoDX=p{C}+3%eEk%ezHdS`NZBSQvy zI7!yzbA0E<?Qe{J=*UzA@@hb97W$U0q!>@0Vt#*!HSI zn+TC11Jlx0SJ%+6=g@r+g1v<>^gBBf0E@q2S-z%I#8R^#{dXGlNQEg9GY^Z>91uhA zoT11XPcOL=y+i<$Q%6^K{$j!O&X=#X-4+9Vecxs-Y^`#EsI-OeO*EKH&zl zY++`l$>PwXq(6R3v0~$e(?8=9Q{$qbV|ltztP!H9t=xw{mMC$3P(Nx0p4HF9G&& zTEEY_3RpY)YQi4e2h%~l-rk(+W|j*7`>d?S>iCt{9N)?-@+_6AC2yJ#i#j^YL;aA= z=)iEBacx*X_(m(aJytfozL$MqPy^$0XvC~)x}*--kzn}Ll*`*&c-^zh?O_;=IZLd}M8 zxfy|8TPMMt#Jaj@vn9O~Z$FCh5=&rVx^8eZMo|Jv6!bd-{S$sQ;#{~S-_2Fc2pX|K z@+`~DoLaq}hZ^SEC!WHoG7)B+X-+2s?BBhzHJw7%bhwihK(fG=<6&ZNe38J#D_P*3 z8pSXPYzhmv>2u=D>#B>J^Rs!K*`-ihf1rNZeLn%FYipb00==8%bzB+qqfDna&G*bt zU7={wma$->+{uNJo>R8hjMGUG6dNvxDZCL?36CGG(dV`9#bm?J$KHZ)1=P4?()_)D zkJ@T%6KWdx;!I7M3TzB~m+_MOwvNxt5@`gMB8L?P-3xq-=uM{m&@=cnD~pas$4N%o zj0xr@y((HCDY(Br(f?I1y=TzwuK@EL<>B)^JFaQ-((wl7yrIL3U*GeyBO2@KvdYS$ zzA93>9o{ufeah|lt2wg1-aFbaE6beJ8pZ9!U6Km&vT44j$ZSH2cPCU(fC)R9llN7R zUjhinM=K1@%QhpY4dw_R&jWN6!y&ri}=V{mp#1v z9=_;F>GZKK-=ol}DM9LwjI7M-fsdVr$X}G}6w&za_g}-0m?*XQGMOsZhHSVf{@v0t zNTyGsz4(PgiHvkb47)PCJziWPxMO8yUoqMH6Lgl_*ld5AE5D=`F@Rzx5$p|S+x31m zff(why|Iuq@4Du64w2cWkDEP-E@WCo@uUecE_LPkYd|9?dcnQBJD0NaeW&pIP&tTO zCoL*Ywllm_t=$x6llT1Rrq+r|92&9tVb0}3B8^Rh!@lD|N+q+UjP%UQ9R+W5GHEZ7 z?Tqx4O^)56vZ|Kv&umUo&hGj_;VcL%dvv&F_?WQplV^2Qn{e)H3-AyZ>}rBxPx%Zo z30PtZV^52GNv_I}mzIg|M;iFHXB!%}qei63Quq$P-rw`zKR6PzEHs3tXIK9q=U7@S z-EppSr{Ff4_dBWEzeEuUu`H^QKMq@sRi?L_cnwq$nsEtrDgBp?GiNSX?}OFxwLZfS znuat4HvrD=9KG$+-A=ZZ!$SWerkgaeIl$#bKViu0dEaHXP8y%1agb`YS=*6O%IF-X zMF_uYk4sDN_!oF#=sS25EToERtv1EYOSv!KLb=>`!0*M&%j=z##J|p#!$r@x9vMW` zcsVo(;H2*=cNO^bxGop){Ljx4fOq=2Uvnu7pO-K`b!}RC3rO3*-DhRNejz+O+-I*! z^SATYjre5yQr)7P(l=97-Q4=JGT7xrEnBFAJkrkAGaWjixaCtrIXMK_i2^$DsCC~v zrFk%fEz{gOK>s$(3{@1F7j1yNcJ6hAgP=!S+e`X2iO~boHd|iZJ%gqbA(#1K zMS;3m{H#z#(aq0Z71^Ld`Q}+s-C{?#QkB9bjP1A$kORqM=xU78e!g+1R0OeY%$1 zx2CADIa{&u3on>T^zx7UcB#E-ciCI&HCO&(azFb+hxa=ouKPbb_b#>?4wB`WKc#=L z4zukSvh@m8efO0*5~s39B^RdeqI)^g3KI~jM``S%J+I@$CZYYPj7NeJFW zw+Ynfuzr#Lk^>XmYJ2!Y=%V*CpJp(P^9MUO1z;}dW)}`^Ll3?UZ#)Us?O|rpNlLPZ z?>_D;0FI2`hZXZC%G*sibXmp*u&+M^55HDdYwWDJm=O~L=W?v(KL*J@mLmj4uixiEyCGaPEK6>F9;Ys52V-0 za1A-xyL`4A+XG_ft2JV*2F*<51XF^`Fz{=MeO@6ocj-L1D^&5VmZ{}D6*Ue_jD5ZDgfJ_nA;>b_!@w=DH2=lT zjx^MEUfv8#PSWjt>{3E4y`JiINZmmAuKZB|^)z<=CLZKNp;p;aVMYI)FA}@OUYKK$ zm)tIzZk7S#6*6nJJLUFBmq}MwS77~d7-)U51PYj9f4)b~s;U+9dx)MMm28dii^a}^ zoHQ?kh*W6+7BMM%e~wuf$BS9LORc1uk;CzzcHOK-tl(R{s6u!+b-$OKoScS+MNaju z4AlQuaWfnE`Rdx!{>!;4P&(qXotgPJK~D4qjAc-y0qApBVme_4PN-$EwA+k7Em}@8 zBPXkhmHgh6ZS{B8n&HKd9x+8@{}vALuO}vMj@kK({(&p&YSut8!NI{Dj{G~0{NOy^ zXyoXm%{cGS#Kr;sB`|s$+r?r6I%Fa`m!s--6x}?|Mr+ISv-9hDN$!EboV!Ms%ia&4 zjNlf?2fQ5kxQnd+qj($Chh1}Y9h%$#+I+Vdn42GU)2AI>dUybz0tn<;R2X~HApkuy zZYhI04k$hdA4eF1q;5ll%GH-CMDv~z4>G$JA%@*}H>D^AsOM5sK+gpEw;IoCcrb)0 ziHnWvgP}c?BVNcxKegE=5;-Et+`W3MF8Gy={m0oOdGK=%aX z%{W&ws}!xQih^BuO}AuQs^|GP)wi&j*<&Dvf=p;Xcf=2{-v=b-W>A=?(&l1jzM>6J z*Na}A7^h%mg=|@Z0>^jc$Vj>*T6noqRff|2qle}l@M9zT;6GH5>Kecr6$1v7P&oW0 zbDHPUx1d{uQ)B8^=Rb82ME=a+E7E671kKPRvu;;k)Bo^a5%=m00lNg?d7`ZV9wVS1 zgHAh;Lq_VqSkSGTde$DD5g|Gim1zCnwXXi<&Y?+|)*Ll+YHuOplSG{#i7{s2%A@bK-`ty3o;s;JzrhRlCQ zDJyu)Af^T-9>8H|+W77FGM9dV_3CMRL|4}YQ3KxpzHUN>q!urrY-?)^xJ@~1H`VGD z3+*+cz@c9a3Be%5o-o&KHVJ?pwhry%;Qex zd>mrBD_MK~@AsHU1?PXm323EdWlsGm0syD1UuqP{g3wp7RaPE+q^ajL})X40hs;e`F05~hd^Iobe)ckzV8BuDa#b64q$8oCWY|8|K^da zx0f=DgYOXb3{gJ5ZD((9P-fEJ^5rgQEolbkPWAPI{a(MrZkw_Q3Hj8_x`TU!Are@L zTv{?oKzzxi`rqrji(HA#m~RIC{lEZ(QosVw+Qz0JKOf{t{X`8JNMF1t7~OKN0}m?5 ztaEX4_F4c_5su`8Ul#v+h6!^pv;6O0Adrpg|2-!E_dkp@1Hj|(f5)%8SNH1wj*aW! z^8fcYNIm4b$N%@Y0Ji_1-WpKX!2JJSR=JOrb#?WTf`Y=LD0Y{u>2Qhy z5npoerexE_QT=HPYCsq>;_iX&NZw=7s%0Q2nDN-t2O6ZxM~`lS8H0-Ce+I^XAEjRF z8Oi>cZ~BLK@#d=~)Ez%*WdR@q{9{{hR!_rl}6k+W%sd^XvjU(Dz zfRxB*x^ZsJ+rwfGkV7#e4`Oy-5yv_u>xjaNB{N+F#G|?h{lC!eiD&>?Yn1 zHPB?dZk$+a>%#vj{nw6zfbB|In%|KDP>Hz&v76dCRD-@pg74)6Zj>Z)5uY;L( z53O4?2JBbtX?C&wJ~VnBt*lZb1$)nAqs`cADl2*Ue9YLUZFxPct&^tvP=_EiNQE8_ zf@s6{bWRt5>4T+beYasQmYB`{9-I7v#>1rTdB1J9j4J;75tpW9rC<{TV$xmoZFr{T z`Xf?Okj@&b*2rlcoOCwu+>C2L*0OTdbZw)i>`t1{)JbUxWOu&Av*`j6sgBmn|HIx} ze?{4L?c=w%fk;V%2q@j%sep9D&>`L3ZO|zVLrKF7-3%o#bV&>y(lC@mH=oPreSFvV zPx$`uUd!bY*K(F~Ugx>by^nny`*^4&=x2Z+Hqf`A3l5~8Wlt;j%_0hQ2r~P01@lY zvkwq_RL%cAm68)OV>woT^U~Vh9)RY#Gg=PD@&G32DHvKIJH|LX-2WyIb{(3Mk`fqj zv)^*W5jAH37>_mPn=yewl`hUgLrp8qblMg$uNb*wk&yJGnHfkkc(YR9^9?p6Rs zM^q*az%cgr9LAvZzmyvjYn!Ps@Vqg+@W1P!Uu!VyAZxT)dXczv_v7xuz-5kBs%{0K z>_v-IK>XJijsm|iICtEQ`pNT^y-mDpF3#DR%kR^w`xDAMb<}U5|5<^5_A!BM7RWp; znyJ^P0Rxup#dTD$#1ePh)%6+K;pIx(;qYzrsXG-gv20{$BmjV?I#e!qFN_0kulkPY z?#@px%sNCYrsN6>H;pQ1?74_9NO@2*s49=k2T(IgrGerFxH7!{IEiZ)oq``I6N!WykA) z4cx)2$x5f3#ue|g?1v>an2V&5TBTDvh48^x2Yh6EI73Ft4~slc@mS3H$EQgZ;gE15 zcHQ}jdC1Coqgh|T@pzw95eA#UQ69$Vp8GlwvOo6yNeTZvp18ny0CWsn-{d3`3i@xk z7wWVk76VouV|kp-&FMy~>FQn`wj7=BjjcPjufmqwBB_}H*QeJ$@Ks>S+9og+OidSb zfb)zdF)4WgV2otzR=XCqzw0H58zG>sev;m#>pPe6$7))#1PFeAQ}EK`j5uof0pJ) z#gFZir~iH)9UTA!I$t`0k;oqIfon%WX1g^O9V(fcv-#hKTF2L2ii_@#*S$pjUX8IG z!_9C~Q<4L&2et#Q&)kZe`6~=j16Iajckujp?Hqs0VVw2AVRn|;x@H<~=EU&hV^?~m zbWbGdc5t_=lEcA4Qh6hIf5o<>{+hyhF>cK!rv3t(`Rnf6hmEhH!itq$oR6ms?ffCE_S!C z;kDu+jLXaC`(;(tK08x6dhx;KmR*qxr~pb=NGaYF_}BHFWX|)Y*Y+)r=Ys_KuFqaxTpiu0@Jj z6~h`G<^vN~ZdN*3yMlsqo={enFmg{n-~|H-xP>nE6?i(!Kd=2=`bpQ9FUQA`3Tz#| z$8ckSssuPjnDg;B)@gxQGq~O6j{pO=1$J>u3rl3wsU=oHM$6^vqW15vkUEB=Nt|36DU&-|RWK@iK01O;G z;Meu0XG=`@{QNvTB-c4S97x|RTW_s$g09X|jd4*5Y%LqZ+{$|fhJQ{p{pY_Y+7~dF zdX4Sn&qa!B#sc7#{{kS%u9hH*^E@0KS(cRgLg|b2I7{aJemcZi`B_dUd>K7kyAf zvwQw$Q<4KU+MlOt@+K8WEXMqBfz{cm#HpkV;wnfkwKzHDeqCB@ZfRcEf>5KzdHB^G z2f#D6JqsJU&9)vi&f+NedKsSrV0k`e69k;yIfwJDtt~)?XyD!1-1)I8x6|#%qnkYi zx8_W!R%Iy!!kMmwM&9&LW#Tu3K-}?~O#@>;{mvN@nlo2w$8f}B9{f7RZw^}GYCsAK zdx)9k+379vpB+vb8VU{$2fM{tu7FMa2+>K+CUeK-F_cxm0eH2tim=@jVQi6KpWgam zPag^K@_HbUh|Z4;!<&Du^t)?-^RJM~R=aROzW?L??)nvL?6Sg66F}?qYZEi&G4H3~ z%P7ySMtw%3yxN>uf!f*5>*+GbcE^e1t39*_aEFvyI<{_}&kwpLumDM}ysxh>z3m^^LzwXYv&5hMQj>nV06hu- zo{B=qOLaC(zx6vDk1v0Q^(qbN8P;tQH77yi2L@9Ov3IziZl=c4<>4y}IRGB%z?lM? zCftF{#2Nq`&q2Mz;SDxq#obHTnhSK!c z{k8rP%e|Fj&Z8E;wtL{DZRhx1r({%U8@2svtsN7%xkZBK|A=v*+G1CywZ?(x@$JuQ zSoB#^AV@GiKouBWa1z}4@fgs60cgN*<5oIUqFYFmT2(;-IspIIp*-xht22NyugZ|O z*v?Uz@=;OshPxR5SlO}hf5_tRi@8AoL;pmt;P^bWnQmL@D&jY$3X*pRUW<3vPmDp!i2doGOC8tG7`ok7_uXb#{<*WT|J0#>o*$kded8k(Pf=kp zJ|`8R{QW&wkT*4zG#q^z+ACcg}5xuQA3{* zz%xmzao`{UNUFMa`gVbV%Re4HzBfGi6EbD~hRlyGwhj)PnI!QnL*_MwMKIx-c{U&T3HSy<4*hcqRftk+$ay(75bGiQWuP<}7&4MkYf6yqeEpS;TFt>||6_zgGFp zOQ7D26LSxe(OWWUUDs%21{!(-+W1jSdx&db(P24_B2rf-CjQ8o9-}#YlYLsLu0+q=0n>R7p0y`jwV8r5BUVOhI-y;q^E zpwhBY0|g9XQ`w9}4#QADEcjfMsqYB8uTfyrt9Au35FkMYivYK1vk>4mo2>;KN6eNt zE(#p~#*GhI4}2YTuDgx@bdl5txOaiEQR4m=XM-TSB9(hql{*^1mHVIB-T#6gpR%&& z?9Ty!r%iBs(SA8F=(UA~1#D9N>*wzS=G0UwiN)0VK435sxkrvIj2r*Df0)aE)Yd%7 z>!#UFfWTGgb!a(&Eeu+_5Rr4KNNMV8%fJP;JzH1_UWi@(Hp^8Fm+=~;vf-ys$x2S% z@(i;`ms2lyTqdJ>F=W!xZNjNPU%oNDGt2>aX}R~3Y`0bv9p0Xdju#w6VgMY478mh* zk$bDe9--BlmDA6{xXnw4Urlk8KrF`9e^|w|Yu)F*wjMcXqWM^|U|d4b*CUy65XNW-2j!L^Jr3NwnUl zSm+*}VsadqWo7g(?YiTs39y89AD8AU%hzm?7H8vO4-r8NYigKKx0iY|OU?>zlI5&D zcfj|`^O6q-4Df1fdzcbfE*1m&fHvMA`6L?9?%ko5!5arS@dv+V{**%>!$NTXUisV^ zz%UXZ0|wd7;;7w%tsHTX9iQVYAf;cwu%n4RBZUhn7wjp-H~& z1#lJjrb%GnnSK{Ya2tKno+#@u6IX%bRi@oc97vd25|CLOU)=%|=@0#mQWO)a;`xw{ z_Vyy@JNbD0)?`l(vsk#<*|k$F*oLrEyR)1lDX8)QGlJ&HNr_I?{5}m}yjJSMJ)B)d zlzXevCNX06;jqc$^7q%|%?B0(shY+IM@bc-RRN1tU8~Gv9dCjoE3IX#w)>>&!0$keq$K|B*Tu4jrzxVJqi>)`*KvCKRUZ9TOW*2 zh1a3asDR;b!$T3p=KzOm-XL8m1`CAX&9eQ|tJt^ZDEtHJ0DpQ>rUmU@eeU56iwjp} z08&zbQN#xUQ2~v=Om14*9SnIu-jj6(PKGq?A5T`Pi`Jd0i({~ zVpa1F*u`JNZpCt(_7?txRN;UAzKtlr8deL+3uNuicg_O#^?DlW>J2vKm{{ao8^P;w ztn94GBtx<)cE>zf9LV8>1oiIZ2d|@t08|gZs2`!*Xog7k)aharJCCgCip8%F7O zeALVxv4Qq*v{cVE%S6n_E!287r{QY_5OCu1F!{-*<0 z09|p_xoO?&%@n54Z=htt@zu9TCq84yyzDEmtL$iQZ-4JoUr=zmh#>>8^wuleZU<{s zMDGW}jsUNd{C^uigf-&VcibJ`&}WMvdovuN_)LmYsfNBTmW~y3_D1dgJ)oK8k4*>3 z+QuEOSA$7Yn$QuxYpSpMncfx)ut8# zv*7r6H3NgpR)1iTW;;!={|7~%_2B4TbZng2JM(Q@bx!M%GTP)!paGmD4n#a!dOro; z8Ihd)NrF>`w@!JyDvd;$p%BNWt8-%Gz;czGWi4oZUY`HR{F#HpGSD~pkCGaI!vG+0 z^04*5vio0P$||9?9bftj+5sZ$A^s)bK0Bw@gNvIZAhiNOg95!|jV_rYX;P~-ZNtEU zMzfBL>%M}(ug?G<-}QrwLGn?T0?XCw96#&YIwubbPeQBf-5nnp+flY0zoV+Z~f9nHlDKn4IPN7}a! zg)gG86mlLezR>58o^<1g6J8vGYD^9QsZP^M`#kO*RppG|T})$eKzZrzoKxF<&8m>h zYq731*-A)>_x937PQW!f6_C)~?W~W5mpH8~4`hd6{5fuq{~kA}i@}OwrWwA;ut}ZEIU}k2frq0i;&b7A`#Bpos z&Q94H9UZ001LWlapg9|>?2BEQBRZyo#o94{K)Ex-%=F^;m>fH-$JD6SQprv7EQK|> zOV9L8mU%(5%Ns|&jFrH31inmGyCdRHb$fk!sG+WRJ;+>%pBS#DWodb|+{4Q1fFR6> z6+GPNqX_&_676sbpOT$htWpXZNs}`_A@7GuP51z`(=ggJu6xMbQ-wFWscMRIU=G8*4&p*CVHPQ;a|Bw$%Vnl2hM|f|y z;q;1}Ji#Ro*q3Wu_BYEzkeIQz{oaqTo3__!k!YoNxdP1?d2DWGIsPGH?SK56T36Vk zVb1Z!3UBS;x?%qj=oKlZH@G5@gM&=EDG|7-fYG0*ePu2hx%ZE!+d0D4zdm_Jy$V1* zrIw)(yWd8KYFD0LJqE1HNiYHGmVvIxngKiCM|% z>1r!Kz56@ScC{H8`Y9i}XKf}QUSF(V0up9G<-9OIZ?EO&=?P2?*>@S$wc2mHP`%Tl z#ulx+T^j;M-+=YO0MXfj(`(Z>A3JOv2CZCdZ3Yz2=X;adCrE%ee_-Rky`9qsR1inw zGMIf!lWr>FF36HZv8(+kpObsG74-p&x&J7nvB?@P=B%>CkQ?|5<-p{lb-``J4bPS% zPB6@n2l$TZLCQYIyW2$UhOb_P0_638FU(WtPlWn%+S-bY1o}TKo30jC7C;r}G`Vzj z8`y#zuK_-$%I$Bb??0dV^Qq^Klh&W@{@-Vx<-afNe|~m<^xuH;KR+7&VR-!S^N#p0 zxcQ&I`uX(#{^|cM)BkOW|JeiocXIw4C;opICH5x-v;#4RLs5{@ZLCq**x=O#|IJ91Duenl3ZV1xff;!qLsG-YDm8 z44F(r>68okd?~fFb3fK7Qy;u?a>kZ2iUf$K^tJ_~@O84@W#Q@?WwTio_qde7!{=*d|DCdO&WsYB&`iO2Q*Eqc%U0bp5S3hOz-od{5Q( z3@^TiC7r}?t>vlPjFuJ59tPtuH5F8}4%n!wnMG&?#S=DwJapX`&Q(mmdf9&ofwc7v z(&zJHv1P*?sGS5OTW5kN@El9(WCOUKLph~ITtT>=ri(^ZO62}7g?JdFo2 zXz3%$mfZMkpCxF$Z*BVpqp}`X$5BL*bBs>i^_$5ga`xUCS-J^5TqI~tB{nwTsznYo z*LWbBkVjc`^ii2+gJC+Smkc?2T~{xC9PGGBAEsw|r-dm+zqZ_s zKxDhnvRbyt?WgvOeeL{B{gr!^#kKyjTmGzjm7`uJAzZdbXBZGmm~+*)+_Pc+B{rCQ zGweTjt6O*ZH`j%lrwdrcckHcwE`9lhM!^Rz-qm9uN!Zz={YxYp0hwr&lEVQQj@0>| zK4}JZ^zp0SOV7w)7C7b?c_Pi}m!BK9z+ZlHqh-g{h_oJnyo;PdFQqGts-))APUL@{ z6ih|bwRikR8BT&PuXzz#MUT>PW=q8L4XWUr}Pd)DMWE)$s4^2(zX@mfMF2%xiPGprq{3|1`DLReH1=#KhI=y*=2hRIHN4*|bW%$0(_-M3wr7umcM%O{PSTf;0-wpb|(6OAiVaP=_d)Gr*BA0r> zB9Do+5c>FM-wbd6ftVT{gwTO$mpDGr2Wdu&4IEbo@+5nD7~!O5uX8_)FI3q8zlcc$ zCh4N#(gBk%<=;@e)CGOq#|bQA(Z)KDDincHSPnv?ZS=_Z&?_plAfJ)iW^!RLCsSJH z0O)>dskUWk8!J(7h`;eH-)ej~FHH)HMwF1ZFN$HAJ3{C^=|aiTlR`n7*SrYle%YEr z;k&gCl?wi7Y;P}@N0yYmYzN` zBWPpIH$-ZwTf2Zj3h3c$Dm%@NNrxCNl%l)OWfEI zR;bY_EEBP;Pw0`>FVA|&`?cXfZ3Qvsl9L+G_!iBQz&nzVZ7Q!O7H|52Hew1tv%3q8 z5ZCJM7LQ;$oWu%`GXgh0$u_>P?>vFG3 z;l+!K3!-#>Q9I@lln_V|kh_!&_<)i*Atd;8yN9<2Ex_BX%BP)Y~vh;y1Hf3WvTF4!$tLyP>6W*?8$tS?xpS`J33n3 zV}8RlNIEK%rqg4Sthig=n}E@H$APX+p|j-Xkg7iUiqYZbQBe*rQ@s4_lFL^e z6|v=8s1kG528o*2E%T=;8o`e_S(MuBQzk(5D(4-XiRMFsSekj1k!Ch6?H=QvyDF~= z$vhfs(thTcR@Tg>ITA5{A#=!ciiMae`bEPj(*&@E-@B~|N8S}wLOL5FDq^Z1kM-1v z`aV{43q()0g3De#DeAA!>GRbL_-<TvMnwi~69nHxGAH;`QsnjUQv6iSZXzmvTC~zT@?<{{`Kwp^6 zrzYiT>N54D(nKVmz3-qCnwkoY3No`Jf@t(eW#wOwSd&+elJ}RM3CE0(bYte}=IYSJ zlJm`GIXZip=ibvr^8GTc9*!=(p62*Yz@>WkV&D~J^H5%;B2AYgdnv!N>ltAb=!;Ag zNTvcgxHDumFotHhnT*b^xhdmWKAL1k8h`x*RU)e!A@sH#81EkQQrq_Auz)wtfw|Q< z=_k)__0N<%=7yyVHi#bS(nk$mttQTH534Ni)@aAzk`p6}5qpmrCZd*4$2yObXVq`h zF=P_lIYqgB8EcY-o>&bktX)ead;6R02V2Isom?(yR*#CL$V7rn!L(iJuN-8{o|(#) zXR2IpfT0Vr(q}XDqo^WICGHSU2=S|Dr37~WY8=rfXha1^3tmGL(i30`XE!sAdXq#5 z1p|+SZxsgxj_ufSM(@2@>a-121Q;8;lGtaRE$U^{qq$9NGiOKShzRF0dyJyn(cu^= zu0D#FMTB^c7VM#W=#-+UASiorL?!048CJxZ>Qzmc_pU~z{cnXYOD={>(dXoqL^G{` z4=IgYbT!bCRkfebe8B#TMY{ctB~?hqViC{ks-a6!zbhmEz+B+*Ea_Fw!J-+5O$sWs3FS_$8TRp2Dix z#Xr$6)cBM%u|`Dci^N6UnaO(V!8yA78KkHlX)g$g8EgywIA8>fV$k(3&2pyqWx;EE zD(f}>ZCn>}&6_ar0=HpRE|2xS_JWQrWdJYAPa%qPbZRraaWfA}(~?zqp#POuHktp0 zF#hzfOz%8X0eb1o9(<$jGA%}?SIC~jCvZu9!;*3c)rYujp`tkK!&uKs--<)2aWj8* zgBCh`JSDnYl~!>Z@85WZt0=r^+q?(E==Ek8Hj2L-HVV$n>Y)&RyZjU6z?`juBut_O z&qwlSH2ti*3@v-yR&HyR;Kt=k$5}mmIeZg8qfxOaie~-Dh z5#Ds2&S0tw*8hRNkf@o^SbU$-dc9dN?vqds&&hAI@lOqlMiyOHDI0{1hA*PoI65A} z4F?SW{fma8KiMwBRMKN0w1_WL;F%w(?|B14{~qHC^QK#HRHX<~Bi`=~v=}@LhS_RO zZ|x3_iBM2l8xwxDSE8+tuO82tT(boy8T)_`YRC*CuDFt zXfD=S3ND+9A$fyOKehOa-bAT7y*Hz09*^wZ0Pmf>lsJuU%gTm$)OuNO&$THnP-xw?hOfyhF+UK)+V1*kxHv@BzO!pmXJa#6qtZnN*)7GJIOc zia5}_N>B7%yPp*YLx zCp7#4DfnUfy(~bIBxEU6AuWN}ZG^+pW4U}@S^t|iQ{S@>cKW#kU^7lmjbn>Z%=Ye= z&GL5h6wSYEU=1zGV?E7Gflk%8ZiL>85$an5$frEvKBO(Y3Ri z%&?r?8`_CW_tz8_)DPEqwh~m2&54xo_1#7$`H9xy6#@`GGR{ zAqQkh3oxxE!Myf=D37rC3)U*Xx$L&@B}rG3f*C}(uNHMSEtv||n;FlfLC#%)l4p2mJjdkp1)>Tg3GAG@m~+MW2!mq9)!z~w zM~pXE%D<|Z)9QcPLn+%PF#J zl$2SECQLy+Pjq+3Xb83Bqb`~cwMSf)1SrZ2wz8j0rsVUyZ*HPfIS~9HGr1?uVtORI z^j?kbA!6yplw&E2CN``_(gKn$%Te)Ue>?}YG?;8HHh)&lA6}zmZ-u-e=&Hb!qIN>W z6M2Gnhy%#*!$H%(c~|eBPF4`^Dh62XJ@~!fJ>PLrWy64A{5zA$7*DapMG709=z5UijI*jqZ%xZurS}QHU$7|0>JPi~ zqKJ&-RCuPgKBURX<7#coypHDnYZ2C9%@h2L9lMgsG^VuaR&vA@Z@%@DVGqRQ(U)DV zUlQW4&P8T4LwQog4`N<+J7C?sCvd>fa44)4r{C;)Equab(Ti{Y^^MW<5&T6jx%*K3 zXBEsV+$QjdzN1X(E$+xb1RYM>m$;8H0=UocL!fpQG2(ECkS8=m z#gTR|9C#{VLNAE3Ez>!4#U9O4?V-CQUI!E}%XHn4nw~G=N~eZgjw@1J(9b<}+P?nL3_Fvs ztqlGGF~t4gL~P^O(W6Yi`kDa8#NX}=Cw}ELz2*{1B8SX(L(wYDDj}yp`ebKD@UJ7m zQ~J6x2tRZxdk}QWL-mDP+enCrVGffmvNw3^Tb0zL`b2rYT<<#JN666kS=(tNvh?w{ z)Glr&UgJRDJa~Fa10=dyHvZbNx4B41-dP9K66^JCRaijnMxdwSks{)|4$iKyOy;z4 z^pWoVq?~SvEn0XG`}%7On1@`If8!=D#y`bOz?f)!z2#Xz^K5y z(|W1|5nt_z|0S;q&1%*MJvCIqUpbJy3>QI=tm+gE>vSo}B0S@5Y5Zi6y?JiveLI7cZy3iKQ)X6FahDbX7Uer7b zMKS*DxkxfGVrihoH8iDo(2_#qzVI$Lr4QYzQ&cBiC5pF%`5@G^X1LG!wD{k%NV)BX zpP7i5fRwen=^*S{^UqZb)G{3V`0;IR(>}Q3tqVHLL*){A74XXQ2kMh)!G}S^DlB%nY2%a6UzSs6B}AFY%NCQ*9dXKu~yJ9ifrpMGama{uUWfy znbjcW1d{@-cbRBki_cF+pI&TZYRGW1Pn%DJ6L>vVr3rr?QRyXdX?yPzM7Kuuh$8h1 z_9eJSU^R07PnVW?-wJ3r4No0JI#Dbr2C49{vcw}N6?-obD zPk`-Oj_2NLvhQL#fpOfs8)LnBFn`1Qcsh}&vnJ1gkt?XXwBG}I9ijo$EKKvaO6lg# zz`R%22(gBU_k%cdn1WQp?7*Bys2*_jZ*YeiqPkMeyma8vy3H%xb=dc)YIu+VbVxe} zjlaa}G)KanjC>)R!GqWS``eLg1JRY|yZID`o|Gf}h;jvkM-H!tV>F%xyr_)%)#D5C zEQl32qyH+vq>C;c6D6NAOxF^Wc!@2;Q^*ZbOu#z3iF7yXnl)?%Lu}lT zH&arl6lKmrQXq|7t@$qj^G3#}^LTJ~(6D|_YEqKH04sjaPsK(9gZC%XlUAMApE64C zE$T2Rg!@bh&Els(qId?;aXm_jnc-lq@PQf=`|{Qw?hdOGO0qL!zSS(k(=41zF}#UD zab-kB@xEwTe*fjF-m8w!ogt$$ugi8Ab4AK|I+ydIwC$H4DDC9q(ahj+{vuEK!Mh*O zhnoeW4G~!5g%GR0npqwr6z7@9wMvojEb^P)om-8*a{(-Yq#d#>x29u!;RE(g1s{Yo z)(W-HRFqv+zB)r$C^iNX|B^ejJ&OE$5MxJ^)(CP6NFR!+U@R#+HWCg>ze*_7A%@D} ze-zqsXa+ponWYIb<_t8fxa2+ZyHnGTt>y2y1=Gi47!o6`q_ zogG<@JVrsaxAR;v2_rkU5T)o7EQeP`7(R(dLhAo=zrrn>llT7ug)t!P#jmljf;Lf= z8aoLz`9if%tYQz%V^vnmeRxw2q`#DVk0fjJ>efQAdz~JE229VQUCq}su&xP=404!7 z%cJgjLHJgZ@?upy?JM5(f;~6%F4C1Ny!4x|H5{9Fuix)FgIZ-^vdaDo$KFE-0M|K3 z>n^gX_~+SG`__|ucu%|S956A7`g814}r=^pga2&qkksQ-P7vgq)n)iij5@-a&=< zO)&i=0lxdrQmJ3yViJsyZ;n2#qu_)yhb^h->RFa8)L6o7GQ)go^_V8niu#<eQ>5dMxWe4Lk2j<+(%X<(PUR ztF;q21A1+W)VNCKmB~w7Mr@_sR-kB%^cZk6-k*-j1uHb!pG&{m=kGS)A2POMa4JpH zG=zG+;hC#D7sH>q&1~vCAatz4;B1LfTEcIBOCWc}OG|^3K=z;L! zRLBzPlIG~Ks@f|UgQEc-qeXUQ8$TOr?HjFaRc=jac^P*O;oi45*a=2628 z!IFIFe-vxP8?s;y>DMQpcFTRa`8iWQ;Pip|3}+BL&PBZv^)v9FYHyhjj$>T8Y9fJV zZ}eE===Vb%XbhD!D|Z8V>Je70ngAm&J_yX+d*UYU8T`5_wi6L$0~=Fl{r9B2^aJ!kr&V4s4Wr zPjZ7V{V!;4aUDh8=xIwvKC55+-4wpLLxM@FNohMWe~lMxh%Q%+S8h$EtaNWjG7UXZ z9c?~8-I0|w(LrpOB_5S)X>7fngNeiby;bUoOO zzcM!wP)+?F&}W5*ew%;J`6~Ctdi@!0t(g53`$jcJ49N_H%Xl>TQ5)EhA- zUI-CGWnRI?*9n8PF=y;7l&EB%p^abO;WLB##FBWlA5XAGFM}h*R6vt-XPj}0pnOGv z?tIF9MSotrK@9F%(lgU9I06+wy9s)47VAD!O%vP=t^9^so$RRhm<#-}@I+e3eRuIy z4a*7d<7-S%23w93Byhj3;?!YH>R#-u7x_r(Rv+?9>1PTva38|fs zTO0W&zZW8;%A%FXS?$eF7um4h2IetTw^UR=(4J!&2zcII%cJH7Um|6VD{4adkw6?Y zy+RYDY`{YFJERVGAQ>hUI<+H3`mxTANzrTFyBouxHCiUYnh?*=B-<`ad#GrgJIATM zpQg4f1>YeaX}-B)kN5|RdaR6X$#BQ~(;@y!Ej+s&mc-*33epbR2MW87h}fq&#(F8JovQQ+NhPaGfz-h%|FDO7n6xt>Ef)igzjjf zax(tmJluiJpz@6HXAXVuUXPLXS%z<8S0{p3tM!`~RniJmrd!KW1FQV9$=}K(>ywX; z_=z0a?_<4$V5t!IAWpLT9%!%~`19}!rGKn+EZ3?FQIyFX^5S-_NbU86q+p@d&_Ri% zjV;LPRc{CxL0)M`5uGo@rarxM$C83fk{k<@r@GxF$;P|(tqpI2!@`v_pE|YzkFkGV zm7KV&?yyO6C>dN8ruJ zOz0XI)q)}C^sFQms@@&ED-tEH^{&wmac1Uvk8sIrDxSdrl(Mz+0)}-+UE~G-Ez0uH3w^r_8uS5^_d#^ z;RmK3(;dt$>cIF8oiad1tWVJ1{lBpl?MA7%=JSWv)>!zq z!RRsOJW}Jb7skO8X%*jSnp?+bjZAypj5#>HCLi9R-^cPPQt%G-td2qUHPU4ksy@wz zyXN}8)#e_&Hp56BrKNQk*H&Rv6Zh9ar)_fV=R+Sl6W?>Gh;0rwgLE9^UJ1U3WH=ia zh&@WcN!gH!*OkING5(q;ugiK~{?z3T-e|N&>U9c_s+T~Y;<=$3LC+TsA4Fet-reC^SLQA_P>btO zw(odWqC#J$o8ygzgahxcWx|dwW_?c_Ktu7Z-ZTjK*n9|zq|GwWu}<6RWw(O0CHl&? zTapv$GaPCLHSyN3!FuWN90E7w-mZnbS;c>9y70j9TClLm; zkzZNLs2i+M#4i{Ef7*$;zh;;OaADa^nDC_fD=OH2V^JW`XP^|Kdt4-+fG`W|d98H2 z0LVKihoPNRYH@tnljbz2W4SC}vJrUM=sSGgeuimnQ?Ze7L!~lO=qI|Kh{|^m&8c?@ z=EjsJ%)*3SoU6o$CtL-)c%#V$>WP@PZp%8p6=M}id6>OZQ7z#igw>Ctd-+MJ9MnDx z$O(ldgdUrgnYD&mZo?)O4f1Ynn}ku}B}b1+#@?T;u7hzm<@PL{*<|RLRj=h2%(@cM zr|2;m>21v$A%5r(eBtp@YmO9yc-9uW#iN*yt`porqAev9m%WofDPpDV-sD zv;yC&le7gr(mP|L!5?o4v_|DJheb^YNR~og)cqT;)jKoIjS}!H7l$gWqh$B0pMRneST~LE1^Gy)l zeir(TI$uQUz|>vofhZL!tJp3?N0g3$)`%CrUR+up>YSyGE!FzgfU+{GPheHXk%u{s zZUL2HoB83f_a?@ZOp>@GiQ5!HjevnSft=y@3RmAImPJkHvCin#HjKe4CMNvV>sHR$ z?q610GNWcpaz@HEqXwp?-ZbX8i-IP--r(XrA0yx7I|Y*%URaWM8Q!nTLxV zNAEFgjgJUk2Qx)vCE2^zUTdR>6aWNnYe2(Q$p(UqM{18WtOa688jUCzWR=mxIr1j+ zil~v=aOvJDV_EV<;xrLqJC;qRS@IUfke8Ovem1agIu?s7$M>;x>I?gB>&P2H2$K12 zAi{&bk{0TQR_fWAq~wk+9zmaW`0P_BF$UFUgdcrjW!?J3StC~x3yA!PvrvPlx{OVQ zc&~4bb@Grw@A*2IINkLey|bdm*rhl#Z=gjig6R6amk<*9uIx@Ep6qVWqmt|G=!U69 zQe)%KF!{Ig3cPpUZnx5q)E~4Z@~HDFTboo{33HRWJvw^%)zI2Rv<_xX@EVm)$8j0? zU0>IeeJKC8Qie|W{C2{H2aWg}~n(QdZH7c`)2V zOaTwc40lP8ldGWO6pK&$oyai|sxC`|8}V!~nlXsZ&K8gH%M^aui?hiQJ!X4Gnr@L- zaivzixR0%tl1L0RDB_Bxo+T-p!&uESWO`pG2vur%unhpoj2r4MN=B~vQ;!|}IUfCJU7w6bB=V&u zzpF~rYq?hzY;MX@G3D*Yu&r~axLv2So-UWSVHPO2sU{`qo)UTP{m}kQIfFL3{2jtH zj1aB>!L=iTw##E#QBw337n@>vnmSE^6TIL;loAjB0bYAn5+$Y=xb>Ynh(*X=c%Za& z-sW^NI!P(LLUir37P}T?Xg!L`#M>W>YSib0p#qd$wmN<^lXJB6ElC9`?BAL)f5-Z}8z`qqU#ASE5rDIg&r-6bF(QX(bY-3*-q0wN{dARr9gozhZ6 zGc?lOG34;=-@W&o^PTD2v=)#`?8}xTqNF zc%=QRVkg>(qaci?-SNG523;APrkTY*l%ysU<-o!IN*Oma;@j{< z?2orBA8Cv5(s~x)ja_P*F)_bHobMJ03DnH0s{0nYEz-bmz-ip{Z}`!=rzsfUunCsu zi?b4;cCY-H@gKKQnz9>p>+#gcn|D z*^G>ArThe#!8IPkinaLMkK6o3V6Bh%$`vl0+nKva}~pb-7O z;M+1AY3yRQkM(iR$*+ZfAw=+3S*J8V8PTr!O$mfYCCk^!UPUl!s2bJm0|45YuiAQT zbxwf^O6CGY%#mrK<_o2sF(W$Z!-a{Y;$zeBKr&X+AJ%0bdhlkMM)# zO0^2+Uhi+q1;mPr@=m^t;)Ujg%I@d2kdvH*?YaPG|H#xGLB$eCLVXLlB^jTRuQNE# zJgU7`U7@MJ7#9~XKJWdW6BS&rTPKLjK3$L7Sa;sG9{8T4Q%&5<>Pg0+!v2Z2z#3|? zrW9Ew8%GnhT1TV{(O?xZ%K`L;1y2%@GL?iEIj~6JFxt)GzI3N8uEOSP>7mETIY`v`Rran6cZ=6MmEu2@FVlGC z1R_I6EZGL%DbbtmrN!h!jXls6wykx}e921$E06ch-Y*94a!N_o3YS&9i1;Rt_9cUL zIL_U;UrJ?g-Vx~oXWvRgi+}w7`QM~=aeHwkO9lFVp)|5M^98nLwMl0C>a^Zb`T-be zB9?&wk4{*6DOYY?BzYc3$;SF{Q@Ya7zE}0Aun^MTPJ7v={r4r17cVTQKSrAFAGY zai-tmqo%h)!v0knppkL|+&O8i)nD-N@ez5}%~$k1^L1>gNAKQs38X)=_=|ld>8x-6 zAHvH2{?X}b$Zvm0&6?odF6i#j=iL5y7m$wFOuAq8A=T|QI_U8J`=5V#5V4_FTFv1VvO@i#Hv78@3~>_L#&^Pc=f=Vj&~Ar`@~?6r5IyiBHWq{; zBt%jJT7%MuTRKjYUDJIS{QbF-9wx3r|99)&Kjj~ofq!~7U7klPKSet5@ix<;HJ<~# zdvCS}pO~)lSv>xSFY~|GUrA8VeN3;Oe8Neg=XG8|j$>z5#a*JrYNX|ga!1QiNzcZI| zQ;cqDOD#pYp7l`u<&pF2jD@V3e-#8yoC?2mKGEW5+iktro60Y;r&NWlxae6?F3q=c0N0SXhJczSC7=N3EwkYr;ePQu>f8Xtnfw)pV4#7)Ug zSh+pELo#ZJ>A%Zb17%b8EiOleF9#Mq_{NFh+U zcRO2I)_W&B$O$JTOc|(x#wO{o|5D zvJ%UwO6KuR@L+Ar|9=Zr20-)HmEu(P`DRG-L9z1)DC;HQCu0^q863`fcyL_u?|T5* zGXwQC^x(PI*TuJ`hKb(N`dak0@Y^RbX*2YxefWP>!QwMyxQgy0TYCV$f8bT1Ps??T z-OBqw0|iOycNU%h)eIUqP>!j!TEjZTawNp!KJ0jSYI+FFfcM}VHVd-Tbrt`0yXADw zCmIzbLZNo#2huilgJV-+cf-`8M{|EjWz@Zas?wX45K!^eNgv+Vx!Xb;j3 zb*Cm2f?u^dr=%QP*DHMT6YfVm8<~yLcyET5DGxuL*c`%lu zr^|9VISb*cKPSC`sxZ(VrAqgrdgQ10|3=|a4lWsXb zKe0L>Vy>IAuA4&2_LdDOlZHxkRl$*cZCD-h$Xy@D^2Ac6CYFhjzL+nxbxfZBV^CZs z-uqDn%tvl#%X(WTxP%TjYH;c{=*Bb7290U!B*PcL#%fZI+S)__J?!t54;x!4z>c%f zfesD1wJz(Vr7as2KP89Bse%>l0IG|!%a*xk-i6GXGZ&fboWo*;BZvUNFv1V9IJeC$ z00tc?Dg5EIy^(`N(No)&($@Nt&nMknM@1EEWnt;&>7M5&(J6_~m8PT;ZV)fRon1eA z0A4u4$O3tov>K;2U0JqECFIbnl2`aFY8FM4NU9u27QTd!|2$}I6>u5ryq%Yf)b{Y? zXh;e_>A0>63HygQO_xSoD!BCcXL#8lQl)CXjJD&>*-#~UtrUx-((m?&Q^at=0cQ9O zAfvUQF?%cPc=eF1j7yuOHD{Fx!T74aH6Jb;@RrjKJUsxcIRxT9XQ*WVBTH|@VwqO! zvV{3q*FT)G_()E`$g_7Wg3zlO>7=?>S9Zu+Hkp3P1c1@!Ug>ZOo|~q8+^M$Xi`&lI zwUjWl*ip+!#7uw0HzI(n$>2hN?xl+hSV{-Kw3vWx=={xq9)hDTfxD`K~ay+x-k`tM?b{Z@_NYK)3Y5o3@mZ6grzw%6UE)R>eRSM?a|5( zHhq^Vw1W#_;1u+%%$32V4saHn1%qw&9&`fd^~n*l{SP$$!Cm8T$eSALy#w#u&zDdm z8#J`SyI(>F+0)K1TyaH==k}PXGd2x`k9SsKPOp{Sg@p-RQ~;cmToe(3EG`oO0=+t# zxwuptaBt9}6ij&(wp%ITKRDQ@Tv%u$iJZ?$pDb+}knlZn>CHudwR)Z5(m8T(rC?^3 z&|^Z`w)o{@j4CO67HZRJ`PhHfsp5EX7luvawxBTJ*3bYN=fDSqw-f0Kd}l8g_)1$3 z&S+QczjfeCtb+-yV8Vh2nb!Sr&~Jb8JY$qvPg>sgpf zI}h!rGmVn-O~s4=R*%fi?t+k^b#&XtF%3Spo)ehidXJ-xy=aHm z*~^&l0yi8omRei<>oR@#&HI{xG%z9n&<+8*5EFM73`#NdF0UwwQu2sr#01Cpyo)PJCfIq$ zlUB~k$|@6n=nbQ8%=XwXdQ;gmX)!!JGz>H^*bdwAyjg5l$kjnprb;9)ha_j^gxT=u zsEIu(l|Yl%{1B<(lkxIhG%IUG6?=H_OVbgPTK>j5s8Q1# zV${UQ#*p>RR2yw{E7B@ng`l?jxT;s7#<{lZzOm#kK|EHb6??zVAI zQaROcV%MPw-ES=h9=Ta%Ez5&@N|B@z2|rLL{YY@FQ-THBx7;|@@-D!2WLus4@|2MhfZQt z{8w>6EqyBR@B|M1uyk7|b6U7rpU_Y-TD-bL}!rxESAYRcTS zuS!dfR>w&6`wXfr>&=8llH>qT+yiY6&ixsfZN1AIpvPsTX8?2@KynCa22R=cSr`KT zxwBAG=+k{pijh}?FWZBYmp_8fvt-6LiE(yek|bi}O8*UH)N+Q?$>BnT`Z_AA?k(5u zm5*?2jGfilTsDDA%9KfyH+>*99Ud4fo)f{i0kgO|!bVO7(7Z-8op~a?E*}9W!Wex>Q z@y~+(9zVMW^q|gp@sr<`Us;GpVh%NEEZ_eDb4p$r(;BoaZ(q$HH3YQ#>Xy$YCkxPT zaKC&5=`lI!)y@*yQEfln#_N2T$minZ=MUxHpQ)9%lxvavyQ=@m5H_9~w{2MGc58qq zc6-PGzdR_#;p@0RYkVFEIKn+?tH#F7aiUI$dhFEkq;s46 zL4n6W_)fnk`PfH5%lnP5_j-@Yq&7mR4;NV7w=8WI66Jr940*#Lez2Fb+JY#0 zhPLeqE+9|Xa;C%oo5?E&G&4+8e~HeXwLe2chf}Sd>gpGOHF!K%nxF)6dU`WWT8RsGDknbu2#0dTMPfX=^+A9rR8^UA-su^6xXazFU{~I!HI^Yiy zGmU7CqHc9asl9#sX2B{~Kp_x~j=jh0tDw*jCS@VBw7J&2J z*#S3!ObH6!^-d@2rM{#)mR9X&$B<^>`O;bYRPn$ylWuG-O2wg)61#Ge3m19nLXf+b2Za;xmAO_l-f$4}O%=xay%O>n$t94*AuTYVPwecx{!GCh#~0 z#8f*Qs*-{5M?ArlM0?pQK;444%Q*1^oR5G$3fe6j*_emY-e1VnDQPT_@kzbWauaYJ zDc}wZw3e&$_A{y(=m|P?tsj;F;>!hI6=4;nMdD}o@aFY(Fon3Xj!qiy&I2$WGIZ#! z{(U~UmMxAswRGk^cYn1f`wq|+Ce75RPwrDv82RoH(I+P)xli#ggEOq|TTc?w*c1;U z!IS3D8S-kWbDHF>ZftF{(x+|DLZ8;NF5S!r2M1Z%P{3SgH@D)8v*^3qagPo7YzRD; zJTB@KI&ma7|DK;?(hrRc6_v9;J(N1-QM&i>dU;)%pp`ufLY+En1b!cYLSrFx>H>Pg zJ{A$R!{+9id`1^gpV*h`yumy zscXkpeEInKhil4;HQkjRSg2|ikNod@A?bT+8b8v~x`Zn$il}QPv$lX zxW%dctP~HJxg0FE&}rKPyUzwFDsCUZs^z-vK9ifpKzH)?Zm!)K(Q9<{e?mOQzd!&S z->CazJ5zymgsS9I4Ggs}t9&JPBQOAnzW*Hl`nY)X)ap66V!-vWuDH4H1nV>V_Q>UB z`CMDq)Cb-f)Ah^57^9G^+<2rbd$U4o4O1Ykj}&RyXJ9|!ZQ z3LAP49$bBhvdP8)fL#t+rDG>f7q@(}?GLl^{PpfirZ6LKH4iPzlsZ#%!2m>9&W_V= zQbtLZ#QQ55dq;A5l^4myENm8tbdD25$KiGYeC1d(hC8jzurk2K(z4&zsZ+wQSIwc^ z0XKYk{r%&`7+@I-z#;&?8TuZ5{D9vU&=I@4yQ9;({Z*91Gr&N3Mr!10A={S>*vtUD zGgaVHwW|2ppA@6O`z0{v_STN$g8rw^pWpUgoV${6==e+)@d}>-rlQh{wui8TirEwr z-0&I=E5nF&8 z!^hw74Hz;kTou@v;n;QzV1QvMKd5&6M(>n4MmbD$R{VdQ5xHgI4txn{hd?tKhSx}z zkC2=)IN65*EFgcua3S9*AOC&GOVjCmu1@VSng}h*>CTg~=BHG~;_`ZBeL$CpGrVLU zf7m@LtzfDZDZAZPuI!sWn$@`rF=8gD4!K^uFH=|DEztt7$RYotqod{umr8@R6l8Vo z>I~R;DEJ7whItg-hbY-XR>$fLd?y7eEk*!YODE}@k-@>_2yK`Kt;=*=ih-+3{1w2) zOMbpF;Gnzu&YT`I=EZkE{4q0=sz%871u;J#UrQ#myvRg1J!1rF*zS{*m~81b zp)S6-;Af&EK%@$W*!OKmM=R%k{IgZ(X}i6(iOSRe5D>V!(tbXjP#ZbIH1#FGUoZvw zgG{f{rS>L%#&#=nxQ6}Brams33`wmHQSXz4&E2%5715bljL_j90(4IWA`j=xDfxut zMGmF}MuJ%A&Wn7CIh7%*hiVS+RHl)#k^5d;$TGCzR$6A~6_wEnMD-*DSK@DqQ9z^U zp+IXrpc|sX%}`=Wnc6zz8%>oMT!Wqq@RxsX-megKe2hrf5KfMDnPm8*eK{Dre3JZKyL(oT&NGXVH;>-9|rnxQj+G5 zq0rqJE0H?SIh98%V>Q4fOJdXJNEJ0(r%ebTfAY5s>)I5HB;h3YpVw3*h4P9{0(lMK zK}}P_iN@(#I0dNY9XFR3yko-mM}mfnHM-&9M zoYwU;mDJGD(WZL7tbRr_1l`jd?V=(BQXToh*48nZ(NZ8oT|F=8zJIiUIB5om(3zQQ zxgh%N-1Ll3xub5-EaUPD^bCH6mn~Ky(0DU?B%gN~%y=3@+2mg!yTxeJM$8 z+CN^(;h{zpWF!mABRhKqs1i^{u_^D%1o?+q6f)#oJShs zo`#WvGwdrG85LW_4;^zaxy+A zMer^U>)U$(@VN;`?>5nC)^O3my?GY58hpoUDl~s|{gK7(%3Tm4Y?02!0V%Ja*|}&| zR|kIK(j zr=+N=3inDsZhK5N=?Mc29Bb4kKK*llt{vq8M7{kRRB<_P}P9;Ygifk4PftWZCO46Q-h>t1KRe**l58#I{*Tg zgWqD+LwF{atBE3Q?s&U-vQt2-+d|iaVaTq*oY2b zF>)3*0Alw(abJCBgtvY&Kv9m7)Z#x-`*i>`%7a;_BH(-0tREgLIDi*^O6Rr% zD=4U$-)xZ<=*1MHceubof674uqZYs~&?%AD6BzsI4>^RBP1W_^JPX4E@OLCC=>nw<6)TqY z*C&*n>%bK9ab*I8mx>zRG% zZCI7MBW?W*DKLk|-|Zb0F~d9rs1FE0J&B~iY`*#-vxT1Rqy`Wkh-EV+3??QdosBj< z-w4dmH&7={Lj;pC1pclz-6$qGx85oWpRXc729-N|; zM^(bJ0MZMpPE|qu(IPF=NGku+@nVIz@ha}9P)DD7r)Of<)fK09;&5-G?w8xUQ(qTo#T{A+H zUuwUjpvLw1;dakvFDg#*@7O3*AT59IyD0xvYQ9_tAZf9&*>i04g)VuQmzTfM_TxF@ zx0`*V+tz#J0R~cFk-gqs>}7zd{h+{1qxufE>~*j-4>s`6taq;;paD+<=A^V=d6^f+ zT-NbQfJd`?D@BgBM^DQzV4fI;xP?XK_LZ^ONTVf+o+u~Ylw%Kld59X8pBMRQr( z!y&Iq;Ko%7uc@k5(_P8%SiA9Me{=@mMf#`V@8+w%0aPC*;WT*%^y`^GCWc2Ia5_NS zHq~jg`Uh(@K=3&IzzM+H0xapYX5r!F@}lD6!NEbBPMByR@V>)yqJhA*#BTph>#6-C ziaIb#$t7kboh{N%0gEt`+clWL3QRm>e=4vrm7$yf8NIW+UKKO|4+4_y<+DdzN|Bo` zU&nx}@>mN6gzKRy_{mVrZEX2a09X)veH(ZU73nB0jo#QtSWut za3?x;D{esyo8%z?eSAU9Z$F;5lMa_5d5-Gu<91*9>tP`}%KgcalIN$VqtwLlqx^GZ zw+M{L&U`40EwMB}Czh^A%uKw&1Iil;-#d+E8Wbdla-ura;BT*xMr!v9d8 zJhm}(kfi5HJ$`_*Ot=yJ?D6rj4aaBWs4aM;j7l{voVZ8gZvDP8xqTTp7qaJ^Wmq|} zf8%GUsk|JWp+hpVkgsR@v$HZMCswz-IV&MhlzDY^K+lS+t@ZGLw)3`0@m=avVz*34 zUh6x<);M57Y!=a4Z)a*hELR9f%ENP>YIM0Nz7u}oN*mc`3T{<;?d=1-9P7L<{#msd zGS*?nL-x8}s}zQJxCmaKL*uBdD?5+v6l7qb*gR=GHY|c%qFLr&9}B=6ga?D`jKU1bW}9(?EIUF3`LH3zB^@^KIh4pFCB#3@-9qV z!rye5EPvvUIS;nnM&KfxC9$NpX?OhZ+dxUD$eHvMqs(L%#r2nR1eX_1f$J{KJ!tiQ z2IV|{A3K|Ni?)m^N{Y%VN-74Sqv#;~FMq4%{b*9?ejnOw&}P^}rr1Ho*Ow7M>vuiJ z6trJywp%r6?<$%)Xr?+2_O7MO5P!HixJ%a+2L0``+VS#PX?NP$CS{$m6^W)HA!KnK zSSCXbsuPTD`x_%Of7a1Ef7XusmgR5t=VM2x48CP#5*-W1CndCJQp-<{T&dVQQur8~ zQXzfjZtIT;qo#Q*OZ{PQ>9Wv*^S*>3Q#tJ~I(DYEn}Copkm)?%dFws;iuxVf;^a># zK_)`bj1lrK$bVnVG; zN~+=SWz@*|`FG2_r>i=?n71HM*VaXCjbo0Od;fLQkvI=obPy84#ie>voj-#^Hw#ya zpk7V}GC?B7Z#VQcF52>#ER0&j;||t|kReDMwyZ=rJjNPv+AUms%%5P@v=zxJ)F^J& zhi*(UE7J(Jaa$z}wmqFjtUj%btW|N44C6_b>ou>-h%Dc>ir5^UDr#2@{)!iAK0k`> z3|LE>Uv6FDK`ivRHv8dB0D^E?lv$t1qz^>6wcQAwaQT^)G5G(XA95VvKjm z4`a`KdbiQJhbLL7Zdb=g(Qk3GCNJ@!i)v#-|G&NUSvMvU44&Fcv z>lH=-B|76(ChtAXR4UT5JLXqRmOI;n=AUW@-R>^u3xn^Ar>tz0NWSNN2t7a3;g#hO zArxF@wZDp$&pZ3T_eV^8XEbZ-@b=s3IAGQ_Y;yw=JkS@Gq>_G!oeG)|?MDf}{YS^}rV|47E2{+O|lM6Aw*{t^=$sY3DI~MdvuHc`WqnK{3Ij zmh-mSHg4%33K~qwmS2r7Tl0uN9%404R=S`x7$M;-6uxIXzq;z;(q|_Ld_027i?v|0vxCI11tM>c4wUb zgwbJ;L|y@}7v9#0$m%myN#puHY8lvi=UE)ic%cRDn?88-g$Rw7IE`(RtM^wvS}A&2 zJ6pJq4zOu4^~<88-{rID8mhVTUNq?Ensbx)pj{2Ju|Nko*Ik4;Bo$-HxxIFOcJ#g4 zA`4SsG|gd}J<9MnY;_fn{vBbHASXz%OeN}m3=E&rAK$SPy3E;!e)7IvPdXneNn(>o zRG_Yp_W%3iRG2J`2%g$c_Z#R;b(kxKe;Al}nbzDFAXt)ton`~7hD~0B3v^9pK*p(? zmE5y^bAOwQkvDaqab7~JRe_6%Z7uR+&^U^U`(b}ek*7GZXCu4SIroee__0@4B_A2P zxuR4cv9zKsTP@#0PSVBfMSq&{qz4BhKRzDX&)nSJrgD%h>MhiL>S$_ump_AniacTq ztlzf#EOaW}T^#{c@rH_d&R)gA!J%DxZDYAj6xoscD++o&)xpa+ulHgjmRP}bLNObu zzK9ybv&S(j&O#rb_b*=0peq_V9!e~{h}P0p)6CZ>*1)*C%d>*0*duW9sHb7W>MfV~ zoZe!Nnwq0=L{{4)sY;etY%s@w%Ud{Few+&AqA9bZJ)S{2M9kt-d)H?*9sesi9~YbE z59Q*4x4IrGT~8#VPQoFbm16@Z?5wP%<9nA%O!+po-}lp7j9>_PQ1{|6cp?~LkM~+L z9WMDOaN&{tW9%wfA(}~D$6_W$@tAyg&h!oF(=Z{+P(DhD`7)+g&4nQF)L>M=u zl}k-tTOv~UC{o5kK^r$0s*)1!OMg%++d@ppQiiwZE}l%d(>NSV&Lcm}IxWS}zkmOE zyjB%jBVIJKH;br^mXmpWd@$7GTZJuN%CYgJ z0m7eW0jN~`YHuk72yIx(C!{)9?FA@1)n&r?gu_0o>xE}T0d=$Qwz6W#Vc|w86(nUZ z5KaetAx1301`f;ZaSgp6eIob0K%mGz(`uatWmFWYA_TF03T@z{-B_2Igkd(n`&DI! zkdyP%ibs!-{Vdklh+6TCX`HjM(fw%AhFPl;8NdBhOuL^1TvwNgysN#Y#PT+!6d%Q; zc3ui8fK%+k3-;9)3NM;Yzj|_5ZGTE_OtHU!`XeEA@amo=SNs03m~lEd*mOmvi_8g} zl$mtiHCa3kNoxf6Nv~b90=|1PeYVOc%b63|R~B|p%L}dY+QkDh^}^^uV&-D!!M^e| z0V#4pYdJx4Hh&jr@#6m;i-LgGF3lXQc!H$qh|cj{EK@Q~H!Ir~#@e2{5%umR{-2N- z6)XHx(Fy?~zIwK`>h{^D>^n3Z+#&4|ojFTh!eyoMP&TwGGjkh8eHlk*`e zc0TatouL$W#wbq)2Oq_dDNo3=$3i7C@utAa^-0&zPqO$Z;iF8=?aKJQa681bjk_Du zM$*oX%khg#{f=}kL!D5%KSaDt?ut>Uppi5Q)(frUJ#b3J%Aav^Y%yq0t^bN(zc$l4 z1&0>YKvi*brLY)>&~HO3vA?oh2x`WQ#(d)`z48V0<>$mMQ$n-h8>eX;pqZBvnm<{p^RJXHjvo}e5nK5lo6^AdiS#*YWR5T9K!iAYjF0bN!;uJ=sdHe& z=l9z)N5amBhqr8GDp8ceO)d$)t$7UETybRMZsv-tvQu$Er)SrG`)4*e0qrd5(Th}X4)bv3!m7{0tCY6e)d%b8P)+R9tJtr zPGACK@;_rI%A^NbzPeH%ExHJUwOfzc)c<&)1t_`i({WA`TF=EDF*oRCRrO4(~R z31398de`GasG>+#OxI64{`5aZMQ_yNKp?aPVb|WOpTFP66-mwK<`xn6)Hp5!vzFLs zGLGgp}G_=yiq`T6%B7*A{^tqRwdf^4-VSfC|W4!F@p1~4GjZiEr=ug_{gFR zdFFp29zk!N^VXb#?(XhZ+A~Xa-~2t=8Z_r2Kv651rYKjTcRY4JBE!Rb9@)nTGUcLh zp<-S&GXL0_9ddV7_D|APtX@3LPBJwH6J;#wdcCAztK zI%^$-!5|Lo!HmR4BDh#E-MjKntF@6%vF~$mhZ~+_fZi5s$jQl#_`1eSSfdE2dQn+B^17CHxdw6^6NP>`lNP=_oy6!nlvQ zA4daC!$KMgRL9ahgs;N7tV^_6oPJs1kQ;Ymcd~9J&2*NpqJRj1NrK(x)+{HsQd`^o zO(nE^)iK9tyzAqM(N$RK&nmvkpQ%e&p+G|U?8VYzPOq@>pYXSUE02Pqo z`Yth=V z(adJxt94Or_lqxRdoctInh$-}N0y8xX!F9m1o_kcG$ zl9S!*DXU=GXiIz-M=qobIQ{bCRF|y;6=Y0isVTwpK9=GNr#k7C)Yk%}R60+aiB%x#Z0LoJ|Crk6L0b?On>Jp$oWo5t4m#9-tSi1|U zcqxddYXLW8DiK>cvp2mD^@O=`a>ghufL|Gv^FD7LVM60Uac%6T{?gjQZaU>$PG(YP z%o(k`AC*X1R9ce#G}6W?VX(b$%9_CB*RgIFGRQ&_S@Fvd6$IFit(IF}5k@3pTDZD4 z20T&~RSOuABir~76rK5f;ZpSTa6t1y8p^sKj$zTFKUP*cDHjyr%L|nGI2P;GN6B8Cw3tPr-GRm%ux6 z^KQqotT!b?!vK`=wYqqa_xv$7lUY}13rYW_sIV+1?#mb{; z19VG274Nw%tdnl@I}nv?*f>WS^Q1|v7)Il{&ava+DR>y>eky0;6$Vl-b{h6AxD4k{iBpT6>8YiGo~!$j%OmG#2EiKm~GO0=8Tddl0K^u`La@MSR+*h&=A39a!ziPGDW8hvKTSRi%a)&= zQh-1_vRIJOlptCzGSUSxkTvt&zkOe@cD~!R+R)$z`_VAap>!sg4pfj^$K>F;$0rgp z*vqxF;6s7OqJ~(9F+((1+_qbBnNgYsRKq}U%rgv7=*&K9K4BOWnuqQR2=t=W^G&zx3f zCMKpK8G1lE!Ggn2t}Gv$RVDiHXJUg27hUROv@Q`1&1(Wd%wuH%d0m62<9XCyIC=M> zY_JZ)tJ3!@MdhVA0xly!4B;xWc^Ep)oUO+LZ+^wraX&Bh*~CSsTptPa_ri-Ojfj_< zDMSh~g%29BjD)7`mCEH8OzpA+SG`{nXgn5iUK?@;eTy&)gLgF6hW43uJ*Sew7kSJM zSWse0pmQ!Q=(#75ro(^zwyWJA|1M;(Gw2rb_Ee_i!#xg4f=Y-~Cz2tAY#+$f|F@NR zycJ?UCRJsiLC#ac{kWOkP&6t-@P+QHK8~9lbVoJ6I_5jcVuZJ<1QMv~GdqxS+}?Ly zLGPa)?sJy^j7dD%_JlAo2_xt%LfNzU5xtP6D+bIn%hb$YG_RQzz_N^O!p z>y6mZrJR}j9yGAAMfyTWze~+s)CIoh+fCoo)5`3b02@^MGGtyOIa@J2IBF+wJ?4He zmMwa+xAh!kGR-Q7ODWgd`f6TD((}hN7blA8rZn&S1kONno?Xl&pUF9bCtn+hG_`D= z;uULYwacZg1B-H4Dy^7Lx}3}?iQ<0{jPuDxE88$2&?2KMrZS$kqTNmCe-%YiY7B?Lg@W0BH=jOV-X;vZp zXT9J{aBHj7$8pc+pEHqKBp2t4>m<3yg2~D0T%NzRhK9WSzZUc38Xf0i>+ykyg8Cb= z27yn;xDqJ%ZMwTsy{VT1j`6sV5fp-VGCO2`G~863za!4ySjIvtg)b31w&j}bYcjD8 zNHT$*_Mrd`G(EUA5R;R7)vJEuUpF#5964xC5selpbr02Z`ccc|sXibr=q_xm*GdJYeX543YE=|_Sujw*`lgDXCK!Szvxle%`f7&Q8%IbO0G{ps!Ze;@V zZ_B=?O^^|p(kaypp?;g{K%ecS(M9;g-X$>A&1|LONdEUFL2X)D&3OrMSg0QzD^gLJ zsrKU^xSc8O8CSKR$|{V>vr=of^o2+;-4EYs4?Rs$WDvbJ<##)NRqU8svoj0|o2SS7 ze`)~i|Q2aV{%hMz-BFvao=Cdbl>jNKdVU&uiNnY=* zSxU4@UuAbpK}4&RJ^ccmCwj!0J5Pb7OW+greR3O``OrU%^&U5BkC29zI)VrV)C*$^ ztaUi&H@moQ-{CY$@_DbsH1#70zGU^O64H5n4B_N_yt!;1j>e98Wfa&$SV4F@5H?^&*nAl})=W1H}QYcdA({zs<)HN4OU|E`+ zpZ}jQ{hw$ZgahAtJS*8tc}#xPKAqJ4{fog*p!?@Dl$T{fxxAqE=LF8=`JX;>#>mJV zl6iQN8TmCFj?9B^g`0B;)(9hdjkzieFD?o>^m?SF-_ff%7#KJjW)E89p2DrTcA4^v zYwDcW^0WtH$Yp&{8?Cn-t$i_o5!zdwIRbf4U+*~gcw@kONivtbKFZww}&V#NREJwn^MYqv@FMJ z-CFQ@WZAt z`FAI!U}#tvFgq6v!bY3=H5KJP=giN}{sM$8h5tfIT#Rwm@2Mx*VX>a#NRd+ley!)k z7xog`(EV-pafUWW-0Wi3p|!yYH{u)6AqF|wGr12UfZcT8Wb^XY&U7ZjIa zcj@Pqo%b)hvj;_=>$g2^62Itde9+4uWF*9FqVjm}{2R!uagOVJ8L4OB` zQ{tq5R@yd87-)kJHX?Pb=rn?j0#9mn)Cb&^qiS!}w}@ZR3b~q|bP;{RbCI>U`XV8I zf7#T!rfZ;n-{&c*sRiTNtvg)m{ta3k|BI%cKfn|%?y)bO)n-UfSiW5KO0>~FNuS(O#>avCqylOW)Fucn66hEDZFRhbq>*CD2+6x zzAe?CcHGft4xH?pnPt<^w^r8Khn(zBQ1-b>Z$ieuSH#CGMUvfNu zcE)}gbC*Ek~RpsCIt09ACr=$5LFcdIXrIl$j z@zPKli>H3KAlcY*)h|XRi#eUIdxK>Ci`LBf|55dpQBg+i{_xNp(hUOAf*{=>ASK=1 z-Q6i6-60I!-Q9>ZNJ)2hGc@n*Ip_bJ^M0H)vu53~_f@~xSe510SP6WWcUQbE_Q3F8 zenql#M`EVwT(X1kLm1g9&&oUDPRQX7hkrzy3h9ep>PNh;-bM* zY5FR8Glw6r6va)yDT$8Bpq!*}Os2Each}rs{HTZW6HKit(v_b?MM1|y^dPFNDruRu zV2k|Tx0P@OjHD*~oy=weA3_G;^J`^H()8?0%E~H}=YjeX*q%Uj@QRS_OS^IDsdGnu z%mow%zvq{g?LVogsFlwW#Y<`^E8`QsYr9F_2Y2-V4e$sfT_2Ls+IDP6*aCz;A_iIh zFvVl%SGw{(b1u7gu3Hf!8|i~^yWXcNoU9S1(P|a7+2b+1 zD3P!Iz5}^yd4f}2*xn&1JXH-O7PR4<)@Rs*vCA!v*rA3^a@`X7icHr(KXs`J5rTS> zq_Ddl>`X=E+=M0Rz;ckQo21cQSJlERuK|J9RgP@O5DuYN1wF-dugi)t+CQT+Gd7%H zZpRU$S_jV+kiUNr2*5Ci#K)ia@w)DgM(=1Sw0x%6K$Z=T#lM8K-ICQ+LPBA;xa|fm zRJ)%|IknF;LoG_y<_}TXezbgr6coLX!9&hpenm+gzb@|WAM2_pJuY%t2L;2JVEvdf zVPK$FE}O-wEh#JFWaqxP9YFa>=Ep!^U6IHLyiP(?FFQX)KSj&@4d!=|DV_B@r?FiX z6{VL6QY?C{e)P9(#j*?=*GzcU-zs&=XHekyLBf`vbI-Iz0Lrt_i!wIOqSI~37m*il z_yr#9#qWEP(SGKY;(f{i9$T0XZB~_i2&+)GbC*&NQfP(?&j=0kWk9=kux(x;2 zjRxPbw&=Nn02>1znBO~Y-xDn#D7DE?=JXb?ui|zqp{C|0;%I3q)p}iY(qu_!XRV^8OJUPE*l6}D&%6E1vaYiNIiX=-e1iL4e0)?G8pSA` z-m|jE%Xjx&1c2VIoVsFTm(Td^JDxKGCf8dbacc%ZGuK>q&7{T{En!@rhF7l2tR6+g zX)xSpu0_NI0$H#x0bm#24>5$8EPy4zP9!97+=Sb2K^Hx4L=-JiKF8}jou;mrIX@On z&C0AriMP9hOYm(Ehz$3u-rj_4Ig{q(fBc_aeWf%*Bq>VDs^}T$^Jgq<@URB~%70nX z&M(Ni26ZAYJ@i5=1gK75#Vi}9`r#*ja;}N&xezhn)_9&g<;8?iga-vtCU+=(sD|s&($=2; zjr28^EPa-vw53FY4vP{|NnM@GXW`EQbIh28kFPJ+^X=g0$*#D>jMxlYZ(Vj!K-KWM zLWrp46r&6>JILTcZS3r~u&L<>GZp;(V^Gh?DC19o%`ulxY702^~!6R_OzYlL{8Ov8v+7#%2n^zXm>s0XQfkZTyoj1=PQ(ni~8yLIi~R7 ziz{-5*$&hIbBHd1_U_B8ATtQiW5&f%yxmmT5J{t;UPO08;=ZYf9gMIDJ-e>{9-X%% zLWPG9R@74r*@lG%MavCVs3mI=W0BODY<>owDNLoZt}^6m+H{cz&QDOUy6Yab{I=*X z%;SxR)7|VXadWX=ce>XLP0Ths&M6k|TMv}7Uyj2j!B85O-)oU{0)h|X-(1Bnx z#@$4~y`RP`*cVBJG?-{sHT6ZLP1Ys^T+fsw)MBLbds%}dz5w;Ci-||~nt)d!_aOob zs3j%E^R6#>OuJR96MfK;H+o{*(WqSv{bae`+NfgnJ=bTuQy!1Z!R;|Wo4n>?uzspl zwA*>^_Pm{m%G|6GfgC1m;xHom#rbOea!dS|p^lPOgc#h^l=`$WB;(LWUT2NJ#HiYC zCzBJ5pR_APp}O^}&&K(W--CnZtaPtus^>TwNzk=(RnW6HvRc?BQL30BT@8kDQsF)QnsPHlK~m^rt_5#IEYaa z`C^owo|k#I1<%VZQ{WXTl_n&%1LuvUt%d;b$y%+myPck%Zzll2157rIAJm23 zS2u%2ULFgJCVdXi&FgPW^vt_gEfJ58NZ7wI$k=lUVj>mbgu?{mA8^6a8XdfIXpWW{B*oRxFG+^+FQZ%?&o{$me0vro_wi!X}dUcJomj&wrrIP+w~SmwF^YjZ(G!p?;ycIZbRv(R7=T}!*J!@Py7H#%6* zXDdkC4lY{kJG>mpXN$2KKju`T;p603AxI2vzvSR%WEoZXWRe&dHmku$^Sr<0z<&5Rj2YV2z2iwm_51ezzqf~dK(UW z0s~ltrUnyy#<-Khp(TF(VjdR(z%t1g-*Q-xb7fAjXyEPa4FL6QJ9+Y3Kp|LSo7MBQ ze(jDJLC#eunL6SonV6F?@9gB~+gtMmM>Fkk}h4=D$ zhKJ48oHpy;J`={qVdw{&eW*a!t0U{71;4nSeAs-o;)JB%l_EuRva-wGymAIi&JrUc^OfLUNNzy4-@UY3J<=pHzpv7G|#e{y3B!VfUr~T9Q zSk1u!GbrbFZg;Zt9!q7Jj{sKuJG7v~i`>lP80Ztd9kwV%+*eq-7UjT?(7`D|uhr1M zzi1n8`SqCv6FFepILCl_+%G>&lo-qYy!f#S$8E9U`XY4vX=!U-Bs6$%BD>Y$<;S71xvbr>qCUPw!}D6} z+pn_q*W-Iw3hLnr#>dO%Z<;)Cz)(|BYM^)>uwB_|6rY?(P=Ww`eIzAI%l;0oGPwH# zQRS-{O(|}@bL7tLNRxezlG8_-sA!jJPq_a=Y)B>S^m=2h{fzz)&d*FwWC@&;Z^Tks zfOVK}E_{548Np!Ef4^HIy-!88^y)?o?ST7rM?C(8L}Wjx>O3gCmEu%6?#0^N90scU zwb6EFqMx)TPP+2NX-*$*V3$R3HJ4VD$d=IW`32Br%QdWvBE*85wO9u^SEzZsFCSCO zU#AbzY_lV%wcSWt)6=iltWlnQR&irJ=*?e`H=gZSH3M>Z&Ao8d~7kj)qhdhu@U| zOdRL^7^+C~kt$;+B@MS<$=;;V7jnayjEBD3zSwL4P;miz?rU*Dfyu7BJ`AXZQeI!5 zoG4mPUA?%f`t>wch>9v#n+SLs{LejIzo`+4QSq{1l>hSQS@9he=@R;39zakC+=)E6 z>`R~zKLKFnbgcgYL1SlpWqYh&!`6O`&O1z4&_+4=o0WRw>hCG#)YmmxH7e_9fiOA$ z7T|l8Wm}Vd-0u5Fb3$MfaGR^S^`cb!E*V;RQ|+@9cV0XedxjK}ww9<+mQ^MDLuNu_ zbZ|t;f+&Ep0Wzw;%qwf#oU3FlbiYMg=DsvkLxE8nXX4PQq^l#{q%CZ%(;6=>c)vAw z!^vE%P=*pQDh2P{deXvtI!6ohZ?qYA7b@`uDrbg=lB8c+S_Z|*nGp5&QMRAwUAiY2 z{O`Vnd7R>1Y17@%#rV83oCsOK+j`K6W0Oj?iP47ec! zx|yvWFMy)nWlReI0JyshM_=!1(3?lwqP^(|vAO;Qn)SEmSmNv4?`p;VFa77!^-im7 z%OQ+h`o55IEzfp>KYWEnO|QGn-5n`z<^5_MX+OJ87(J}^1;y<@bv4@T0!0%qW1#-q z(%p4i<<9n^jhZM^h~A9l&WeFnFh#gbDGvM1F_%4YR8dn!TC#HZwuzA9FR@Tjh{-=F zioZpYIzjLS;pQxz&Ero``b@F!)nXkZz8s5>p@{iD+`p%S4vlF$%JO2kJQ2vYA@%LG zIBSIiW}>6ODlf5_#49!7c6olj>l|G21~?>3Zl`GsyU83+u~~(@4AnUe#+>gnjhZyp z-h86xj=ZcFD^7ykg>+AUC+UPzgg2+W#ObU2g|c?7QAGOAZ~sv{{WPX2&({2UqTm0y zKGDR15;vJgp!}Pe0)DkZQ+TUW=Qm=4B)8(W4+;z30spme1!2V%X%eRvHUE7oa1IE1 z>Vz!{Foy(d9tYyMv9;oR@^XJ7B*{cRs%m%EGK3{)fkItJF%ekd`M9Gec<=MgaDT3e z%k-8!;~JTF*0jBUp~h55_2Kq~g?Gp-3^jSq6} z_&dLk59C@1thcL*^VMnTYUF1`nO16jn@e=vhTnr$=;&y=?4HeZ zPKxkgGr*m5)wh%%?Rs^6`MC=Qhdh;*Rtov^*cxP; zBwQYI0>xLQyKUuAP0r9o4oJw}ULGSi00X38-2hBwfcKQ%d@VCtuA6b!Q|p``28oO< z>R7#hHMdT(infky!oKp_UCyp}+^px~<$EXj)%j+&5J+|IZg}GlM}ysz{lR@-pfe?C zeu>3FUPn5>=lk1-qJqHRK7=I62db#d?cpW0(1h&o;3qZ*$cO(h_(b-AQSv|79uRZ` zcjMo@CG-G;(l4n&o9ECeIra6P!b2Kpb@RbV5aD?)knjqpZ7ROVy^h1Ra@-}{pH+M{ z5C)XyA{=qkiMwq@z~u3qfFLFNCrT!Tz4s!Du%H|+f>B26y6cGgIutg$RwD}qJ7uk9(S%SdQfkr9aT(qe*ZN}Q2Wn8M{VKzTbiJ7>Pe95>IXoAq$<1BDvSy-}js za8$7DnhKH4a%=;ra(FR2KB3A_j;wY;7FtIwRrV1;0RO;W$p4zT@V&qNT1K++MxPcZ zWgj%QF(p-hFwAp@n5ZBqfFhi?wXPzMx4YzIY=yfdXTy zba_&mABJ{9zj3W4NKA;w-lC5T+HkX*0=9h&xJ%acwVUe{%)>1~7sOV8m^|?#=)4^I*t_zqTs^CH77V0oB%oO4FT?-^{aHkwyPRW(U32| zY<5FAU8}b3g5!u7NCXR9gdhbSEzgaG)>Q<#pJRAsGAe5GFCBKredcD|=#i1==Cx^n zgl#M{N#&$Sr|l?@XKY~hh9@@K)sVtd#urB%+n7pM1NuJ?Tlfr{%(GX()Xv=ef`Y0W z2|Wto{k8}MBhWlzx#+fePOx3c2f=bgNE@TYKnu_;Ri|cWe3L+njY0J^AASawn{WG` zy|n2NO}%M<%FCn>N3@k4^Yy~7%~*M2V|8h3fxFdt z?Iw@j6TQ`Kuml^y&1+}-?fL5IL8*+;YnN31oTBb=x$3ccrN)tH!oRZn;lb;{bF@XL zNulQ}rU9FFgI4FFO=&VCa>S_RLn^YTIrnY`=1OWID@d>Mwb?g=+B6*Of~bH=Z;1c3w@YL` zJ0Q0xNQScSy8msiLTq1Ty2p*Xf)X(@!a%oB5eEC#gPllll3>QTaqNu$Y&Xud7n>r8B2L*&3AD!+cW=G zn`0F!+6IoBp`9eIeCAHOdF7NkY1As^Hu(pa#7ma)a zc|F?x0Hnt2^-6@2s(WCQ__RNB46;gHDpJEa22t4U;@lP9Z~K1!9A*OND+7eA&Ug+p z4?Wy*b5Q}7|3}&ay35#rT~|yl$7A&x9{2+P`9)7*qI>UaLDYP+%1>`U zhE?X=h()37clYf_3XHD4L`lrVC(f32=n7NfRlQz)EVF8l24?WFYXvLfJk>d~mSboU zjfE`Z*-}M!f0>nj9DqMu)1yrt-B9~7_&rz?&-nn;I-mLz|9}76wN|I!<;g-l6noSo z#ChUi$b&D8xpQf0{I^=O>t@&U%OfOCS`7Vlvw+NKehp!e(8F75m46?muh)3r`WPoE z6cALX>N6FVHF2@|PwYI%%NFr6e54w*ghZa{8V}2-Ir3#&q2nvhl%iXX6qYB2nVTwG zT>lY3B5L{a-!Ty=^ALzNHqjl)&2U>TgyNpKBsJlKe!IJI#;gf~9Yvq)M zQN1R*QG^V`kURm&f%W`uCcm#Y&A_0T!jVHphJFGL{x0_(uvtO|c9YnUF0S=1HyM*c z*@Fj+{9Y~-it+u$hP8`Y8UUuv_!|8WTg@eU!}m}!`dRz4Cf&Ndjy}s-##ptWe%`)> zQ5#P8AE=I%Aw|m%8D98Xv1>kploj9J^niGwOsftsxe}&JT!|q*oi;y$1DH2^l`vR` z;U|(BY(N3H{Z%VN1?&YtA6S12PnFtFBSg(t9d1IMIvR6|btbL#pL`ALv<`xE4{tAR zM@Rn#3N#iOVWT)+TU_MNJ6UkgRN5E*p>hAWT)x?H3V>sVjl<&bAFq*R0BC5rI(PS~ zS>Gmd$AUt71PfKr?-E6dqD#=M0(_{dF zrCq0NJzR|G`DQg$&+kb_I{$Uqu|*dS(sFQ7o%-?pcK=M(2KVdC(nr$Pt$Rh7)Q(TZ zRO$_*yViuNDoc_ekeM9WVsRL;AGMyJ-Cbp7U#3qTsR$Hke#V}!>z0+^=KDf;qPg<$ zSW2S4tki24SX+fwJpr{TQmt5ibSU@c%rxHajEIzw0cXMTFWUc0rNR^#n17Vurv;PF z5WW_}1pO>T`lWj!;1jC!oM&!6ZQ0ON&1cWap?&umzq;91u|0}(s@uHoIxAD|M>O*F zUBSjw;C72>t{aK+Ia3b39C2sDduHCugy~8JOA-XYsTUZ6h|q@t>-p|71qj|mj#Hc1 zUorC7sRbBQ6YK>)hz31(>ArT$sW0=vktMV3-x)uxZ6|H{cA8GMR$E?Ar|v6~*1a9@ z3_|U=15CZog_Mv{^-|qdH(&-K8r`9lPvuWkM9l6G_CKWoa6f_y4q*+%h|h;dB+R!> zcLL0zIpFnGttth@X@4jX5IGKi!vgpBJC;P)a;R206|rxs5KMqC<(C-MVTh7blHB~e zu>5cbLlFQ3dHrg?NkzCDnf+_n{%4g!gkno1K!=DH5b@n@sv*Y+BMa?{WoS#az_TWjnj~fwOHcW#Glg&n4mYfCXEUT5~sLu%PEk9N=rswokyM z#zfZ>{{bDeG22?6gq60d^jQy~CM`-+`RmKWB=gmm3B5K}`#Uj65fsS7>MyLcIR%B? zKGQ%jTW?^O9c!ZH2vF%^kaZh0YDxW`XPi6K^!(u%UkL~M@C1wSh!=khiIZxxw-Q&i zwG%-aHb79B8*)aGZWn`1MC$tHCDfzLaEL{?<)rq1Heoif2@!X4p$8qM^~jCl4HVW* zxM<=C@IT-oB~9>4*`$r7WOi&-A=%ww_Ee-1o9A1>Rx9y&o{|~8e^{*2nQYLiv}MWh zyF5I!&CHD-IpUT6Nt)VE##0d6u4Zqtbs%!gbxOtJ2mqZ)swxQgp6#p%Eb_67Gb@^{ z7>NE5MfTZ)+DbG;BRxXSZdUlJX-w?3l&|%#{!p+~Aso@~nJR#gM`P6c?I;~k! zT?;F$Ky8+YyCOy0Lilmk2M_6Sc&VUvDaE?p&6*W%Vvz9Bdm8czC2Rlnnwd_dhWK)>I=TVKVf-({_6Gr2 z5aNADpl@4zEG^=enOT*$^F@O6(i%M8G#)UUiH;(d)T3A9{-2*_t~ZvAu!68rwqUsg~Gg8(4|;mn7zQy$nyWrx!en)iz?%m8nv zQ*~?=tJKvH4&>A6^&)TGOb|uGRkX?0s^a(ifL%*e*87Krlw*9-*dt0NM{o$g!%kfJ zi5+#JwGE#sG##f#lU5}#wL*vfB`U&h`Q=)B5l+t7&TKDks;m9C`=NB6Gkxf$`b;Ld z+k5>rGLnsLy+PXlElUoWayHN{xeysE9L^08gD5PS45%pC;}WvDr8@Wph7is106i*!gD39 zMB(Z(S=AM}unn95=yqZ_j#I?q>q*_w%3|n3Cfovs+DGE(b-N||P6QeF2}Cf-?bBr$ z+=zU3e>$tDGhy*TakhyF5&q{=uWEqZDESl!ePFjzJ3Bi!Dqy)8*_+ZIrr8s!6w z;dU3)H*-yn?sJ)=09`FGLx0=pgRE;cIX$fze=tq9lOLlG`GA|mDJdHKvdlsV64t>- z7V78i%*`undI2txKdA~@FqU}de8zA1JN$!Ug!|)+WYP4B1X$oZq z;wjHvI&Wr!m~at*9~@=P+`48wZCsq)AvXWHT7b~MvdHuf5G*l&FjY^TCHBPRE_xfYI2{W2UMB$a#b$2_sPfUO)(*tQ=6N)f&azc0Ga^hg?)$g zf1%^D7pm(t=?9wht2ij2&CB$O8u}1+3{S=sEreXsUteB|FZ0my*Rl-5JCxz<54#xsg;%9od`g+#D}jQN?Xd zODDMVFo&(m5%>IW9Gt!s?j?ox^q|NuW;Y!@qguWr8*p?-KBEg#xHXtMxUrMn2n2~w z6-MrNUQR)5GikTo{C=N41K1Ccp$|$FgvclDkfw5b{S14d*tOb849ZF?i*d+>6)}sE zKfF;2zYqm-OO!h+ZLCzO%+C^|;t&ygjFh}ID1?j}dV71t;jffigI#J2J{|mURHU8I zd;eM6O>gQx9!AUi?rILT`1AdPheQaM##sUNz(os5G2BV2(hMmgFsaLIc4~Y_N=S)y zB?bSVN)&U*7aeEieOy?WSH$b=@27mEecx%6hF~eVh;*P^jq#;jVaven2gV1cDLC_>>Kggj@8N$a$~ZP+6iZ58qCBNP*y(hBV8cKPqS%qQR2YBjg%*j^Tg7f6 z!CQOp`%ad|S&2k*GVxBhR7sgPXIrW-wIkX?`oqrh_p({7M1Y>gM)HfARw5B+<+B|R zoBjK3+LOx**XPkHfeC2ITug?IeGpVDZq^P3gcyjZB2rWAH$3bF3+BC=Ck8Vf247lT{@Vayx zjlPNq;o{Kf8HHQ~QojLs#cjOUIXCy9U1kp$TTjRp1S=?YxHgn$N1;I_lY5loekO>H zfrETskWA19yr2Po@2%UUPm=oIogKdO;??~%ty@6>EHLt!>YVH1gLti?pBl9In*CKv;7949im#%aer7wd1|s7`r`()QKLf-o~wV&^>y!-5Z2H5 zoFwvX69Z!xZQhfQnbi_R-L3xJz2>&TOZ4d&7JL7O@Ge?zev#gV1b&+MBPVZ;`uBjr z59BuN>5UUrg8*-7Ls%&q6ESFLgt_KP}>1i$}>=JI5F<7#l#oX$f%* z`8UKbE3A}dh>1a@!AAPuItK$5sMv(b$k=~=m9+M@(0IM;yUd4o>k;LeuJ6{ZH|#AR zeSJ1h9evTlRblugE-Dz??`Zoz0TajjwJEXj0t1%Vniv_4F?_j0EqpAOYuLb$vYXRi?&;XzHtBn5rO{cgn z_7Gk}6HsWiK?Jos_a?`8!dC2|c~zrml+BAlo@th`emJ5~)hIlXz!57y!OI;yM}^iC zFD6(tycHx)EFYGJT@N4l-_`M*>S=P_*}ifUmM6Nw?Q_%8a6~SHn`|^17q&@k?K<2}BaO->2{}YS3Bw=EcyCeL!$T)os+CF86+} zYu3|w+8%T^kBUV^u{qxbDpQ~9{Bu@RcOv?{Y3ymEfeJMx-mcMu8~DUUQNb)Fc^M!? z&S{4!720NtodLA$VFW5f(K=0wX4pFBeZuB5R;SivWer_McV1wEqt?0flz7n!pp3wd z$Pi2btM9lZpQZfMMJM;z7<-sDF!av)yp7!}_ z+`C+j2@DLgeziyzkdoxt7MSjIyggVI8@dpMAiF1Pm2)YajY=aWYb{qa;9VV1;4C?c zI0no~i=erqC77;qE-)3v$sY}gtBBR8P*P&2t7}Z(bHzWpVtNtf-55d5ePBRQqp*elkH;xeQwilTtaX2JZ@M=+Z z4q6Dax;-mk4HlrLByrc9UjgVghHTnf`Tcy1fe0ZBB{d5rpFqN@uDXwv9XAyneOp&2 z&y;K&hdZq<9NXEIPy76we?_7cghjU43BH*z?Cvw?{5_z1i0DCvgdstR2UCQUd45rB zrm8MBpzk+gYt=zdm*kC3A;5RtI>9ZDC#n=|&Y`Af9sw-Y+4ud}I@|aTqn4W0m4@#a z23dipj}%mMiiid4$k#k{*aiwX7{Y8{yTk;FrgyODp!`+0YTXUM4gpXFMh-)R4$vN} zbQAea+1Oxkk$2~2%So8=K?Zec0>2WfbkBJzl{8e6vekPw9ROT!z>L=Y8?aib>lx_i zn3x#MYBdPVOzSiRl-jsf9jhzK+FThkOzk-{DHL!vuOG;;qznu|!@EN{Gl!FUvg0EY z%GT^^D8Dyr#3&7wd~D8>s1u4qPL!$@$oO2la%#lPKY@Y(e$Bhw-Y*EZxRKvu5dO|F z{v*{^m2NvMAhGJU(fsDsMd`n2h|(qqWhJ~RfZe~u01ViLG5>`6b3l|771P1segif~ zlCO&FJgtnW+Xv0gfZkeBku|KpR-;mfx!K!EeFHR3yj(7bo+fNRbnNS&7}B*DkB-5=;gsKXx*q)y)HR( zc!SEjYVhfMNLXk{Xo7s>N1%mMRhg^Nltt~=U@}V$|3s8bnN^gOq*`EZs;)YJtoXvu1Y^j%pp~QP+fBKN_2Aoj~CFCFU#QJVeM%30ewXL zuHS zLj}Xf&&4$@9fe_a*3S=p+9Sk^YFK}(vkTv{leFN<_9#{&*%wZF0>V%RZz^W~bzk7p_xw#Te1u$2tIm@;M zTz0ub^VyR^a{%8e3WLF{FaJ&Ro}yYDt{h_yi_L!6J|E|3bPA%du_p~RM1*cSS$bRP z7z{%_SY7J*ykA}MxLZAPL{zBNZ&*)lG*#34^45G|IXL~cgr*I+*@>6Wbzh8a$Nt=i zDL+C>mH%m%*{p{14jwo)ZZnbq%J5|8^+c~96K`=n^tAWyDWAS(hXTbVCs8yL-qX=; zy<&WfoPMRw%XQg#H=dNQs~coEmw5R(Jj1StoOrndN?Am*qxoO;n9dq+0$Ees^*O5 zN^vrp?Xm=IrHunbOEW)14p&xu;>8Ne%6+yg-MgQkv5RM#z(-4Msq4>@l~YbUB!F<@ zkY4wC-08={55cvBpG$2*Z;M`sBd*bMv4&}L5xIx}#qW^)r|cf9-E7?A3{6uL`0?rS zo3{I@Is*r9u*lH9lmPXlAcyrB-f`h-pWs)?1A(r;2NV1l;lA7mxju$H}H9Zka z?V^{{b5e2^-`qxXQzfe-PjS_6so@(x3wPrQxu6u^!fq(Mgj>3kb?B04$#7>Vre6!Kd<%-hoom?-k0 zx2OHI!(OSs&MbfCOxVholn|w0c5cvWtX4`dX4QZafk7k{Q3%jLB%um_QI#g=G_FZ? zkjP02ar-$=6V@-04z!>cAmM;@;ENjJB5(Wr_p@#fq_#$Uz`@o4q!?+io-(+%Z>fiND+$Eh7ik8pi zezOx{Cqm01fyxe>R)EUZS5s+meTr#b^BF%xS?n-vmr&d%$3jO0=7>OkS$dubb>JrH z3Ygr#RB@#%&%fhQd$~&O&^z4ThC_tL1bDtKn@LlrD?t#UhoYh;IZ8a3AcJ}hCMaxL zht6Kf{Fw%=)iV9pkqLSa|HG{m__Sqra)4hhksFYWIsbv3n{HdpP1W^OUFdmh_i&1@ za83_6<;AW$B}J-uYH#A_bhEnq2q9og^_7gS<;#-rv^9khXUi4ykly@aDd0CKd?Yfm z(;UAwJ>oV3JKY=p{Ta|`04*A*Xm1CfoX__G02rlHQQ0T-I`niI?N1MayIB^uxQPpQ z=a_edP5o%)@W(H%z6U+^!vu>!k~|f#Rw~u3wKEY&ly#}N2UUW)wWpBA5ln3H&z$GB|uhF46p{0~cPf z91JLYATRGZsB*c9#p_UfF#nF6J1IFGmjqY~&>$d^Sh{F^)+7;E1QEFKMgyiH7PEG~ zbkU*H#J`(VsugewfmM+dGp#@}ILgRSiRT=g7mf`Y# zrxZqQPQU?B-TnMF4jvlq!+8|{DO zyR&T2VqsZKNX|~9 zJF>p<_bP7*lEtaVBbDei)$x2PhWWjaLb>Y%uK&=tF4q4`$f*ZY9ml^LCcJ^S=t(Dt z%3}u|xL2WjDSzlJD;ud$Pf6wER2Bi*;f|VQvB}pDMCH+i8k+9nwhW^Z6*vGZm_n8p z+%^16Hv%+??8~o&>U3+h)|155-O^u3J!>|&yR0YmvLu0TZM_X#cstWKVkkByMhjs6 zJTJO!ZA>WxE;3iwmz;#(beO{=ih2CryOpZb3B7J1RI0JOi)2ze9?FFQz(^n+r+rLW z=6NWd(481(LB&AgcR6t}2sj5E!fbfe^5+1oI(&5VdkC!W^F(>qnjYX`1^i1GaKuZr z_FV(OwV3#L?>=gm6#832od5b4D!x8HEVO5Xr;6X=mnD=ea)J&#F#)(LW|(AJUcQX4 zr25meA^+%pjq;Rn6Vwd++zk94YbXDPUV7c0)q3)+wuhIW)y?EdZ6T#OAbn{XG5{V_ zQ7`^gnJIeJ%g;Qx9=5~*-8zYM1$mz@vP@T90Z7Qu&|T~n4~fKqIpWqZJC&_8q=0~p zMXgF<@oo$lx^knP&6hK#7b4tAxiPeu1F-rCZkCeNo~;t8a{C9D+$rA5B7&hxd=+vtZRHBM+q z2+q#l%lT~~0^~IBXW*RGi|c7OKN*l5fEVI(|CbSP8H=6-u+p1@l>3KfYa9Cw{WrU( z=X@@yi4M1ZJYi5dF80-fzR!)BgF!GJ2^LOVCJtl|K|gIAkI_uG+j4N9ykpA6pi?wG z9gKikU_W^VK(8>M@6fN$wB{WTy~xSZ-n(8ub$eeRw{N?9_|VY&5BIYdf4&Z+^(iGO zHyTi@srMpALcu~IuEeB7Qg#?E9M)+ZHwFKW9>HW zS9JFsf2aF{%koln7-$$tf9dmZH9UZI#bV7R{fn}GV7gmA_GHE9WAfcqw9Qg&b4ACr zGR+5|3Tf6miqD+AS25DL*>R*2x<6?@=KB2N+X*4%Wp(>yAhg~d_PuI-3@~J(0j05; zR#Z3RgD9AJomNV6yWej06{Rdi45c(8Wv~V>?bTk)*fJKF{xY%%z8svy0Zlvv`kWNg zeS?eO9u3~}EnpTSpe<*!_o?*#9F?d-07m41%`<2}>7>I5klIttH)32F^cvO|$rDX= zyEpIZze%{7?>>$%S0bH}8Rd3Yt82n7w%r1lP;@2EYK&+pGgB_b!IOuS?nUJHmC5g6 zu$la+(t=$%Efp`YkPOY%q^*9@l16OVh{&b7W5o2K=_{+mMI)!bbwCLO*j^^+{2Vpq zTkRa}01LI~O9Q&Sk8yTLRkLlP6x0tlYKv%vf@y=);h}xjuoP%)d_kB2HNF^_un6DD z!!&K#Sw@+jSb0=_In>5xA7e(3a6*IPqzhA7fEx8uDSORrOx9*+Z;zbMZF*)jkwObn z+9IRv2gj#yh8X|?WYwnm=Up#F3V&AS98TN1_4TfP!i#wFSNSXaDV^D$$m9_+5Z0F- zJr{In$SF~wxvtTwAn&fH?x}7~S!MiS*_-$|)vp0)>raH7tR9mbhovSk&UBSI3KS7gljO@Z*Ar7F$=cp9oMp<(}4yD$ylumg*Y2 zic2L4&Sd7b8eW~lAs>5!@}@h2u#%-eUdN%5&JNl|$ub~epiLj6 z$-?Va&T;xmm(Nhq0XAad-{i?IJSlT~3_cN|hMFm_jD zz_^Z#?3@Re^l2&RK{TL9c#T;w_z%@rn}%z=yKg*BeXVpbo;f0lHvpY3K55RzqTzAm zNbQXvJO(v4sV6;ho_-*49{@O{FCBFaevI@>_{e(5I2$79$bPf8{ahMdus%|r%s4do z^*emTcOPk)iq)F#_)E|x6hBo z{ag{h^Aj;Pm8visv&QjxVNGj(oX8 z&IgDcuzH7d54iRHl^7-ffiR(KNz+@NMr;x20|eZ&=elj_m>Cg3{GZ(=l!Y?Gh?VzL zPtR^|dK3b-V+u|rkk*7ZOmUP_R9p#vFaN zmw^%-6+XDv024W6S3CqzQCrylO0ng$Vi zt55GBC##57E;IaslO8GWdOIZl^XJ#tJMNg7gN6?*W0%f}n=LAoM64~4E-D;`QLCdP zn3v8|!MS|xm47sTp}8NQ)d5Wr(St_gP7q^Cr zib@KCOjypyh9?BbDKO``HS?=Dv|d`rBy2-Ku+V^H$_CHeUBv@LB&OVlC-M~&)YHpg znvk{(Fy06zjOqZ@z%>do9JHUG-rB^fcebCE-RKb2*VuJQ83^Q>mGIl;rQ_`_pn~+m z>LXOOL~Fgy{yQL+SGs3|Aeha9d2xJvY^!{DGux3-?|kjbD*%?0mkz(ZYg;zes+>AP zlfX-S|E~S&l7p{0r>zM%^&wsX^Bvi=mWGQ7L2SR_C&FFTxq(8{&D`Fm!4MxoWX$>j zrfPLg_zYRIe{x2)pUDf^?H;ax($s~hQoeeJep{j~?i41%ZJu|~$YjdZ2m?HLjjHvypq5HF_~bNV zv_OoBXra1zJMC)ZsN>lP9z0=IPkE+O$$8IDCW2`dt@i(s_SR8Vb?+DNA*4%1lm-D2 z5Ri^TBPbn8mvo2Jp}Ulnl$Mt6E&=K8?(WV*+~xbN-yQeQyT=(0WN>Q7+G{<}oS#`Z zfp*0=>#H6rn>B7TWx=$WVt!G5{_qSx77xOA_vdT~&y4ml&}(E8mb8+NSY6>Dl9-V$ zQ&`gS&;=;!JjZH#n0NVkcj7(gPXVlC;8B#b=y=-<%0K3&)jbU6b2Z%E6G5RS>W3!y zM0?~*#yY{(QiYmd!u05P2`|X`cgND}fQ<0IX@Pa~O9X0YwAI*!Le(Mk;DY1~g)08r zN%om!8iga*2xDn5X2#wVa#Fw>ipE{+of59vrcK{OTH^`QUDOL2+ElzSL{p;_I9yT+0+iiH#GGfJ+`iqOf( zM4b@~{zlFiajCGm{S;QYYP}FktBE$ASi2O=yWsh4QamE~Mk{r=mfYlN|H3e{dX=X344UD#+(~&a4&d zESaBgPL{}2#YNU>b=$t}GdoXIB4&b{pJWbS}ym zeGF}A0v>6tmxnw!Tf3`_OLqC@J7uRBMC8)VF@T;9I<_C4D&5)cglfUVAjsSCN!SN+cp=^Qg z>91U*=D9T<7PRQviyy4RiB+nP7cWGkSK#M+I~ODrmq1NmdXh*Qg^DHiQ@Jvzo2oM?>%J{F-AYy*LBaE) zfE5_x3E`m)kNMS>s=Oo?-jSXVA)VehJ@773gChBhe2$bw%-WJ z3xXdS^wV*1bUPGi)LtjxJr{ z#+cWpj06l}t1kl3uiQ{WS^N;2a{AgVST$Iri<@^(_LeEXhK5n2*=*d#07g6_AW^N( zcvcbhzxpmelp*0d&e8m!rJ#fjDO<`_YZ3^L_!^tFRaIGryv}(x+-R5Z$MKG1XNHq1 zC0oT*^gFa?S&bf}v@S2KZ+hZ={J>8tw3EdRMgW1Fsz3a_;EsBE0Ih*7dS^dC9d7mt zXQxS0YD{-O(eFmA3}glYw>XPs!kDNeo}XGV92S*Kejb|0snKtQ)y7Y~0?cjz2k<0u zaX-r)31UO?U^p+1F9r%qd!4TWQ07h3#!9kACj42jMgjaiL!y`qbdZv{-F3I$>uA^6 zYUNJ+5qr<%cJbBdxch4?yAxrGqTZ0ebvHV=`Uw&R*lx}D2H*X{W4E-nQhkRVrI z+BH}d()KHQ?r`JU2rwTp)|*y#j1#Lp700%p+(8qubba>4X~k~KD<%5uprLgHXgfnl z^qTv>V_Hl*_=T8?e+`K%|9|Iccnn>gIaKz~_2V3CjY zR#xI6Zztfn@3B{!xzwL!YwImC@PMH@l4B1iX!{BM63oXCbxlFYs%(cPDePsa(mp3H zAu@ZH^E=-wSv<~sLS)JV_SeXeA^9;Mja?c1Fu7V(LJGY)`}%6NK~nZ}+axxTy0=E%+=_bk6%1n>9ctW?@3_nYw@b1t@8&8iQtv5roc zZMl`k|DL@f0;%mrcU1?1{)*>;-LnY7rn-=r(5X7A<)|T}wF1VM)OhnG*83R25nHjkA01zMcgQ z-ZG#S+ke|K{r==5P_yVClhWu3g4nP7h2rMS3Q z&}J!OthcpqAl+H(x4J&fG!WEE+TR&tN|DJaeNNPjW&u?ad4t>)yBhyw|h646!B zK0gH{I`o5SXE6;Jxx2-RO?AP2|4uVqEinS5Ba~$6#8zuHV}_~bz2Boc8?h^=UcHy1 z<+d0}fxMs&E=_FgC8XXaf~V|bB0o#qZ*cHK$*Y4NHgpyY&12O&wMC8PJfv4ZDm9fy z_MxccsMLyj;&Q(8<#^;-T43ivBYe>vF2i(rfHHoOTtCg1B9ayo9)6fI$4JE%d=|hv z{#=aRA*M8n4WxDBB#Po)K@Z)x4mXXfemq7NhP-Aj?&|JscD$<_6YW_&E>I3muexo- zjeCTG!k*gMa)O*TO8i3_C9Z9uYV)(gm#-0n#Qb_kHQ#-G5pUJrWH)F7T+K#Dp(}m9 zxrh&9)u90Xv&WnsWAZs{`IYKK*ffxqRo>}qeb#tnZ+&j7hD)rE_6AoP53XO<<;;U6 zdq5Hmh<%R##0oNKj8l^gF@eo*^0w5Q!YgNNH3lG%arm4sMuwjngUN|d#BZVI_>Ud@ z(UzaSl%xNAO%W6u>C&4j0=0xa##in4^KY$Xfqkx5z=J?=zh+7tgmy?mAczRR3ZK1u zTA4f(;}21^wab{V9X3FONGI}|P7Y{^=%lvJCC%4g3h=;W_16aG;TV`0zM`UQN=lZ;C8`p;E| zyqJS;I%v@Cv))%YN{Lt)#07ocOv9OWSX$f0oP3SFJlcbPxTeh_Z&5>ryG z?jZInONy$h^wDf-UWdIJIVY!%{tzToT)9NvX#&TKz3bk%A}1H;SONkWr)?l*ej7}H2NaM9LbDal`O_9vq+wJ5H~2cZ{p|8IV|3drG`lL@Y3FR3yy^YE=3`+z zg8UfSoNa9JsBXX2KT?B&y5@~0qC1pi92TE1tqFa=ZaMb<|``~v>zGicKN0?PiI1DvIl{Jfd3(L zOjOU+=B{JE`L42CuM#jYk%d(!s`jPpR%Pz6e+9_O>q<%Y1^uY46!t9LyvO0`CzCRTs!RgEyQn*!ZYe)qGfe% zeW3L#xg{gcbwYjI#+C2O8CfpG&6ADql57#eIFMJ`)CIbbzrnKU7K-POKoa zIIb;4TQPY|*+_$O=26<6JD=f;uyKY@>V-|(xihegd@xav!~ zZ*y z9A?edF-}Ia4cGok<9C_)B|JVzfwTNF$qggs90KvwDPEYLU4JULCV(I|93S=m?t-h* z4%gb_?CvtPaX5B&x37KI@z6Fb9-1LzS>x7V2zYux18x})`V`cYC>Mw0mh&B)l?D+( z{^4cSFESgC4Tc}tS9BN{a!Bq#Ac?Nhz9%?BQ9Up(_GiEGu<$f_U+S}}56l&@_n2`~ z1?7HxRd}_d;oIUhG$f-@#es3~U^#6zw*6L`KDNhb&;={P>Z3(sy(N0pJ&AD1)Luuc z;;SeSwrZ0EfowUrd{yOx4XiQrcvfFju}W)n!X;4+#$?;<5Kq&zP?~{9vhdR}K89H@k+oYW;8K zd&!>#dADP|kcJ4Y%JwCXQpgwuY6F^%H zsBq?6_@2>OoSFTN$W7~mw%UiNhsbd2D^G7PL94wjCBfS*a(w%hwe7Qz5Wg!Y$nW!A zQ{%xD0F9()W^z0E+R2j?EBFg%!SXV$4xC=AUEOww2;u#D{hqejjS(m+X;p9aK0G`Y zy_s#A*gmI^*|>@ihF*}cCP*7IXKfJR7v7wRS~CyEB0bdufoc1D)^}ICI?u+((S%Y` zFKY&qy?40|xSbCorFVuw5f&tj09`_qF|$@}?8WX(=z|7(b0{+cSgyVNXSs&9T%u-| z9e#J3s`Ux={${eOCvQd!(`Z;@+fyvSCF>?fkc(Wa46f6V$pa@< zdaG~uZacDY!)bS(QYv$dM7yOL4ERt$S8;7szs`^64h%O2Em0o@;0JP&@g&?X17__D zF6*X}85VfR$K71bsh~UKuyiUXW2ej0<=Z}nF8VP0MLuIZiadRMJFWpfZhXiHoPryz z-`>k<-Q2pG2|hr)GOvDd$IWDMW*OVMX&}SMnR9!y?trBXcz$Wti!RG{i5A9WW8oqlSsvM`a^ zZ!n)A#0uCy69QN-P=)aq!D^#tpx1UH7$7WOejRmt+6p7 zp?gozZN#F=sW0Z$*75A@p9^iU8noHJh*Udpn8x79`5&{)_!U4G+U5)>@EE#g^ zvg&c0zCS6UP?4>%kj$oEUEK;&)gvWPW7c@pHy4|!Om=h6TP!%qd1~HMKdGJIib4LM z4>k5P`QbJZ(&=%(4#OA5BI*}>u)1F<2WK-FpxH69RNv^?)`Slx=)&?QVr^gpzx7IQ zPd1z15cFEGHs7p_lC5m;FVvE7LXc&F9K9z0LsBQ8n$6J~N$;fKKU|bCHyVtTeajQ% zvT%lbVoD6y+@ATjbq<0oR;(>B7*K#hR^h>)_+U*O+E?_qja&rNWS??5Hkq{fU0ECs z`npq6MwE@g%pwB%djl7U>$_J+J3i3#3vNZ|0tfA2(+P`G6f&`TvIpP?VQQ;w2w7Gi zpYNPUn-!50{UW0PS=FA``^={pl3LowUERTg#j}#8XcB?zx=5~K0SZNeytXGRc2cp% za|1c-!1`<461mRwYk&AV<(-Q|7B9xh?-ql%+HNkq;|c(W6a#CTsoH#zr68VRdVZLw zDL)Gghrr3$`pbyA9cJ2lbRbd6B(;F%bGH3`i?7I`j;4yv8SM{UW2RehVG~j9=;-Li zrYw7cI{TJ~48N+yNLi0hJ7(=1PIa_tqzDq2&4C#kNO)_?-f5Q6fI#XU59#{)fv`h4 zc)is0<9=8*>hKEY$@NsGz_O2 zM(uBBm0Q}7TMf!SqAZ&|5+csyLh~`PEv4*lHsK|}tI_Tx{Jgm(iEK|a2^WB~95D=B)d3(-`EZoR%2qMPvj*lz)W;twm5Q5P4 z4=&*Gd{)%S;`zlYnuA@oQUgG+nue+jY~Q%FXg&?4SGm=(h4#7@rq1Ue=Ni;F1uR>S ztiEb>o$dDxFbKFW?*Qj%w`+(SU@h_1>RpTtgFGXDYI1p$OSx;ywy?Y%-V@P0^L6It z#+7)UDB3W>KNbrvU5|}|4M)R7Ki!doz)FCZkU)8`UVh5lbWfOdjR3h8Gc0bnTmP|q z2h6Q6NC{WCxuL}-hs$j6@T9D|Z=p2I0?RW(T}Yk3(%dgecx7^3=^>!2$Kh!u0M7CX z^tKy7c9^-Vyo&Zv`jt7bwmWL5^~Cn6p)s04W@wgfno^IKUq{Qo?ES9+0qfMCA?U zy1=Eqc=tLB#5I~>C$?kYoLIX>v>Bm3x-#4(c&#-bQIq;u+C#m3pGzc1yv9v~20EOc zaz~aC8xV+>pC0yj#-FQ)pETsdC_Cn#+>alFAsA!_pUTYcab!S1-&(2u!oote^?aS< z9ZG<%TD4UmGAhHanZ>@MgdHtmwH1M8<9DEz<{_y9+$#8LG3XYIUMu_!d3i$+)viYr z(t+E2MJ6`1!+ULxxrw{8*vp7cP22v}9}z5n6wP}W^7sq~a3$QDHwJl~`9sBZuJ-0S z95EqYH!fMq8K=j`7IT!K`gQHnYd{8CYLe@sPW=E?cL4!H(=Jgd;0*%QfjQT$3iCEL zt4%QM0iH4Xm+!-mDbrf+foanbf+vhuC=1f9gC!YGFeFcdCAAn@?p;rt9{ak&;WOJ7 zr+B9;4V?m-$81@YugceRDm5I*`8Zrm76tgDlImpRqE)mcWftwW?41p<$In29DR32k zKzZ+vI+(<2_V?sgL4bfc!U4cI0!JWlrUN6jHY+YdYH&JGaT0-v;gfBV|MZuHh7KI_D>PFM%V} z%uHfDjDza+@5#Z=utIv|74a9ZPd){0x)Nr&@wNyN@97cUOF{*Ms=4eNWp^92yyc@4nCTsL&e;Vw z-Ni|35j^VGLxZp2c?zV zyCs>=zkK;yuVvj~Bx^B)uZ;ZNyjT=z^2IOJoNvkfXpr9kVp@20qLHjUxPA;+=`=nR znq(^Zqt~c`vWcu;SDu7sb66pDq9@A^>ajR*xcIi%8;6%4t0lD?I6DO)Ali^HGZdGp z767KVt^cWFA*dw)H{v5NCySM>KUd1(%K%1C7MIWU0#fVQjua@D!Usc#82lN!7ezU< zx``e)qd`mRU1l$ATPji0#6kTT)VqbWrg zL4XyPC93=P7a+wfpMF`a@+?kCS$WdX@VUHcrX#Lh&d1%YtGTF;)ijG@3s8>~GbX|N6PmS%wUwxf^S*I9qpQ|fszvaLVs@4>jfJ{GN)McIau zf6{@7q%VQ9DUG`s9Yhn4O-lhTm3CTCSO^8%CQR@ySJ7NmdLsRi+{yJN+O9njHUgyc z%V{HdO8+ZNL_GE2adMnLr*R2Zns1Y4`q4tGW*N?{h?O7^A4s1W-N5rc&EMpLIT?1A zN7A&`T?=Et1~~fZ1n*~iW6NZx>srwA?nEIbHm;!U*^6&4wNp~W9{Hc9x_8XhS2pMD zX*Xf}2t;iUh;}pyEZF*wI2vRg-@QASlQ4PRZ@{^0kDX}O;(gD~EAx^-f%^$b`k(TFo@z;arkEIwOt~=6ku-osJ>Ay zUq9e5FI5HkB@~cxf}y`=mq!b4C?Wz0k%a}EjhD-dnLe7^Tg8`bcZP9}fF}MePt-ie ztZ(~lu>jj;`z43^^9O}8TMY1+ONWc&%=Aw z?s29aB_aa9mQvj;_AuH{`~IS{D){VjkZXF(l1WV*X)j|ljNWK$t2wK3I0-t1hc?9X zo0NDb4KJ1pJCElLF|0Y0JAW?A^glhYN1}mvQ$VPKFnnnwe>>B5qC<~b>Anl zG$NVP4_wRZV$Ao)n>vzNioon~1%GJNUBw(^{t%Q=wcfE_0P!S6wqxYGC@Qy%!)cg1U@85CNE1N|j4v~d22ZZ|I1_1#f&G83 zl8Oo~J+CgHOz{4SdY!x8;g=hM0yQKzy^l!w@s&LBV_cOd!zgVZwAE!T!Xt?Zn3EWC;Hri3M zVZ#Qzwzmbt21EkCbVf(%eNoSN-IpT+RoyQ1cD9y%pRDe=j@~tzqWVyvWUx?kw+^a_ zP4=Wt(nZrFgUDRhvh`tYvhZG`@@JBUT8*siHOuy44cc_YDsKtx$--C9=KsPEA&J?4 zA;pTg$D4=EeR_&V_XiLF5r}KP+uBJ(6gZ56%i)B5#Yl$w0;DE(Zs64@?0GGJYE{YY zzW+kX$iM*N2v0+mjg{?+y8*s{<2%p1`N-ragap)4-*UtBN`KJJ&r#HLz7NEFG~nvW z?%Vy@KGZrY5gS?L>twoHD)uGo^1Jv2i&iv5$XeJV3`7X1dM5qiP-h5@U0z5pu19e$ z#HsX3p zL9|uGw}O}7`{^O$!^Uk^zD#OiZXx_^cYKp6%f-1T&um8=oA`Cq0;9PM5J?_+9u{A_ zx0wU6fc_x=0t}2AqNo>ig}d|SM~>h06uA=CRVnHB%mg*#N- zE@EfE{XLUrlU1eO>U-S%$ywR*=^spw|H=go1l%taSABB^bw;+&^h)2p>Pe=-2ZJaj z%0CM*L9ja4ac@`)4R30ed$W1#-l*($^^JJ`8QHrP{g(@BCw<0?chl2f*(qXzVk>nq zshN1)rZt#7j@sp{q`M6T+^rR!jGdENHeD^nRL2(o<-?XgML>d6H&>NhD;QP}F;d=u zQbBaUpLhO}5-&)g{y;QL+woUoZ2C#lo;w^fxRrvmXYR8aF1odf*quyFT8BZ4jgNOa z==X8spi$_JJZ+yV^@v0$3*!v2LP7M>nzovo8fF-^GU}=ZW_!y^ zYARGDM?PM<43O}8%$Tr?1v~=WN)kRtmlHn)nbBk}*5AdwNffWvbh%_B%U9wK?T96%g2e%jWq^qo{*y|Ly?$2v~@ujz|KI<{x|MG#~hRSO72m-Qt( zsIB)B^N7AZ6?3$09*~Vye6En^e*|l=>tY-+Xx>f92iSeE!V)+iac=zx$gjc~39Qa~ z3@c@;hG5~E_9?fghr#r<2n1O;L8Uf-U8%CNs3OQ3+qWeM5m^||hr-^)SJhsQL)A)n z7#Lr4(}Z7t0|r(d=|&c_DWTbq(kQFr&r{V-25ggCD3(q=uAKK%d^^)y+rVu}#>h@s zL1JTH)3*i0@SUCn8x8(oxBr;|mzGd#PbKC=@ZpI~*6?ICJ)A_)b!VJ3#|5KLr8|nRQ(A#e#AEv*dXd{#W#8 zQ84Y7i?OGaMpM7FQ8KRUQ+aEXP$3iq=uO&He*oUEml~9&?wqzUdz-K48FBWCywkUG z4x7f;9G8uPg&vA1*&IeiZWWEYI>`N` zuYpPVd9X&vQmA)*CAd)`WZb5AuU#l8Q$>f=*pJ2ahdj1@d6j&)k*1h0?c%Q)_TyYK zYIy35jwz1q%k9)N9XB8S8gYfYvvTIEo4JpcmZhrr4z?(XhAh8gBn`{s9e7D01hoxK zxk{V}HeS^qe14)m-hB9y?h|<*nRtqS6mT7IwcyUuqu@Cl)tU7L?U>NWE%w zTJQME_8croaWts$l|M}YsFugYGBKy98EV0;;XLS^ag$s;WixtJdBDVPH8y1Tyf{U` zbN%=Ck8#Bd>@Pxyk_Yrk-*_C*ksk&&$FReWoDPHNS6rT& z!5KpJy1{Og?L~-DvItWSaR>@$uxaJYwX@YX<$R>yFN=#O>7y}9e@%}MKWPL~m{)O= zz>v+G9I!0A!9^7MLxL)ftq?_NRK%LN+(=sIe)`+U2rH0~t+1??Zg{ZL$IR%-cdxT& zR4v$B({P8ao@4lz6%}n8JM;w-e_5`2Uv_%$ibp`8u1>VO zOsPPxq^M@cRzaKcqtW$H03|B-yF{u-UTp+5-qGsQ)orW*+5BKM>mqd!Y<>ktMMVze zjp_eHuk_9U`P;&?EF?rkMA7=In=2Fco!`G9uTE3sth8CxY@9;EtK$|v104$}n;%p# zHy!OY-Jt>lpC2MZfEYjnWoK&>b9>oR+6^8^2zowr2gpE+a}39~&bb4qUIR?Umc6=( z&2KijJ|~NjAF&xm(xfV%6k}tO8uSfwaVMVT0Qe5a*F z6~KJ|$x%i|YbO@@9LKOkM$lG%wsJ{Ole%vtf3}+*@8accRaA8%eT}rWw9=4pa_Y8$x)0b_6;1r(mt2 zc9GsKKbe0(9RQ2C2;0-D`{XvAHL`_2YE{tFv(y~3GP5AvWg#86dy#&#Kjp(XdSgw6 zZF`RcQ1L`ZnX}!wylQMT5D2tR?oKuD1xe2UyxRZ;?KA6(HEXMJ4u|nd2P|IxH|#;}29YF>wqKwLMO1yMCLD&=@g@vn<8`Wh z?7e=RIUd|=ggdStsOUq1hg`uKTQl09ZWs{O1bbRou;8Vf!6E~0)EBzhWe*|tm_gQ&Y9Nf_$;7)14#Hi&Z++LX^mWe%3Fsy z`Rc(pQQw-is|;2zd_C`~Hy3a$_C?^0p!L-PT3>&}{Xv)r+qF!6)!P!e6RA_)eU$boGD+TC1{Y~=b7w&A-Yl@taDYTxJyTO8Vj)BDO@D)ijsvFsJZYY2OTz(Cz>(dypMv1C4umanlBqaehmR?73_9EPtO9h zuc+)gqa@l{wC47BcMf--kv;hVD7CL@p#PWL8jkh%=FPV?zN(C=sqf{(P)fJu)cvMNT@v(b#u2;Ak!G?CufXpGT*q9(rnScP? zR3>>E#fD5P0D*1j^8w3Mp;jTv=-&h0R0au%{xFg6u(`su@`SL8-p% zB=txrI@o1AJvpJC!{mNV! zsSHVv{07v?WhZ9k?JL|V-JktX;Qo(L?Z0A4d<(}ofwh-@=L0tb@r?L2Iim1R)5&r~ z&_en7D@2+V0!gAUc}?`}ai7848Ro32pYnN9J$YSCBuQZSu{^w^1od%-dJ0Ci>R%Ng z#PVGR$mM%^j`#jOGU$a}0DLJS*`F*clpXCg3?&J*EPWQz&aXqCJ=454FUDnFor18v z&WG$50lGB zlBW*vTAy+m3G(}KFxzZ!0zos1?~jP};6piV@>oIBoc#A-e7r+AF*SF4aJ1qt1&pi( z#rjGO*m9vF3>K#Wlgu4aRR31-p>zeu+fQh&yoqh|PCv@9914ZjTc8asUY<5J>TYzg zCG4>o4`$95Y)~&mq5>5w*dEA!D55~t(D_ooTMd4whB;D{#C4mzCB&YN-%cj|V1^vE z&HoL97m5eb<0R~VMTMe=^w0Bt3rM!~Vj=wV^S}N*M7Dq?q|NI^TK?N@W zZ427%{^5#7Ywt*MY=cPnMR40w%eeD@uWN%R)Y{1QVx$XQE|PfSJHo^Ep1j_!-Q!vt ze!}Y)!_!bG{(oQ5%NHbVC(}0K#-BZ^HQ=PKW`Am^<7M6zw(DyO)^X=?=i%4vRiKTyJ}Cn(5;`EE0_`epCcuL_T&IH zu&6Uq>Ul)e!pjM!ZZ17Kiq(aeoBgzpIOp z30IdQt#=gB!>LK<*gU%$kvu_oB6A$yAI**X0sDhr(lH~CC6NTw!t*@{X!-YcbVz0I z2#F|IF#f(^9LWR2^R%m9mws7gMQx4GbMsa9+y9Q;(qa&62QL)mKTieKcwe&Y=>PL+ z|G%gE|Box>|MgTnUg*Ey)`aGpo}FEJ%jcnO`(#ony{5bS&PC6MHmhn5P7Z7$?xi~O zqM1R2f-B>7*F#T(>e)H6@P4z#yDE)Bf?kg>N~Hz?9SXd+grUi#Xe2`5<`XU)0YHySr|?h|QDb2Fhw_(}O$Qb#@UR_4kapL$rx$~crmY!$%dFDVOtXgKyMU(lfr1xsIGm~fU zOFi6txGZ~vKmos#lxxQ1b~20O?uo|u7NZ@i+mmuMP28yLaIx8G;cnI2%SgcukOpez zpkx>`xw-nN#ej9DpejS0^vfR0dt7qPigKj6d<$56o&zzv&+jjDzo-HdWV-oS)SJKb zw$eOUEUN9CyWDwu3Fr5BeIR#VE6I2tDqnQ9nV!X_pDK9D>ZnwYfa}{K=b_ zqb1IyPd4p-2|ZP zho{)IvPdAi9jVK&dNkK~$X3>mt za6Nd~-i%L)B`9q9Ic(zOZ5(oSC%{7C4!zUJn0D*f&Jl2W;CF|g9qfJXE-v3}h7ZRn zfwa|iG)nS@V~fE9fAq?wW0%GIrPEZ;WMVuq5jx$F)z_4MiU)Cvn>eI=OBabnJCUix zS_;vb#yTGGxW$cHezM|8>$h9k?hn0>qGan2zF|JAJH2+l1l%0K{K@Ug1TJm2H)-++ zkZ6bd#khZ6F}X_1xmR@-J39d!es~$5$83q9*0doX-r0T|;0QI3on(e*?1Yz}3=kQF zvpLp?$|T;;J3rTyezB`QrG$qZ-nnXDOa1i=$!5tkmN=4rK|#UZ$%A%-VQ(yu9#Pl% zS1(*I>W2#^>qJZr4ZZ#8*-R)XJ-K@h_Y_5=^n2Tirt60%$-C`3R&nlWV$MfeT-CJP zk1H_P+ryp^LXyST$z##YcdA+os21VPKSxd3SVs~xl4_?3eJI%3*=zYz`a;ggjR!8S z-Ed{%=jse~cZd^HoVN?}66*H)`c^wa@M%o%TeIqW8B(2=`>ScJ=%^9v9cRf&*7q zZ4b$I7xBr@@@7}vSLfx7Zf^qH&6sx7m(FiqQ29it)^#6VF2c7?J()G)vf#lJ*kLHt*op}3-ZQ}u(3mN z>b3COWv635C^#?Q`Bi33Z+l>@gu(=Ki>*J%$(_QH#C=l~6W&z=5h5=C-8bQ!)ki6B zZSS5vCxzaiHELJu8&6jpda4~=mK}&`c_!Z($K5tCF!M8~AAk$!@XfqswHkBnQbD;~ zq7YhlchBusuyxh5xLW~Oi&{=t-2QP)wLn3!`4aMdwmDPfY{6ISOJP%)^20OdEE*0g zy$L)dIW(FyI!8K6#P`T4@&rc`xVMeQx`n%)!;ccS-jl%F56d^Dx!19hl>vHEP&{*E zBX(zpD6u~_K3*6tx3Dn(*RQhKLY%-1Py~Ip{%kV^wv4;z?tWvMY{Q^Y?{V5|?Ox&l zA7BscUp@Ys{*F#U?_dQccyn@?^?<*&Il;xA!hTTmk(NYfiY67W%v|nTj?MVt2A=9% zJMG%TpyPDc7?;pxcnMadH_hxH$`P_yTs+{lM7?87GP~|v^`cv)Q_EcQ^FFOIoBKgQ z57#>f6%}PA01NRQ{_JSLe|%QX!BP1*bnDzDk3|hw%=^KqV@kV@icaY_R5jnnYwqVd zt6-%2urGA_oax$q0WrUQr9@4Z2v#9T!5Z6FY z-JE-@Ep+RL^W-&@|@9(NIG8bg_cw6oK9=}Y)H;OiK)*HXCtGxfy z+{3$`3 z3mq4;hV@%b=l)P2?0~b$rVc)=pQRo+Ep?aiKNHVrF=0>QJVy4_evXDr4yP$Obc~Jd zzqQoXmUWb|yrX%Zjr$6d5_WY%_L`ALnRq)*$Hr0f6%^=bDCE& z=lb+%(k2>u+4nI7g`bj|f$gsEW2aPz(kEr>sRqV6Xio3l3h^OZM}czh8gbE7Trvbg zf%)$R5%;?>7xR0S+oIUfX%0ua9|xv!o(s^cc)V7$QM)>6_YIZ$>>>-*+Emf`3g=!O zOajB_Yf_Ol#+NCe{QR&Dx9$jswpg&D`+yspI2p@I|D?9ZmN&eIfxbZ?we4W4wDAgn zTEk5`^-4>GyzpLreK~k4o-tF6U#H0w*W1XPI25aliB9~c0MsKbO-&{As4YIM+1wws z^?vI7UI4Btu7}IqaSKxuv!;{C({NJ$vb55#EU_l(o^oRAPnlYVFxdia~O`WZBDVHV;gl^>Zf%`h4eB;n`f;-Y2dy8NX;b(n0snQG@j%;qvyq-7ZS z=j6oJoc@CEc&yxP2F9zUT?|`XO{!ixe6H;(S3K$J-gG;+M@!ytTF1b2;_R-sn@s7A zX|o*pOt!jn^%Q4kadQN7AdJaofBj)wd5Y#L)(5a4{fqO8avf^<)zk>*gD;w>YmF~o zmJbiD*zV6X($-aSEy61b3Vc|$cchpjlJUtPpUHJ43IDm(bH9^b+kWQNSI+PL<_sqK5j3t(}{IAf-W%R&_T)y>(A=s^|T9 zheN(}71m4V*y5|r6-;IdzXG-TgMHWnTVj9yM1jO$PkOZan~l1U0e-(xIGdP|AT8cz&fGjz51Z=cc~;)pI0j8*<#R|`;yN0F9kDmvX1$h8=X48u6AfUv>Qq;)4N<| z6w~LKRq*aV7DXlHSvdj$_aL9eqq|J0@;sE#^n&TSHYb&xsm@EBDv4KjZ}-qcuUuWZ zNPE+^J~UwB>G&?@z*38tH0El_EOiHB`j&02%zn-n&*RX`M2#dLiaFbTPyPNQk1c0C zyZPAp{y#-jPcRc#Qmf>e0!alV(X##&xLdoQ8*07D7`@3{Fvhr6*KG4l3QBZ ztyk4|rR#F2&3f3}-`jh)4c`t7e^KdrnGzCWA53)@7hg{1d~{#I{@#8W9HzsjhvUIV zn7cc>=4NIleZe=x)ODkv5==Nmu67C$v$;pw*>#>KeqGiEO64Zb8>sU ze_QD^&D`9dK|;!JJGD*QtN8^2f#dvVSi;}4InW$FlFMe2($niJ{eReduc#)w?qAd{ ziU@)f0YP4>A|PG5AVokxKzdVp?}QSH1x2MPRl0(JNQn?2^e9zo=%I%mAS3}ohmgJe zzWx5s9_Qwab9XKdgNxia4C`6XT66x|OpdAI8+Ud-4R>>|pRQ*b5tik`MUolsf|BA= zqj{2nic60OXM{AlI3dB{wK5R`t-HQ>k-5yeo=_@n&83R_)ALAMoBG_Dkj9x+|EBFO zT#P8a_f2s~sA@AA>{;km8WxhAL>~tn5Uiysbjy1O3LGdW&WPPXZ5VQ6D$yyEL+0W( zMe*2qq@pcSnts6O>6j!pH~E`kpL-dM@d{u2=M|q6z1m`-Ib91%%VHoUGnhndAZ14w z!?<$4kKn3gg*0kGtGZi*Ogro)i8T2Vrvs;gJ1AG^hmypv3Qhx zJZYKHOHcJq+UCec&i zTp$fq>?^Fm*ChIXKD^x6N)XUj5y-y=H_3Xkm1=oW*j}msv=+rc?^TrvJh3z7nzHavJDcZ&4%dI0UBWe$+PM@NG75QIyg8edR zY)J2|W82Flq9Oak*Zhe*JQwC~Ls@F+k`TDeYgswe1>?EJqbYK9v zu&dYz-C3O$wBoB+DXk)UwQ`3hR@yRH6a7kFg{tFC zwaiPyKTD9*B*Lh36$cEap^r9g^Q{o4-tvtOOgTJ0>9vS{cq#C;E^Nq!mv3ZvI4TuZ zQZ^?r;w%x;C|v2Sp*gN$^B8--?5Eqw2t~mr?Bm7y)%uJq9c$am+21`<$upVI))#xT z$cgZKuFJ-K49zcU|k!-1(U$ zUm6aUH0g+wd=p{=UiWpI!PBRISX`XHd%3&M-xn4V>a{Hgv1LjWbML8|!~#9C?fPa1 z-*pd4OGqPmYvJ|b-gN!<9fhlR=c{h`WC3;=KA${pR%cN-1Wc`&>pdGwj_~(%2ADBZ zbpJDi*(>UdbPLvubQ<-Ct>ihdRw5!W`$$XZL4DjT&Wi;=lp=OzFx9i;8@(2nUPAtm0EU}}~ofgwyo`!fB*?FmTC{VfDrJE z4lVE9UY9%V!RaPM=G^#s_H3JpY=n>2XFP40og&R^BJ{d-YX_2EMx zP`<$$rd!AzS$W?IC*{GL#I)!TCHV`)4xKIRZX-=mroG=N!}J9DUQd+VVSXOj|C8Sb z-~K2{@AlActI&3ZLmY@>MchG4s#TNeT*Dr=q?Aqqfx6wdrlyltA3O2Eq2%I~%8B<3 zx-nI|2n?l$R6RKa_gjZV4Xu`xm2pU|Z?s&bg+S$_nJA55wO_qD=1{1^kC}xW7@>u& zww)l5ltbsJE9=ZqhYuL3dU&OK%d&_1>)-g@D;(aY`#+jr$w!Y{T38?*r`ri{N{4EI z&}S68C$nB{tx*3)t1lbjy*u^hA3MAF8v%ngt=Yn-PVt=k_eNcj+{dwzV_6k#DarztEVjTi;r*|TVF{Bn!3F0L0rPG&BxZV}+Bt(A#5seN{mLwyxDLHB<6(pFpKNB-=P-Iq}u_`&=ywY9Z@uxzE!qpD7O`;zIq zU8u$l-(N$fI3Khz!PkB{LVSnR|75gad6~_#VJBl1gI!!+#A0EVwIQ7qOLlRrBW2?1 zR9p!>nh8%0Euf<}3auckZD%iDy%qWN=~MOiruZ4GjZRi|c<*JE{nCt4dX<^bqX@Kh z*!d2h-;Oq+2UQn2L+(DD#X~0J@NMGZX)*^X8M%|WNn+}Pf`aiZqQ`Y^Fxil8efAC` zW22?z#mKJeCjQiGgS%QIpxH3&yYB?uj04Wuq@)z{yD4VKV*zHkxtvy4(+R;TqjzfF z9~0V}f6Yegkwo_j1lpj;`Mk$o%+cXazA+wBtaNLwG82XI-2I%Vel_c$<5+>w$g62@ zTnBA==fj{IF%&#IjoS*#-?>%V6v=B*pmjT^x~asRng5wz;hcxtCWB(=)wG|)YE@2A z^?&&NXdk(;8g6{hmjst->w_2ENuufknzrew-X||8arMNJ6&cR&PvrXD+l?dw6iFsTmA$cwzFg1bWky+H&_iynMHh|5MHVK#$L$0vfz#hJ-su@q zvE^-L2E?)dMbq?UlC;M(xGBYtcGm_9P4;-ZO~{8Gp_)k{0V0{R4$Bn|f@0$0=Sm^yGO=13hor%-t}cKygtXIQSYk_uRIjViK6Q$r1NByL ztxV)cIEsu~V-yUzIf^JLc|7~Z#wk?E`AupPS%IbZw-G z)Vs@YqQ3O*bq7FMxpx#ltuup+8(kA`MFGAI6~}^}OnwlOzv{cm&Pi6|(5DAf%8H;E zJUpWjva=@q=1olbmIBlhi61>8)XF`r`Ti4E2~%VX>$yZJG0#d&c0l!H9N@fmXlSn1 zrt%ke?9N;?kWxU`bB5K1E=~0KYv@?Dj8GbH?Ux9HJaWs>8M8gb!{aiAC0fNDU*nL8 z>tL3Tti(GU6W%@2E zjHa*X-yKZPPS8ACB|M`?LLhwaufxL+Hq&vQhn|Q2&D3#~67cb)W$$}(Uz0AJIhi;- zG=n=*j$7hU@_Wrz2nq%iGHlcDFvP;&CIJS`EPl0?U+w+>v@u(Mp;I?Dc1(TsYKKia z{qY55x=f+IKlDlTDxk2sc$J+l+at9+FeMujr{LIA-Vvk!J+SrNb07)iPfLsZ*j~Ul zE~W@WKfBj(JP#jW8xuaI#lt7V4pIMu57bRlaC3}T3=_N~c=q%vItQsR|87nc_haK| z>U?x6*-ng=dTaos+vVJAWfV!iV^bKxA1c$~Lpv2P zBrbx4+qbQ$X&W1;h>5X-;+R0_M4zVyr%JxGl0Q_QwR9J71EW->r1L_i4WScz$frhV zvF3FtT&qt^am z9Rb1ODblM`lt(dUJt+Wz?mP6@J&(B? zq@I78T}MpA5v7d0LGsel4)mI-n06Oqf^;GYUC}C}Zq#wuOv4*(DsBed`^)=z@mGXP zyqrTQLq%?*Ub{zOtky$?fqUH|vH*&as|!2zLiPXJHt_Zm?6&Se5!dhM&H9Gw>%AGB z_Wdl+lb(okMr>^lcl~)p+077fI+4YJ{B4V>klwy5-f);nF&gJDo6+kEpKv#3vjdko zp@0oG*<{xdjZ(N-e0%4;GN|oMOo*F7mY#@7d|7dEV4dY*wPxdOW>F0-oKi+chDI!F zQEqUpi|hoTVFIcG06&A@1rv4NzHLt~9yP^FEq$QT(cykeL)~{=HY~#x(wL1spcZLz zZ$}{^tE2GRaTmlmCg;#Gmlzq-KQ*}Z+sP(FN4;<;Y_Al}dPQvLiy{3E`HY%8YG{^6 zBStgf+mk^e?L9ux_b!s3a^^jZaf!4uwXVHyrI0gX$7lU66FwEz%kfVT4adN@ma`9ij=L}<~UX7$P_ogn(- zIBM#MG%*)J^sdPJ`;}TK6@y2xq06uOcUkS}=^S+5r!b`!7Zqp~Ps?TLPrg3w^w3DV zdj__8oj6v6FlDi$p`qcAW6N0fRz*ODzkgB)*>!gQ4z${>#F+*MT$7iYEoSS6oI5JuZRs9ULFvR z(_Uxgo^Y0+*l}-QDEFLh36Vse%Xbb`zpol`Sk}u)ZPvXBc$m{Hfb_mxJniF*e zPQ_jo(+Mk4>b2Bkrr!yr#7z0_)?rfTr+en?0?0*Pg?1HJvi5$~ z=Z?FXw>0s;=g%i$UYKA_38h zu>O&>Z13bI@rp-Fr+2i!RW(;&ba?a}vi}#$>P)GHzhUIZl9C=zH7n-3QyvnmmqsMo z_QN#`O{*X+i6Tu7qBn4Ctbq95Z$wzZEETz@MF~M^UV{zW%TYA3I;_fm3nQtfrKL8k z``|zsEyE? z9^jlE{<-!Zd$3(8p>o5{K;5z3CPJtIAk{fxYd(yd-4;j}T)eoG#a+JQwor5_T9p}y zs1kVICw#j`zn@eJs5{v#6pnNl8r$0|mCfMkq5SsB-iSAxT z3?;CI@oO3k+hg<8(9jEE2CLg{0L3nMuzT*tv_VMt@vW68#ItP@ZE|{BV3KvFWgH@=OzPGWHo`Z}xX0=arT9WDQ z?O{4K9T^kar2ux|44voAUdR;Qd(jf2bn**-%@9$%O^(4ZiIAfVfMcsXPva`}-;3iA zI7V{eL`K zIgOAPKj##l;i@3&B-^Om_K7AukDK#X(wpbV7;u&cJwUv|^2#OtZ12t`6U2S13ypnq z-s@xK@TRhvPkiE%k*^AK!sfs&DJw<_*CWN(oTCva+#}eEsVEg+OE*A8?vTcuqAq zpv$a0h9+9I#`KMDL}*cQ$)nJcqxI7S0RCp_rlutyOo^j76%M}xxKSpYTK~+;Uq?BQ z8E@Fpv&q`Oz;Fr(O+IX%G1I&)G+w2VckNuZq$FZpV7!ebFKHROJ;4fE$F$gX$IQUo@!m??gIiWR)Vdgo{~8!mC(~sWdXfej-LU%g(Rup!_ z)8;dcf74E`d{_s;PgH6DK8$6H1&Fe}b@ef4#4pZth|LxVNh8-?<48jKTj^ijTB*s*!ywx&1Z7k+$RcYsKShH{Z751DllGkl&z6N zx_HL+!Mgs_eQ+GCUbnf;uI)0Oy{AY{$JT&*6Kx3zcRqNlW8vn83yxz`f)Lrh@y94E z#|Km6*x1mem>7U8u55>&D+kTmiDQw&uV~7c@R59j({$=SWz8?OGPH2qcWTNU#cM@M?B-x@IwJ7ms`^ zEcBYFkh~wP>#{05zP7VNT{Ru8jMy{*IiOgF8=E9_d1uK7~epd~oaUl)6y}95npYH|z%B!go)gE;<3REuNr2 z2l4AR5u2TVak(mOWr0b;&S83b`gvuBUDnQxtFI`WQvaBqj@`FWp}IX&Zr45PVP^@^?ZDy@+WMpM%WW{!f1blkxWQ(7C{?i0g zD=Rx4kB3G9ph=FM3_=mqtYIiuNfDb`DdRLis9>zDe+&5CcUP!qZok7czZ>)y!PBG{ zVj3z%+i*19p`!|FRLoxZ$NJEC7eHgXHZzTj!xqc=le>{SyOcFoH<}BtI~J-Y z@!W4|X-d}y^MB#}4YW#)yDOFiwQPogeI%gV)!%@s0zUe)vef$viIWtA!9{= za)wve6{OImr=@K!z7BNcJKDzn2FT!B*yVF)V(1!m-TLuaSvIht zHGkPSgB(nb{r0I-9{(1p4^``=7ScV@hiS+ebbpGU6LhEzuiq8SqgRDkYY@iLmeY>s z_-x6Ge6U^J)tRhjL=TTP?H*b<)CYE_TvyvHh9Rr&S5SQw66)LDuI!14***X5S)Wz$ zh_SJerart0NU1KH$F}moIuet!z%-ca`m>;znWzh>Z*#xe8$akNNkh@}=#mTV_tw#g z9a=FQ+^0D9HZYJZLeps2Os3dv|w~1lg&hpao^3u{ZmF(cPP0oum0a>-(Hb--=8}h$1ZBWxWUC8MXP8#Hakme7sqb> zF%`(f$R{s;e^Ig>`@*fku6X$8`Bg@4zo@HgD4)zuXVWUrg|z@Shwh$Z4~`BWA)zws zX==FGd}Ce8N5srg!#96~(k*MB&EPjy6R|7I=adUav|`lcuWST0#?UEqvuq!&p8tB3 zn4Krye*JT=`QSQtlHJ{XqmQ%NhAz08^?shGNp$Z&sT0c;)`?g83|tXlu+o-hi^dg< zp|$Sq-o8HT!@UG;5u~i~oCxypurzQ6gi?G-0#iCUL_=4eNA1v*Dm-8JQN8jg!5Ka= z>NU3C2jx^nY^>qDv~pWXnceulPQlOjPFllakRHaj(^Utj|LVa0_{nKOeAq#ql@q*3 z0HnkI$f7$&|8Ua;W1tlR5ve;wGOLy_O2|KN#qGSYInR-U!tH3q+}1mL{?wNX|6Aw# z;-Q9;8?v*J4{34iSz9mvABC5i?rnz9ivq=aBf+Yu1FI81S3=Q|QXd>8PwpT__}^c5 zaf#d7>Ppm=A+=$%IU?pS%{dsuto=Hs_e#y+eD~Lfaqg^P!{6U&+6@lfe5L*@2IM+^ z`0#@nT)a)U1EZLKn)jSW=Hz9zxsKNb9Mu!UynBsIB1ca|17{ZH2-W7juWmhh{NWR~ zVcPvfz4G&z{WaQi+aY0AI(TtFnh`8uQKO!d)}gBoCX$j;OG``Vu^Dls*nab=ozE^*XgZ zM8A`ex!674_p!3`8ybW(cB^NTyID#nvXi!AS$@l%anc2_d_RsWQFKW*6@Aw|y zuZx~9ekXS>fBWT8L8i~d4NpYJv;IJ#JBQnes2@Lm5Yl|nVI`LJV5FR* zLjm%5`!%0k0!G$0rg6Ld$P=MK7dh!wr&Snzyc;v(E}9ktuI1zr$;OcD=b4Ss!$#=g z8&_nb9WZ5?ARr_YP2$DRZDnD}( zlboHM9jkiC*Cbn+T6ZQ*>^#is&uZ z$QTyUDMvpEIg7iF@HX3~g|efRyH<*!V{?(89J)LS`3akFse z%@N(#2O5v+y^s*XHxd~1G~g_e-kWM2zLOqt(%19YccbL4G949HjA%AC51o>Nuf?D6 z3kntk7k|6ZWKPI_*+%MvY%qc>LdbeTQYqpnZt#dRV(x9Ra3{(m>pgUoGotCRufyf4 zg_^?-xJWo;j=h)efi_75xxRB}d8A`5j2-#VYa_h6nKwPIZx{rL~eoLov=T33z z2|S(7V7Tg54Oi4RWhVTG#5zn(N$6VKh53Ycas2WcHPwi3S~2H%<`d7scL(&x_qJmx zI~J>oTMh^O~U6pI^^j+|FaSF)*m`9Z9#!&zGr{ z;mf;y>jEUzy>TlYrQ7Ld|qLzk=?t&f0~fwP`cZP?DqY|i^(kOXU# zix?O@p1KP|NA#ufLmOulmXZfzA0@PIT0U29GG_B*KjgOwM?hK}H4}stDLpUAhpNmh z{jfmew|ONE0D32Nu*=BTY=95b9IzJ0n?UgH&v@!ZBzpx?>pyzQSsOu(96xQ81zNJq z>gmy*aG<94ms;L_6_$Z5S#d9d3n8F}yum#bf8Dt-?AD-b6$Ofo-DJ?iGzDuvk0;d1 zjB~^Xc2+p}VSBg?UeM{fNyxD80wV#PN>>-;==cPtNV$LbCI{^EVAGr#HfgWhch*30 zjfWgzv{UO?-KIQEem)>tE5I6#2bpssCY}sWx=UR;L;0_XXU_v=%{9cFlj;C9gUJ8M~qiG~dxlMW`EkOOF<7&ZIDPtJl>f^k*QX~sjqF4(uS_faUDCOC z{0~^T<@s0kts&%Z@}!9RacH`1u)!zhZ0L{jqE&Jr22hi->~X^9&jVc^7*kE697odu z8>tYJ_H%tM6iG*@cLb`XtB5ZNoC%Vw%Mui!5HZ>*jeC7;H)O1CUXA+w?DJ-Kk5^%s z4wX~2++}L&;A}w=kw8hw@a@hh@>Ol++_bK~$7)nu_H!6Pc8G3) z-*Q|Gk`b`D(GnPbL{cgqMQ+YG0ZhXJU}DGTwa1qp)G_hYZvLO;7Vlh03OH=R91ZmcKr>&f4)`vi(eX-v>TiQfs_4rgrbH7n4unMC)`8;8!E@T~!b zB}SXDlN+EgcJu>C7ppz=@Yhf!RQgg-`i~zIIwrm+!HLz(N>I#MS`(9}=^j$>arSG+;izs1??0uY<9+bc!i2?wIL6-RD>w!f(N+4E%DkO&6nw!5@) zubYSxWiKILFja_t+t|98yi`amL#~dR*o0Uo<{qR^b9e#cyue>%e!#6B(mCW>1-|0YUgD3BPm@KVdYb^FksWrBB{T_<-`W;z_*Fm6A$sd%uUWI@?Y5rn zB(1ESSPb}8{8Z|(l~Q+B@x9r(vO=@T$(Kqv)%ByO=tjWewK+yw zrO*x0j=1i=-lF1SPDS~xH^p-wE=RvQU1KR+-|HxLqXoSsPw9*zPKS3)^TGA=2I{5= z_{r!%;%$*T>p%JnLJn%j?c#6pz8`#0dm!-k%6edjj!ExlNe$SLb@)Inq&Br^CVauD zv5uK6<)jSG9rIE^8_~Bv=4)BujWa|j_!D4gtzI>J{|a3Y4np#US- zQD2Bh_LTi7x~A6o91*MFI4tAz_-SKp)EObYD4Fe+e{#au9~4Xl90g-D$w*p7^9uj* z0-X$yk_hlSdd;K~W8AFr>$fy)AtQ{GoALsE4W_g+5vUu5Ifo-?%rSW{p}nA>pq(!p z>mg;~=-9sRv+$SspPI@{I_ECM8h;}!vI94?(a`1|V+tL~#|Aex;pMdAlpKfEKwr|d zwxLXBWzBGx>Yh<6H8d5~5^y6+b46Y_b2=`s!i0{?o>V>GuwKIP#wuRM6~tXnGpbZg z-W5^wpsstC~qgl#pBh1!DYTwzfV1JzS

w(WW3iXt4#yannkO2KQUv^>H>MI(T z$8My$wab1yP`jZX$|vKT@tLMGtQ3>szoRO{2=4T<%HAnX@X+^R3I0q6Smf^46dxJZ z@NWWRPFqDDmFuZNQBQynOSz%*J;)!m?Gp+~LG~5+@;4k}zj7Va4TWer0ZA%^gVa9g z(M?fMT<{%gss5xK#u?romH_6Gna(p+jdEh8Nqn)mfxSF{!RU^BAKmLX^l%x3@L<{u zPP+%#JURxR+9%wV_j)qjRgm~qT9z-EiA_o~QS>CeKCa_2S98x}>tOywx)PylOYy*X z=g-D=-xWkZsxB?tB50efx4nhI1t?@m>ABP3HQUxCs|r4NbZ)x$>p6))JF416+1rm6 zVleutGV1qF$>nUX1cVUNDXs)&r3Ri1u&x_0B$uP8jAb>$hpK>pvPUo|qf-+of_$?? zjFFoW3Xh$$oY|&qq2myT+mcH{$5WE56Bxn~XPiJcFE|<)V%UIcF^n)N^SF8!e&qYq zNjbVaM+VLz$br{8ax0BEgADi&R{&L#5dt5A><4os_O{@LH|yE8^=rNMv{i+IO<jvcX0On{z**>!{rY(ypQ-Tt!;`g*rtmzANO z65B>LCr1g4tbEw8jxK8dyvWIv+&v^{wj&PzJ7V(Z3V2H%vHDy<&z8!A)F+(x?l9Ol zm~@Ahrr9W#=`d4!TnBbI^_t{bzCg zdr>ExqE8YOR_thD98o#jw4Fk*1@!HU3O2Y^=f`Pu37_fl<93tgznrju99&Q`|2$O(a8T>rfl z{2?nil)`v1o1kH5W>BnQKlSVHi#VDjWIFbE=Kr>zK!yRf3v6NYE}W&h)Lc9vLOc>w zqW!lo!aA%xgldM#Nf7v7cJ%-zA3~k9GAN5%<^pZ>ih(ngf8Uz`_uPY+XH1bSp0H4Z zX1^+T-{y$lPax0?O(3l?gH5|49@ypz5*%>v5B&WE&!-czv8c8I*k9(WMmi0I*fk#DXGL8v<>#mN{-`8so4Sjh{`ze%S^q;8z_qs3&v%BE^vb+4G zj~Q=n`=}|Nuq1P31|XB}P!(PH?1uT?rp~+UXL*cByv1M-y<|?LQ{?X#&?CO?;PNhl zp*3ys$-@*`H4Y9}O_b#vtmK zgnxsX{JRz)eo_b=YJ!mXW)4jXK1lcQj*#8?I1qFjr{JY3UPIJban*6Y=mo)vG}%ElorZTk6HfAhgzln%GS#i{ZC=ZtjBpmET}?fx2_Bs6AMn(^Kp z1AgaGW9*@o*N}sw@pA#(lOQQ-&DXi-@3WvFFxbrkOPA}W)mc}rb>j)iZ%nr}`5*Rg z76=Sx#>XnK6?JG3*{*4HV*kE<0WOJD%Q;AUGJT6_8?5}#ZS+SQC=B?I#5j_pVYIaQ z6x2?pE3BCRMJ@#33W4KKJ)2bew-BT43T%$V(m`>+ZedwFCfN0LZg=$G>%+14T=||X&Fc7_XXIMRo7B!_{3Gi~Pa2GjvNN#$A7Va=Q02eX zn$vyu>X0vETj7g=zUsvWxu?ha-s!s6M4Zg!iT*y^uPh5g5B;6Y%Fff4fm<(r-LC9`<}daBz0#n(t&;4e0q-6W@Nn?z>m7B7O9A6 H8T5YuJX2v8 literal 0 HcmV?d00001 diff --git a/docs/src/www/dags_acyclic_vs_cyclic-d1a669bf1b8b6bfa8ac3041788e81171.png b/docs/src/www/dags_acyclic_vs_cyclic-d1a669bf1b8b6bfa8ac3041788e81171.png new file mode 100644 index 0000000000000000000000000000000000000000..eafed015f49e208d7afefcc4f455d4d575c49dfc GIT binary patch literal 32457 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 Lmd$-)6!?DtKq?71 literal 0 HcmV?d00001 diff --git a/docs/src/www/ecophysio_coupling_diagram.png b/docs/src/www/ecophysio_coupling_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..6cd67d39027ca013fcc9d42b0776bf12dcb1a4f4 GIT binary patch literal 157835 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�U|^GBq#@yBl8TL|H@- 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 UGw;G@XB7Oiw{x_;!|+S|f4qBrV*mgE literal 0 HcmV?d00001 diff --git a/docs/src/www/output.png b/docs/src/www/output.png new file mode 100644 index 0000000000000000000000000000000000000000..94aa8786685d8dfd60bc9919739109035a4dea5b GIT binary patch literal 655567 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

nV)b%NDg7iDCjok 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 C`!&b_ literal 0 HcmV?d00001 From 343e57a352e8818d4b75521bc5178c51f0d54397 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 10 Mar 2025 14:56:37 +0100 Subject: [PATCH 097/147] Oops, file wasn't properly saved, ML image was missing --- docs/src/introduction/why_julia.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/introduction/why_julia.md b/docs/src/introduction/why_julia.md index 5abf3a280..b362ec235 100644 --- a/docs/src/introduction/why_julia.md +++ b/docs/src/introduction/why_julia.md @@ -60,7 +60,7 @@ Julia effectively solves this problem. While it might be a little harder to lear Everything can be done using Julia exclusively, so there is no need to learn two languages. No need to interface between them. Iteration speed doesn't suddenly grind to a halt if a low-level implementation is needed. A competent researcher-developer can move seamlessly from prototype to production, while still being able to focus on modeling and the actual plant side of things. -TODO image ML +![Language usage comparison for different ML packages (source: https://pde-on-gpu.vaw.ethz.ch/lecture1/)](l1_flux-vs-tensorflow.png) It seems we aren't the only ones to find Julia a good tool for our job. Other niches where Julia is gaining traction tend to be other computationally heavy areas with much active research, such as machine learning and climate modeling - areas where this balance of expressivity and performance is equally valuable. From 2bbab30d8ac85a2849aaf37144996fd9668043b3 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 10 Mar 2025 15:35:01 +0100 Subject: [PATCH 098/147] Fix ML image path, add MTG images --- docs/src/introduction/why_julia.md | 2 +- docs/src/prerequisites/key_concepts.md | 14 +- docs/src/www/Grassy_plant_MTG_vertical.svg | 1148 ++++++++++++++++++++ docs/src/www/Grassy_plant_scales.svg | 788 ++++++++++++++ docs/src/www/MTG_output.png | Bin 0 -> 67437 bytes 5 files changed, 1948 insertions(+), 4 deletions(-) create mode 100644 docs/src/www/Grassy_plant_MTG_vertical.svg create mode 100644 docs/src/www/Grassy_plant_scales.svg create mode 100644 docs/src/www/MTG_output.png diff --git a/docs/src/introduction/why_julia.md b/docs/src/introduction/why_julia.md index b362ec235..27689570e 100644 --- a/docs/src/introduction/why_julia.md +++ b/docs/src/introduction/why_julia.md @@ -60,7 +60,7 @@ Julia effectively solves this problem. While it might be a little harder to lear Everything can be done using Julia exclusively, so there is no need to learn two languages. No need to interface between them. Iteration speed doesn't suddenly grind to a halt if a low-level implementation is needed. A competent researcher-developer can move seamlessly from prototype to production, while still being able to focus on modeling and the actual plant side of things. -![Language usage comparison for different ML packages (source: https://pde-on-gpu.vaw.ethz.ch/lecture1/)](l1_flux-vs-tensorflow.png) +![Language usage comparison for different ML packages (source: https://pde-on-gpu.vaw.ethz.ch/lecture1/)](../www/l1_flux-vs-tensorflow.png) It seems we aren't the only ones to find Julia a good tool for our job. Other niches where Julia is gaining traction tend to be other computationally heavy areas with much active research, such as machine learning and climate modeling - areas where this balance of expressivity and performance is equally valuable. diff --git a/docs/src/prerequisites/key_concepts.md b/docs/src/prerequisites/key_concepts.md index e2f039ad6..1c44b72e0 100644 --- a/docs/src/prerequisites/key_concepts.md +++ b/docs/src/prerequisites/key_concepts.md @@ -127,14 +127,19 @@ You can read more about some practical differences as a user between single- and ### Multi-scale Tree Graphs +![Grassy plant and equivalent MTG](../www/Grassy_plant_MTG_vertical.svg) + Multi-scale Tree Graphs (MTG) are a data structure used to represent plants. A more detailed introduction to the format and its attributes can be found [in the MultiScaleTreeGraph.jl package documentation](https://vezy.github.io/MultiScaleTreeGraph.jl/stable/the_mtg/mtg_concept/). Multi-scale simulations can operate on MTG objects ; new nodes are added corresponding to new organs created during the plant's growth. -Another companion package, [PlantGeom.jl](https://github.com/VEZY/PlantGeom.jl), can also create MTG objects from .opf files (corresponding to the [Open Plant Format](https://amap-dev.cirad.fr/projects/xplo/wiki/The_opf_format_(*opf)), an alternate means of describing plants computationally). +You can see a basic display of an MTG by simply typing its name in the REPL. + +![example display of an MTG in PlantSimEngine](../www/MTG_output.png) + +!!! note + Another companion package, [PlantGeom.jl](https://github.com/VEZY/PlantGeom.jl), can also create MTG objects from .opf files (corresponding to the [Open Plant Format](https://amap-dev.cirad.fr/projects/xplo/wiki/The_opf_format_(*opf)), an alternate means of describing plants computationally). -TODO example ? -TODO image ? TODO lien avec AMAP ? #### Scale/symbol terminology ambiguity @@ -144,6 +149,8 @@ Multi-scale tree graphs have different terminology (see [Organ/Scale](@ref)): - a symbol corresponds to a PlantSimEngine scale, eg "Plant", "Root", and has nothing to do with the Julia programming language's definition of symbol (eg `:var`) - Scales are integers passed to the Node constructor describing the level of description of the tree graph object. They don't always have a one-to-one correspondence to a multi-scale simulation's scales, but are similar. +[!Three scale levels on an MTG, which differ from typical PlantSimEngine concept of scale](../www/Grassy_plant_scales.svg) + You can find a brief description of the MTG concepts [here](https://vezy.github.io/MultiScaleTreeGraph.jl/stable/the_mtg/mtg_concept/#Node-MTG-and-attributes). Other words are unfortunately reused in various contexts with different meanings: tree/leaf/root have a different meaning when talking about computer science data structure (eg, graphs, dependency graphs and trees). @@ -152,3 +159,4 @@ Other words are unfortunately reused in various contexts with different meanings In the majority of cases, you can assume the tree-related terminology refers to the biological terms, and that "organ" refer to plant organs, and "single-scale", "multi-scale" and "scale" to PlantSimEngine's concept of scales described in [Organ/Scale](@ref). MTG objects are mostly manipulated no a per-node basis, unless a model makes use of functions relating to MTG traversal, in which case you may expect computer science terminology. ### State machines +TODO ? \ No newline at end of file diff --git a/docs/src/www/Grassy_plant_MTG_vertical.svg b/docs/src/www/Grassy_plant_MTG_vertical.svg new file mode 100644 index 000000000..7a7d31eb1 --- /dev/null +++ b/docs/src/www/Grassy_plant_MTG_vertical.svg @@ -0,0 +1,1148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Diagram + MTG + + + + < + + < + + < + + < + + / + + / + + + + I1 + + I2 + + + + I3 + + + + I4 + + I5 + + I6 + + P + + A1 + + A2 + + + + + + + + / + + diff --git a/docs/src/www/Grassy_plant_scales.svg b/docs/src/www/Grassy_plant_scales.svg new file mode 100644 index 000000000..227ca4a64 --- /dev/null +++ b/docs/src/www/Grassy_plant_scales.svg @@ -0,0 +1,788 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + P + + A1 + + + + + + + A2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Plant + + Axes + (a) + (b) + Phytomer + (c) + + + + < + + < + + + + I1 + + + + I2 + + I3 + + I4 + + < + + I5 + + < + + I6 + + + + diff --git a/docs/src/www/MTG_output.png b/docs/src/www/MTG_output.png new file mode 100644 index 0000000000000000000000000000000000000000..36e660a4e44e8eee7cd6db1323ef469d1c1d95b1 GIT binary patch literal 67437 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 D4|B(| literal 0 HcmV?d00001 From 2e7136a8f29816bf9205c8d5b3781cf2f1fee8b3 Mon Sep 17 00:00:00 2001 From: Samuel-AMAP Date: Mon, 10 Mar 2025 16:20:48 +0100 Subject: [PATCH 099/147] Add more images explaining graphs, couplings, dependencies and fix descriptions --- docs/src/introduction/why_julia.md | 1 + docs/src/prerequisites/key_concepts.md | 35 ++++++++++++------ .../working_with_data/visualising_outputs.md | 3 +- ...E2DDAD-7B20-4FE2-AA36-7FAC950382A6-low.png | Bin 0 -> 74153 bytes docs/src/www/LAI_growth2.png | Bin 0 -> 43843 bytes docs/src/www/PBP_dependency_graph.png | Bin 0 -> 144385 bytes ...yclic-d1a669bf1b8b6bfa8ac3041788e81171.png | Bin 0 -> 32457 bytes docs/src/www/ecophysio_coupling_diagram.png | Bin 0 -> 157835 bytes docs/src/www/output.png | Bin 0 -> 655567 bytes 9 files changed, 25 insertions(+), 14 deletions(-) create mode 100644 docs/src/www/GUID-12E2DDAD-7B20-4FE2-AA36-7FAC950382A6-low.png create mode 100644 docs/src/www/LAI_growth2.png create mode 100644 docs/src/www/PBP_dependency_graph.png create mode 100644 docs/src/www/dags_acyclic_vs_cyclic-d1a669bf1b8b6bfa8ac3041788e81171.png create mode 100644 docs/src/www/ecophysio_coupling_diagram.png create mode 100644 docs/src/www/output.png diff --git a/docs/src/introduction/why_julia.md b/docs/src/introduction/why_julia.md index 27689570e..892421fc7 100644 --- a/docs/src/introduction/why_julia.md +++ b/docs/src/introduction/why_julia.md @@ -61,6 +61,7 @@ Julia effectively solves this problem. While it might be a little harder to lear Everything can be done using Julia exclusively, so there is no need to learn two languages. No need to interface between them. Iteration speed doesn't suddenly grind to a halt if a low-level implementation is needed. A competent researcher-developer can move seamlessly from prototype to production, while still being able to focus on modeling and the actual plant side of things. ![Language usage comparison for different ML packages (source: https://pde-on-gpu.vaw.ethz.ch/lecture1/)](../www/l1_flux-vs-tensorflow.png) +(Language usage comparison for different ML packages; source: https://pde-on-gpu.vaw.ethz.ch/lecture1/) It seems we aren't the only ones to find Julia a good tool for our job. Other niches where Julia is gaining traction tend to be other computationally heavy areas with much active research, such as machine learning and climate modeling - areas where this balance of expressivity and performance is equally valuable. diff --git a/docs/src/prerequisites/key_concepts.md b/docs/src/prerequisites/key_concepts.md index 1c44b72e0..5605d85b5 100644 --- a/docs/src/prerequisites/key_concepts.md +++ b/docs/src/prerequisites/key_concepts.md @@ -24,7 +24,7 @@ A process in this package defines a biological or physical phenomena. Think of a See [Implementing a new process](@ref) for a brief explanation on how to declare a new process. -## Models +### Models Models are then implemented for a particular process. @@ -38,34 +38,44 @@ To prepare a simulation, you declare a ModelList with whatever models you wish t For multi-scale simulations, models need to be tied to a particular scale when used. See the [Multiscale modeling](@ref) section below, or the [Multi-scale considerations](@ref) page for a more detailed description of multi-scale peculiarities. -### Variables, inputs, outputs, and simple model coupling +### Variables, inputs, outputs, and model coupling A model used in a simulation requires some input data and parameters, and will compute some other data which may be used by other models. Depending on what models are combined in a simulation, some variables may be inputs of some models, outputs of other models, only be part of intermediary computations, or be a user input to the whole simulation. -TODO exemple avec 2-3 modèles - -TODO image de graphe illustrant un couplage +[!Model coupling example](../www/GUID-12E2DDAD-7B20-4FE2-AA36-7FAC950382A6-low.png) +Model coupling example: each "node" is equivalent to a distinct PlantSimEngine model, "compute()" is equivalent to the model's "run!" function. Source: https://help.autodesk.com/view/MAYAUL/2016/ENU/?guid=__files_GUID_A9070270_9B5D_4511_8012_BC948149884D_htm ### Dependency graphs -Coupling models together in this fashion creates what is known as a Directed Acyclic Graph or DAG, a type of dependency graph. The order in which models are run is determined by the ordering of these models in that graph. +Coupling models together in this fashion creates what is known as a [Directed Acyclic Graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph) or DAG, a type of [dependency graph](https://en.wikipedia.org/wiki/Dependency_graph). The order in which models are run is determined by the ordering of these models in that graph. -TODO image +![Example DAG](../www/dags_acyclic_vs_cyclic-d1a669bf1b8b6bfa8ac3041788e81171.png) +A simple Directed Acyclic Graph, note the required absence of cycles. Source: https://www.astronomer.io/docs/learn/dags/ -PlantSimEngine creates this DAG under the hood by plugging the right variables in the right models. Users therefore only need to declare models, they do not need write the code to connect them as PlantSimEngine does that work for them. +PlantSimEngine creates this Directed Acyclic Graph under the hood by plugging the right variables in the right models. Users therefore only need to declare models, they do not need write the code to connect them as PlantSimEngine does that work for them, as long as the model coupling has no cyclic dependency. ### ["Hard" and "Soft" dependencies](@id hard_dependency_def) -Linking models by finding which output variables are used as input of another model handles many of the coupling situations that can occur (with more situations occurring with multi-scale models and variables), but what if two models are interdependent ? If they need to iterate on some computation and pass variables back and forth ? +Linking models by setting output variables from one model as input of another model handles many typical couplings (with more situations occurring with multi-scale models and variables), but what if two models are interdependent ? What if they need to iterate on some computation and pass variables back and forth ? + +You can find a typical example in a companion package: [PlantBioPhysics.jl](). An energy balance model, the [Monteith model](https://github.com/VEZY/PlantBiophysics.jl/blob/master/src/processes/energy/Monteith.jl), needs to [iteratively run a photosynthesis model](https://github.com/VEZY/PlantBiophysics.jl/blob/c1a75f294109d52dc619f764ce51c6ca1ea897e8/src/processes/energy/Monteith.jl#L154) in its `run!` function. + +See the illustration below of the way these models are interdependent: + +![Example of a coupling with cycles](../www/ecophysio_coupling_diagram.png) +Example of a coupling with a cycle. Source: TODO Model couplings that cause simulation to flow both ways break the 'acyclic' assumption of the dependency graph. PlantSimEngine handles this internally by not having those "heavily-coupled" models -called "hard dependencies" from now on- be part of the main dependency graph. Instead, they are made to be children nodes of the parent/ancestor model, which handles them internally, so they aren't tied to other nodes of the dependency graph. The resulting higher-level graph therefore only links models without any two-way interdependencies, and remains a directed graph, enabling a cohesive simulation order. The simpler couplings in that top-level graph are called "soft dependencies". -This approach does have implications when developing interdependent models : hard dependencies need to be made explicit, and the ancestor needs to call the hard dependency model's `run!` function explicitely in its own `run!` function. Hard dependency models therefore must have only one parent model. +![Hard dependency coupling visualization in PlantSimEngine](../www/PBP_dependency_graph.png) +How PlantSimEngine links these models under the hood. The red models ("hard dependencies") are not exposed in the final dependency graph, which only contains the blue "soft dependencies", and has no cycles. + +TODO discuss visualization -You can find a typical example in a companion package: [PlantBioPhysics.jl](). An energy balance model, the [Monteith model](https://github.com/VEZY/PlantBiophysics.jl/blob/master/src/processes/energy/Monteith.jl), needs to [iteratively run a photosynthesis model](https://github.com/VEZY/PlantBiophysics.jl/blob/c1a75f294109d52dc619f764ce51c6ca1ea897e8/src/processes/energy/Monteith.jl#L154) in its `run!` function. +This approach does have implications when developing interdependent models : hard dependencies need to be made explicit, and the ancestor needs to call the hard dependency model's `run!` function explicitely in its own `run!` function. Hard dependency models therefore must have only one parent model. This reliance on another process makes these models slightly more complex to develop and validate, and less versatile than simpler models. Occasional refactoring may be necessary to handle a hard dependency creeping up when adding new models to a simulation. @@ -128,12 +138,13 @@ You can read more about some practical differences as a user between single- and ### Multi-scale Tree Graphs ![Grassy plant and equivalent MTG](../www/Grassy_plant_MTG_vertical.svg) +A Grassy plant and its equivalent MTG Multi-scale Tree Graphs (MTG) are a data structure used to represent plants. A more detailed introduction to the format and its attributes can be found [in the MultiScaleTreeGraph.jl package documentation](https://vezy.github.io/MultiScaleTreeGraph.jl/stable/the_mtg/mtg_concept/). Multi-scale simulations can operate on MTG objects ; new nodes are added corresponding to new organs created during the plant's growth. -You can see a basic display of an MTG by simply typing its name in the REPL. +You can see a basic display of an MTG by simply typing its name in the REPL: ![example display of an MTG in PlantSimEngine](../www/MTG_output.png) diff --git a/docs/src/working_with_data/visualising_outputs.md b/docs/src/working_with_data/visualising_outputs.md index a26fb30c4..65e22e989 100644 --- a/docs/src/working_with_data/visualising_outputs.md +++ b/docs/src/working_with_data/visualising_outputs.md @@ -87,8 +87,7 @@ lines!(ax2, model[:TT_cu], model[:aPPFD], color=:firebrick1) fig ``` -TODO -! LAI Growth and light interception ../examples/LAI_growth2.png +![LAI Growth and light interception](../www/LAI_growth2.png) ## TimeStepTables and DataFrames diff --git a/docs/src/www/GUID-12E2DDAD-7B20-4FE2-AA36-7FAC950382A6-low.png b/docs/src/www/GUID-12E2DDAD-7B20-4FE2-AA36-7FAC950382A6-low.png new file mode 100644 index 0000000000000000000000000000000000000000..9c3caccc2b9a5129ecfa5ebb6636332e62145a2b GIT binary patch literal 74153 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%0A;gu@xL##%$f@ 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 literal 0 HcmV?d00001 diff --git a/docs/src/www/LAI_growth2.png b/docs/src/www/LAI_growth2.png new file mode 100644 index 0000000000000000000000000000000000000000..8f45a8c7cafdc7088d8ec23d5b174f7e0f5265bf GIT binary patch literal 43843 zcmd43Wn5M5*Dkt15CrK`x}{ON5m=2(*wYWNSaZ%h#~9bR#&sL2ASa22LWlx^K+vS6ph^(P6LJUyUIQ5(`~?pe z?-ls*^us$zDC80LFTF7*8Ui7QNJB+bT$A?}+;mjdACQg?_xNww$SG@8DHj6cnGGFhM&niN8QC;MoAjTp4!#Sfe>sv@_EjkDbhywSaDUztuTWpD$2`e&#@m9T4khQ zmqw*|)TXndmKGraLD$-`KsD;ox7k^(GL1qN22G^T&g=>AwTieH7``}R+KFpwCTJDm zBO?_0zpeksLHPMCCN6F}Ux>P0hVIqS^MB?g{%G_iQ*c{UY^>A6-SscnMLlT`MgW;@ zB{D0Ok&y{PC*gD48qQPBx!9XIjo?3MF*JaUkYZ7z)O0A-nu8F7RA72|_~`y}>G5W_ z#J1u1>8=9o4eEVFsFP$qcW=CI&akK?LZF%S%GtlNvhI(&$O^?^%b~+;%cLPKEzQfz zJNQ_wRrE?ewVjRW-?!Iv-#1<^)}IdYBRzldEjhWtX?OD5w{M83_#7UCWky{QB>a<( zeSxS1qGDo#?v2F$t?5&{_6bk*jbFd8;v(uCBuIEqyD1q7CL+*b&+gwZ5a8iD2YW24 zzCbgb8{3Ic<8)Zm>2SG{iW(Y{*J?^jUzcgj@A2lyr#*l^CfStD?aO41nsd0b{OB=e zBMiTy*bnV$K(=oy=lx%U$F9mdr!tWEhhUstLW5%vUP2P))aO2>+7s;j<_T$U5`U#rXi zK<#&Ne^*0jTER<636I!Ez?)NIm!bHoR%dZB-c`4Cw#lo0#N4W3AreZNz-~5ZUA^E8 zcF6R!#_t&jWM0maQzdDK^~EknTo7UO@5#b34MK~7Iz9)nl&p-!%xZ^-K7E>1)L6MEo&}{5;T(M zwFSFdcF(-C;dK~G7&TA={X+5!3MMS79NsxOabootGy7D49q5f7QNT43Ph>enWYLRO zh1WSNW3+&1q#K(zXjRSSl`Tpa{_QDZ7@qx?5IDoo)(_&}G_aVyBZcBIM7^i^UI1l_ zm7-m(->-KG%($rh&D7k)RC2JVE?&=_dhO*oUwT`&FszBSU91a1AP2q3%gfWPb96f! zkrclE8(pkhf3Vd2sQ-8~)gFo?CMKpS`2(~d>(E}DuI<_ExZu2X!=DrPp}F%4^%>@u zLs`MzEP^~N&PS(D4!)x#vVKPNEm(0ve3z$UGgH1!_IM@v_;8=1eb8_^7%BYl!-^Mz+ral>0}wOr;B&fR1TX@pFZsOXv$wCW-g%#p zlyr~}3(}&`vpUG7k}@2VT*xY@s;`wUcqq{S3M&LVW`thRtB&p`w(1T`@0Piyxnyej zCJV)f_5M^G52UW<%cDJU0etR3UwZ|7gheQs~uB7 zKRAIqrF{ndg(>6lv}+;Z+blazv7|x{k2pfwrf@vGwCbfc--ivuXw?;1X~4=q$7= z<zL-95ehG?rQj4!y3c9v;x^cozzhY=+@!w;GHgIdFcMbq}%m?1u>X zOsTGXCu@I`dSb=D`GR!hv}`e2dE&nPpnmhKPxLbyb`LpM_RU?;6VQS3h$qYYM=6Mq zyJY{h3zff~ilwKH_qMfw^ z!yXiohMIgq-EyRu9@= zoecQ*PO>;n<)gteTNy$IN%2d!?4?RNP2^oD90h0H5`XPdvULq+$=44lh%N6{>{yl;tw%5nock^5dCLTG7@fq9}lnzMZV4qOM8aAa zu=L?iizQsR>=3!67iXIc^$np@U9fdH*7F}=B9MVK&i4aMYKe1p#%k@Fr8lcE7nsr# zv`3`b*h{I<9V4gdPn>2NNT8Eo0hSwo$D0V*yJqzmy0Na8)z?R?V6hAyhy&k!9O6>f z^cBpJZGcz+WI3E?^}*w2iZjQD1T*mp`lm?RFtiXy#6c$NJu?RLaHauVB?S17hKLD6 z4`2ZLgC_6y1XhoK^*l{Ub)q!aj7*oio;@v8hPYA@QzAePGph(wg|3cEHX{U*IlNn2 zJOTuCpyM9r$UBa5`X-eLnxKdOjD+th->@~T2qLL)pHpS z2@+fQP<>rm0mgF;^R%8Zx{cel?z~hY6{|maCg}GeG#4WI435Pgw$3PSPG*~AS~Pee z@iapRg(1cyNX0UhOdJ}8-Cdypka*ZjN?j_m4HiE~{uS-HrH{XLnV>lE0NjMXHLNRk-FG27*n8Cxsj}g85U>)Qj#s!HVUh9aod_@~Db8 zF6(0m0^aJX&;RRtJ}te488ZwoY(@+pZa2A7jaEK%odPg@b|IMQFF_}^6ll;0HpsN_ zSQMGxrG`7Nler^wOlqI0X7f#oKpK#N)jK9LDBo8g32&Ub0=FOP@bq2+EGINZKhi`4 z;v)jQH#%%Nd9TU_Ej-Prizg1$>kvCm+ysbe5X@0PC;9v)3~!s;3i{3_Y@esC2$YfC z1?kX*#!3QD!Li;5gN!yyP4~kYV(3ULn9rBzcvcttulaH=`ca=8zQkP&cOtfH{LWH_O!dT>=VM9AtqPv4t2{!RU55w{cJA3ju9=wF69p}4KyXQAvw-+SB-kz;~hybMfhW$?IyAtrZjk$^e zKENmiQ6NiF;Z+2Wjty>l6gPh1dSASJn$Z10{UJy+=;`VE;#t)hVqs4*e&Xz~UH24Z zD(@P*&nX{-Cyhy0!cqJvh6mdbqTPIN9XL5TJuVNt9$a z(zhj4X_8X@dZc&hzMJ9AgP-f6(*EL}BDG~fF#7{YsGzX$YltJCu7!n#goK2-`PLH* zfl}ih?7Xw@+OhX1eXMi8tu5=a>gwv0bL0raI~`_rc6aem5F@)z=UH0Bc6N5!Ka@>a zn2=EW{veqoi0~?|PFy6t!U$f>9MRVCB&*OYnD_#vyzG2Z9TpRiJQBlwORJ-=&l)8K z(ue|;J3j!|f%^E5GPDFIN#!^4AexE#!HrujTfsOt;5pKbjKM0Mg@ z=#6De5@P1z;SmyQ+Qv_j>)JdG6vcQ5M}YsVKnHC-5&t7xhK~JqQSiF$rPq3<_a!d| zX>Gr0h2fs3pBACk$6`w^r9s;yIt^H6aka&Je7&FQe_6+xtieQ^3G$-;G~u+6&aW?x zI0y+?bZV?-*b??i`2Wzpf?eERE1BY9AyQc<%T*b%GFdWNqM${!0#zn_qc{4&*V$K* zOAHlc`<%x?EK}~Y=~C3IJcA(NunW6N&s|vO9gyg~w#ET@!i*Ky=eNF*snHe5m0?pw zUqV7dDKbHN$6@o5&oQ@cg^r%ykN^cTyHb<=w+%;FhhX9I^TEf$inm>%QNa_Mgmzg^ zA+NwRg@%T%tgL`E&T{^HeEcg+WNd8g=|Fhj0#(b&Mn_}WN~XQ6*2`1mu|_y*HBvd~ zdXN^(^!%pqn94K+j5@z|gb~;-)Kz6<7=m?v7suqbl@UB!U-KNomzv_xkbEtiGI6z2 z8jLIIY5L^dD>nqJ6z*lHD1?$#$6p)@?$683tvR36=8d4rL?U%Hm?<{_yS63v{_>1f zlQR$m=jH;2ASe7S^Rh2?qOI^((NhBkSWy~G4|k_SyE{8mmQ%&LVdBn^Mb?UJ z1MAj7>Ej*%MSA&AU#DZr_(MMduWonJ`I0FoJv}``?AV&Hg?l{Fsl?cOMEvEs@zR{8 z8Jy{d^%W4D{<#fu#Ec$s>oUpLIB?ect@$0|zEHx;JBdPgMAT(U;FfCC9{ZNOYj)1Y z?z3rgGr`kwd_8S_XjSMDMkENnjpz&-T1At_e>xN)cM8SXuCfm$YimKWTLQ^W_y$Z8 zq&7~|`R|>8r!C!V^>aGg8X?egHJmOr{8THz!=phP9T*g}=y^H-JP9)^>*c&RTzcCJ z>kajarmx=v1tt#VCN+Hpbx=^#qa;x#AMS6vzP^l(i&IlkaeTbLw4E-c5ES$R6H=g7 zBqJ?7)8yq5Nh+8vpLWnt!VbAAcKOsy&pm!;dGw@a2X8nH5w))r(v4$=1xtmKxU3U} zeI9;EM^j@UADi^e;dhyq z@iL7mL{lN`8j!!CFAIC3Uy~Dtp8?d&id}%r%C1r&2iKB%i>f9r-jOYvgwLXLwdiqZVq(I{!SQ!v zqt1&J(lWfUde)!l_v&WxK*!<9F!B^3qTMH-R=!VvyQj;HqNAhLi*>I5pt9Q4kfZqe z(6f(we%jeo9PzT*7x?akf5i9Z#4fzH)ttQo9lqtqE$UTn-lnd3mp+ zBuVXWSs=c6NL`I_ReCLJ!gD@^V1~k3^zvFmoDMLz=6&!Djg5`n-QDHn(6N}cD$VC> z?D3Ad5R3Ra;PzY4WvvOAeZEGJxbqPB!lpc-u%&hwfipZK$ugb&>#h|L90;UPN0|* z1+K*3=HfX-Xy2+W5Fjn-FcJ<4L0>Q<&NWI;AOPLB;PLVt3?!Eri}v7n9fC(a-xdlq z%qy(5aJrM1{&RyB!vhrvoDEGb0n#dYcDQe!qvqDzmbki#ylY7VoXzFj`qk4perreV>b(PX0j z0}M1yx}8dsA0957Jq`xo(w|r{4s_=V-CI32i%a?cdoLX((DvDQg%ZFhX+C8@DPlf6MTJjBLRE8fbJNn&($ia9 z$5=rhhAbW~tS#+WXA?Y~Z`(WuEs){-v!4-oFz^dw8P(%rVhT0NR_k_)*G|h4Nn7HH z3;Ak)GC4>LX=AI5fQdK^jmb0+Nk27c%`YiQV>9VZ=5+w1BIF3+=fIuwU?(*8=D`Md zZ(sQ@-vj?z_G#om40`-=-T^j5TwI)o5P8`_h`({)FYllh&L#8;ep0jLm3WWn4ilPM za5$JbQvn`-PV3pqfh2DEM2>A>6siodvSf9@<^`+uAvO4cv!_eR(rMUfqpTdC!EjX- zM=_DQRFf)!mwLU{>ZhupP--@mdb&CE2>k8<>7Pn`UzbmI^P@&Y{_Q7{9t*(PUX_Na zTfF{ADFuFq5RnQ%Yit(yqI=HfY?~7XOn0*cs+B=YOL2RG#q@5Ohx-gsUqMTTpe4%u zc0Lzm1JdMHwxogRSMp6OK}4|O+x^t4I2cpbmV{os4tva z{@r~)W0rOfd}|9L63gFxmLc!TsdhPlnKfI;pgp22FF0{~`H&z~V1GzP5chOQ)W*Vcp)oRBNuM?5Q5?(wE1 z0qyq7y?bd2J7}M%+<0s$a8b_>44z}SG0h7K*=&6aAq8vu81g;PocSp#Ft*PC znUW7TdRaxQkh>RN;!cXRlt5*~cvdZn&|q-`r2Ck|-pu@;e)0LJl}Kk`ug9jL*-a#? zTjZAQfWaF8zJ%Ov+d3OV{%-iyufD%m#$aGeJSCi3Mk1|#kU;Q!4Y0G?!Azguj~qgg zU0`mLK*k#BN~eKYDxg%l=<|3l9ZhRJQyv%s`0pjxH8lA>L`ZSR_KgEr!g}_VI*>Df ztu9Qxf$HD!MT?j+;O+W@aycUd1DEye*`&5DvrbK^!{(r9#%pfwsdbmWo}M=^p7-?d z0P~VVkI;^*=Y)zOfoakBJ|xe&7u+3#lX{zLM~B@$$N@_r|g|9Km$;|xxGX25z`)jVSUe|cxd=2j-%y2_fG8vSPP19W7} zEur4q5d-!Cve)5~8guBRxHPCEa|04dn)Kf)2H}TC1U_j$hk5FP(2%v7_I7Bap3lkO zo^1K8i30Vx!*5YhWv<7o3-vC-4_9lcQplL4U^v3@q;(~8Xu2^lr;rvF^Au+UQMpmc z&kqr<%m0#uk(E{c-Mc=({@rE#CNKq2Z5~uSSR2nqk5<~WJqCBct4s729N)RRx+*Ct znV4kteJ;tZtJ9*!KDHCB#{l##PAJs9G1UpPlQ?;;2uD9KG#1AX$^EQtRl>AFGiOY< z&qB^X+-fz1t~pb~@DI&rPkApd!L%W6!tg?WSHwoW5(PR?2>D;#+T8JWBEO4`j?TQp zkl<9OgH6M6C#Yg%Yd2kQ5ap>5Vq>p&DR_VZg0BSsI_U{Ygva{pm{DS5%8 zQ=!&;`N_@^-K1u5T#qr}+ez=u{e!7xcqrLW_Fo9;x7#PbB}Ya<`9RXnD3vvO(bVpb zSkTnu-5W#iu+jfi$S(oCklEdPXF10k!Jae#C*&-|poZJCc?>KKG)w(YmeDyVO^#W4 zNkf|aTUJ(E1N(SwsvR#enafesfX5D9}+r`fwvf$nZ4m`orBhx~1W&mDhQ z8&}B^2`K!eV;&kv~&mw)Ck_ zUtP#^CokuNk-`|wvURy7DqvX7BYz#I;6(u_4++IY(tSNy20V%{2udO% zt*L?@gK5H#r$fTr9=YKki}V`ZIR(4yMl!|Uym=$bNC2sfpD@%S7BifWx2sHQlE39OE48DfKp-E| z%Ql>?{Fv(D12LZicuLD~xUlBHo}y$SzWa$?m_pR|p*8&$5T^ucgmHa+_0na>zglTB9w5x3l z4Gq-_sz8p+&3y^H#ItA5PB#WTu2zC|Ydk0+l|_r0`cc&;1{zbLDUx6qZ%4|%`K!JH zWza2iQq8}l*x2R9J>Nln@8M$3mfz(-!28ZsTs%;NDIz?)@wk&{$$htIr7hUwLo?(p zlx*YPpfn+M|B;&%hJw=RK8EzzKe2_PnV8_$3hU6AR59g+XWy zu#$IOH-DDCIK`gc%15KL1Ik_JS6mRbVr^NR6Q(aZh;YlVWX@GVrk0A&?}}iv^L!CX z|D8Ty6a|pQ2>2k4^J&4lOfyP?dQyNd^`w|F+9*VkIRGSnSk9>kj!@Yg+GnQ`C2Z$I zee3Y1eE1W9i?0ua!xVDH5(YMYr}^)O!Ph8)Kyvao5*A1Zg+YGcix2Jl6ETTfE(bT9 zF|S{0I+%P7*oUfn;)u2RCVgMuDkhhTR?}2r140&0#6@XljETATdZ*13)^O?KFlZv+;|OqK9i9CO5_ugCNK`2XvEn4=}KwK^vhKo2&$ zJXo+^CdkM)_Y_07ST-YxuQfLY`N7 zIjJDZzkAmv4-dJ#qO*M$`yVJ-Q}oJ}wTdYwgrRhh1U?kWHg?SsSRvr3e6H8yudlD+ zKO5{#mz6Y}e93V^!9*^%Kxt;3@o|}Yb1#Ym_JH}jQVj|M8Q@^jUGoiMV`HrW0=vIC zPe@1*AeCprmrvr__s0+liRfg*f^e7eS%H54iVJ?A`X~UFTpgbowWCUcA{_+f6;NJt zZA0U3@9F|RQJ*%t@fK7z222dx?;z0MTWq`q>6vDwxnho-#t`zdH(*_xS!UUJh1@it*suo0nKAf_=?zoh)7NTz zo?3#=%4D64$qT~>J^sT6Wkto63s8z=D`>lLzX*CQJ)FE44QoluLNj;L;`MZ~-AfBd z^9t|)dlf@!YHG#$%|f)a8`qm@kzrv;uMI_|r9<0R>~5PN@Wc7a+F?$ytj`bHv`;Dd&?>CS9MPdj;r?2Qt%Xcc#xakF{2i>KgUP- z8o>f;0Djex*7JvMbK*?d{6LAa87n24e%b+@Qb`C@ilcYsO9g?Wa7Wb?^l4`Ome?%_x)(v4k zhjCNbNGA^tKHwHLPKx@wOjeN~Arb7_y#PZ?+nddN+;~jH=QsD9e?6%QnJ3r5rd~yb zMNja=WEgAH)oK_k%-G?-3|ib>jyRQ_lwLnMfDKjfN~7C+1ZzQ-o{rnh+VL%kQ|sR{C@^QAzEEJ zw+~o8MrLMNDXA@BFKLmK$e&khvW>+TG4gi2$FNIa6F4w_q7W`_<}?lUWWC>hvdprm zp);7w%MR+!t^P=WV)knBg#-LD(A5B%&Jm$#qI7F()?9Nd?8)IeR-_X#gj>1q4>55t zaMH*PWd?#5(?(8CPKbzzgy9vcH$UDV(!7h~SopNy zG=)aUky}|g?%)O>+K@eQWN=njQ%-TQ95psQF3O~cmVu+jL+oSLw<`z!@DiprjMR;O zQwK3I{~0S&5(FJZMMc9ZEkt;cwK%#ahJu5ylFjP=+T&AGs0e;HH|~=n4Ss3`XecNa zQ|}ro4e;>s2YHWMH~Lt)!G^brJ9{JKek;H>Kwh9-Tal^a0+0@HN-28~S& z88k@Yzv3@RYL$_fXQiY20k{>jG_Og=Tfi~0Mz8d~T^2@HP0@+FmHni0*mb+u38H;h zXv|Zj(7$Gmc(7d&H-%RScn0hd;Qq&~ZUH%}P}*XPyUUS`0S?$9#9<+0CmQe3bl>< zGoVIT(CreK^k4(Ab&*mrYjW=W1T8bLmsF6`F4Gg+lcio6dac;HxR9_{W3Wu2AH@sX z{_rm_{JaFn{v%|-p*rj(p^4W&#_+Ml9`4(SC2+TY01yGCQoAB{Zh+8RuE6zTs2U7Hfv^#7*=fh6(jXo~@=Nel!!9 zn1LpLTq3Dc5w0Xx?TItj(sJgZ^4Kv9@X-(y!l``I{Y#gh*nbpR5qY|L4fWvtBF<1b}qfy zvk9tjpE7>ANO?+}e}v5~d@wScj#*`b{?KA+fdD_V^AV^+#sL6~19$a3152=pZaS|z z0T;+-m>@PmptMX7`_jVn(91`@qs-?bp&V>ib9Kl*Cle(ggkeY(wCxS6d~^VnMEr&o z?g`@K%j>Pu5SRebQ)y`!A@6Onq7xry9qT`D^g|pLGOc%7?;+z(x&A3H6%Q)kUt6Ck zK#H-C5ug02Bh71NV>%K8quEzT7i^|;u+D4R$spwxj&ZE;4ffxIOc`rHBk|m{^JQm| zKVpJ+9t4nKK-4=rXM~5A9?Wmp{0AzT+%7;+PuGRhsW*(0hB_i_V*4$D0zpeGp7~HS zh=Sf;Gk_oN-}R7$RgJNH)3&m|pkXnx=gUV%5&;|b;^uFD9OXa(I1v0EfG{B}HrPxhEj95Qpi6z{K($%N__sDUK~4$d zovh`X`BAs&J-z5Z)C`ju5cof(A-_^KBWHP$1uOOH}J#e7u*jwuv!W%>~LEJsJ=Y zEo)^Me17ErSum~AVCFLMrf32)C*;S(?PRDul_-r2cx)Nl#fhY%md9%?VTo*45Rmr1 zmLLoCE&xKlwYw3}oOs96Np%9$vo-z52I!9T2o|Ph3Lq_;^Hcjl zZb(A2jvJ94^$ReG$u5XU@jN`mH5ruk*>v^4Ul(NOb{5iPYPem=rUX3?drycT72re! zX|gegM($4BtEb-4>%DZ+b<_tot`p#gxLDk);*x(b-*YpnBYWIar_?wAg#uC=_>-qW zE+m9edv>c7-X-L(r_)xzu=FF+RXZAZx>I4g>(~cei2IEwIaFM zwlXj=^$!h^3AoxhI&uJ7mcGhVH?JwbG9zE*A0_9{0y^&TX<0y!m6@e=Lo3^M>i?9J{D(+!`4f9I%z(WnIznrgOtxTgBY!m%Gt zT!V{O#`4w1W%Lv9NQ!#E=;9x}DArl$`tpJx`rF z9MmYRMIZyZj6R#{63q(J$wEzl*P&-eZ5Td4;Q};W+(CR5dCpbX2{8t_fk%Nn8~xoIPhktsT)Fj<6j(hK!q0qbh+kX07!w<6F|Yr z%F0e=fZYz8ABEba^m1D*{)U8fTUBcjt5KHo?n%@KhA=r7%otjGMw~_tN|Gdt*>duR zjA_d{4;q#xFrg>S*Z|r@EqK6yooL^*V!`6-v{X0juV|cg3^WF;ZGEVEOuv& zoH|L_JLCH4zozBcy;GX7H)%?=pWJ5N{HvFzU`XHRFL4m9{P0{q`ryikGvQ-5fk1`c z&Es|W&~L2Yp0z7|*61XVl=18E%upeZL!|+q8887h5N+EjB0LV(4^3ByGua(OrP57qhi#zSKscgMV~zj4H;VNLB!sSlluH6 z1nQab<8o@aI}m#@Z9tax8}gMW<*Ge3fu3iq4*9z79oe(AuCDOh3 zJ|4e-*HHvR02vbB(#ileeAIL_cZuUlS1{1pRB6fjkkUEk=jMid{fZlE=Sc#NhRC)P ztTv6Xls=P%|NJ(P@mp>0uIa+s+8PcHj?3QkNR0_d_t%05)K{#LF7wz335!J1_S=m4hR$g zw)p3)2*{-69EA;Hklm?%^=$#iax^ z7qrrDMRYPWy|$$3CVxhF?ab?`|8_&7MHfTQKI0bTEpp;vw( z;nyd4chnY5D{Jw)EF84LcD5d`9~2C`POtB>g)?-CSf869Sz2G!sQvelp89_t+APs1 z%e3$FdA!x9%r7nmlO-W22*l)!@9<{#dRZ+oIbaZrF!>`iPRaOS}6Cmn&-XJa_B4TQ~0SFI#A|h=C1xzxs zW|Kbra0#)ZuOvYes8w(1iUk8Q_6(T4p92Nx!t4uR991>a~fHDTA z1sES6-`X-aWBemIwEx=Q2@#Nb&|gYC`)X7x_ zThiG07jV%8D8((ceC@ol3#-$H8jT7>{;RWMBm^W{t9LKd)YYrGB!JIiw&|L7>mKM5v!}&s~4aCHT1oiir|n`@0)Gac(xbMiG|!#Kl?eEIM;gt)aVJS z-`LXB-?OK%;8>#)5@zaMY&Q>|dkXn1Hn`D7hgv#-RqfZ+E7bv3L4y;Ip>IhhR($^E z&$QDIP^zivkd8Y!`Gig;IJ9g}JoQG|q)Dz|0v{j$VtXuSyQr$g1Nb99Kll{6sRDJ@ zK+%!246c7B1@^iuZu7N&ozdXC<)52+Aejc2O_2D(zdwQg9K;k@=Kjy0|F8T=3k=hG z(d%-du&^*700D3}AR?6D5)fbza+rfDRDXn>C++R+Wnf{s*kY;FV(zjhT|jgV88NTa zU;-hF(TXcc53us^I={xYkF3We0SgNPBVS-O9Sh4~Z!cha@64Wp!>-^m1SS5|IdOog z!P%|8i?1|SAT#>$fy|Zrl{q&ne-e`PQ=lc{AbUv>e!ZA3=>#jlrX8k75o3m{osHuKS zE+s9k+2mP!%0&x|VIYyS!gK(LX;)6K0T?>k8u)B$RR>78r0 z#FCHK81}>o?~9vHS34Xp7tSu`9B0Zwm?ry^Z7?xDzPC_+35H8Wxw%sSN^s`eqghZD zpw!XROO1=0y!s0gBcS_#)AJ0%Joh_%b#<$QAg}b>Z=1#%`&;MULE5q(#p_SYva+b7 zN9vr;`q_;+>S&lCZ@Y{fDmKf@*(gW19f2h9<;#~D85tM?M=jrJ-u+VgC@zkOB`zt+ zEu>kbHZ?vzZN-&wR8pDdbup8Z<~@5R_%kyTxH5q{x~6}mLMx_2O}YQnCAQB&UEvN+ zzaU(|QA*g!Laf=_aH0NG_^#A_n4GjLkX5_W>CgrPZQYWisu&zR>Fq1}DWU0piGH(1 zwY&7FSM6G+m@&NVIKLj&SPV^0pl4thU2=PMY+uL04j_pDDFzG`10?M!v=p-B5E3F) zGR27d#kny+w)NBRTo8JMfZPxuj2+-{ftWsbY+J2ZXIp2d7(|}5!vAL$(iKUz1P%;m zAJ*EhN&Krfs+|Vt57-7wslQ?u((z%_(7Z;%Pq)6?6h}6d7@JVWVH0Iz0QK?S#ljp@ zMMWc?mB8zgq@-@PLq4;OA~!FihG+2!hK9!(X5-a|_GtMXeUI=Gwfdu=%y6P>$w(O` zC1S7JI_#(Otp=|H0s_(vt|=qd{@a>F7-D~4(paIy{qN2Nlph`0!B#9>zPO}h^VoHu zsj(B()jB#j7r?2BX3Quk<*~U{C=@1_qQ(w&xY9s62050}hs^02td`$= zqpmMX%G>e!-5>0a-{~rgsTGjZN{~klZ;hPIm^E~fsW;r@8?N^{H)jk;xVb^8XPeW4 zp4-zruklZwxNQC}Ex@TmFlZke;uzq&a%0%sKR=5x`BlyomI^GsoCyx2pz-D zDp~Y*^8+0{`!YK2>IGY;9o&TX)c9kmf-FIBkea7fC~6=;y1HHg-2;e`>+8=D{J?yI zAl}~IPQvfZAq10mJsl^R>o+#jF`r>^N>@6Iaz>6Q5sVm{elbFfg<=rOi2u;9Ccx# zRhdCTR|HMgcc3e@SbD{u6eF`U86IN3{1)X|Yi$5zxD~`C-;j_9;*2*&=7)=FY2Om0 z@%v&?Cpk$etr{f6aFU^-IIl)8_J2+2P23urjQGM4U~ryX$0W*ZZ`agPKpd$-HHVy% z(*JYFET87#vIutA$D?J&hDeR~(Bfn%4Sji9LcwQPE$Dp!y1dOu>@BkgXjQkfu6z+=|zDh8&%Q8F0qNK<{tvtH$q_ zMcj`>+%GhN&&)A7deqcyK-rLRqKO%I6ru6OlN-n6Ju0P}nO&X>pE8fwkbtPLF$b zv#X-;_H=X1WohJWo5P;c>lAHb^5ML4&uV8T+PM6k9=|&W9>Lg5#lGA`BAGXN=DUkI z!+z6Mp2)-XBSWn+c+@F~P7pw7P$)qO?!+v^$rB_xx{#Z*+LK$IJ_=ExG)gLMFFzeQ zU@L+bFRW)oLE2|uHud$TaoO-}6+xO&^;nrvJX6ajLiV7LdFQgAJI~FOU(!GAhlu!y zV-nn|?rkTTWTa~~LIlm$B($o&JY37?7o6@zK<;qC0YOZxs1Ye>(F2E|>$f0`=KQdV z`iAbt^6!7z^7geSg|DdxlBGXg>uc9JW*o15TQK5YTSej;Bb#krurk|C}~pRqjGZ8Txo57l?r3m$jVaD^HxPI-I|>=s49aL zps|~N8U^5fxFvJMrLjFPwvC)w(iFIZ%y`DDK_q7!PoZ%iFFncL?oiX(r-Vq-b1XFG(SB8@dahzpQfFSrL1z`w--iOj=l{b8;;Yu%;Z&5dPm|DEOSOoii= z?%7G7x@+wzW`>@;+Ahr;33_jL&FnVgrz(S&&wq{8>Xm-~ZhmI2RT0FZV{Ql+7kxU& z8&5_vzMGdDLM!(x5cT7Or~B$N0C>5qGvoEMf4smBHD3|UFZ>lLL`j*SmvQEp$Ps$w ziGoU16t{Qr+i&~4(iBbCM-Z%~W0oIH{Mc?uG*lEwy8jT^0SG#jqgfF$l+)_pV_a7= z+%)oVPmn{QJ=fP0eYR8JI3~;C!g4R{;X*jAl5h@p=VKd3t2>&K&4}9@@nHrPMMgy2 z+2n;i9HWL^o1vb#nS|-v`KLOH_=-_{eso_@zGzovDq!|zGMRjQN3Xjb=O{bMbQnw| z`aS#Or)Vx&cW*M&y@5vVSJLrqZmF9B`aTOAPpA1^ME?h0UmX>7`+YlfNDL_r0|-b- zcR8d;iNw&Qf=Gv?bV{l;QYs~_f`D`*Ae|y9AR;9qA_({J{oUV=>Oq4D_=8L)HRl!3($x7pu9*5;euydQU+YwzsaKYt&8?sAPS9+wVeoTkPDf>u2R%Jm02TX>{C^PwH+=L*id-IVCT|J>xCpEqN= zGeqzxiHs^$?q8j%P*%fd31$)&_T9VgP&>suTp=b-e_mv&-MP{;v9g*&s0MdQgPO)2 zNX31yXFdOrvGN1ijT?2kgd{2kDZ>3iKHsV7@WTl{e>fg1BqZ(r4%$sV5@tuOR`n^A{bwIKH#)ZVOpkj~@^!YaE{JixOW_9?qp5uQoNUc4g(# z#qWG~%nLIV{x*1h4n771}0TVa|SLU*Ps zmn^!Xj%4g6EkY)J`sMpgdDchzhy^b+y+>L3+?!+)Jxf zP^f;a$=gb1zf<>LKVl1S?vLF})?iD=A>+gd3oDhOI$rY%#S3S;C9ACbLfWX}(e+H? zmRa1m!NXI{!Yl9n(bg)bZr-0y|MuwnD)(eEV?NA7%JMfNG2PR=j{E!D2G#WwAMb_T zWl$EQmmtx+LQEWQb@q!YEKJCenv&9~*xg^An7Di(V0qd1pLe-s!llbXK7vN&A^Nec zr`->eZx_F4oU4D8f*fPftR~zKvU(Kx%^~i-J|&UD8QuN+)^v!NMEoCI7<`Gsm zizL*PwuDB_E(cMw7Oy_>a0H1yC?TMq$DE=Zo&Re=eyQhpqwGx>F+QYDB2Q&z{Q)r zSv}$S?XX9#+#8QiZoVP;4=F{X2-d*$<^Y^(x+`D(Q)Dx&O`i0#A!o9~4R6`pdbYY= zGs}Oibh2mv`iyqRh!S**MmKO{0cf*ng z!vQduY&zp<$_KS@aoMimAt)*Hr$1|~{`}b{lM9{&_$_Wml8HLEhgFjPA=;*8lvqvZLZjGzxvc zUu`Z3L zEhsw+=-*Bt`270W+S)9im2`}a&|Tt0ruH;}`hB&)GwFxh9TTOPR+jU>m-Cogi1sZ` zsmh_5p_3tm5*M${&tC#n zyTRwON6DrYxxi6{UI3u5YaYjsY505zZp7@CRa`8F;{G)~-p}(z$y*}dcU>*o9Uakj z9C^fPenD;j-VbeCNf}qRS$}(l3x||>i<1`K&x6s4uOpNlbrDf@(gXwdwkGQC<-gCi z7MC`4%1N$E#2IsN08VHULTP=-t@QC7%LMByLxwbppcW;R;8 zLq~p#$Gfe)FI2a8JC!xwqrUpg@&Usi6qE9Y6Sv2Uv=2njn-Pc%sEb3uzrp!)op!FA z-)c_+)u4|B0s$3#cW*B!(c8T^m%r=SLzfX?&X1oyvB>*-sj2kFy$i$3FCTmu2G_1xNJ!Qb zd1*S)*bvLjn~XNSdA7>BeNFHsnaH+A-xW-I`zP_z_Z`maC6nlx?>_gX0&v$D!J z(4~{K93f<|ucJsi&;IoshOZCX))tdW@_tVvHZpT*p+{)ml-;{ZxX&vWO?Yy&yStRB zU-}LjAr(nh^=<_qOI(CTjTvkrVAhLSF*nqEZR(qtn7qEPfRzl*ubiBrYdYw_=SFdV z{iD~dXI&%jDyTRpFg|^-x?>x*W{p?oNm-sNkD_IyN_@sG?CX|oLBvGCKpfuwyM88> z&)Q^b#mX#)p*)Jv?5Lqyn&48~7azM}7CR5SGyc1GX$7VaenM#dRPiP9WW8o_FfDl} z!X9tH8@e{oEC~Q_0y|sV3mz~fC0H1Wx-GRqdjN6kUO1jdLlXdxWs}5a=ZsI1U33aU z+L!dgCSMZZhDn^CWg1a@yyM`ca`Tc3JECX%(iagGj43fR9{VfZ0KsOmLYHD#Tk^}-r8VAEUQW(CJM#@y&A+ph z*y?@vZ5>r)ueGx%R zL)6*9``MSsv9__L#d4!XU=M9;*WJ+YcrobG^roY&?S$az7;@`ySxGynMMh}kdqMSz zt%TUSk3ZMrH8=^r>*l%8iS?cOCKYidSi1go=%9fzTabp$}p9RukV3cb)bq$9x(HGFZSDHOU7CUsBvTzvuniw&&^ z8r)vZvR(lRiRw|p;((!pkK<)&d$^RQ;mP%aks5~uw0`;Q8iLY z4~@>pSv+=fDpx6}J{D!Sri)Qo9vyXZ;tz2v7(r5CDUHlH5RxakdF&;U5@8`JsJjvz ziA+sNn!zP%IC^q&(^>w5UV)O9mYHG`Z>lCIWy8LDHnWsowX4;);{)rA{FD?n^?TK| zp*OT}k>%sHK^7fZ49d1I5~@2pupJ*vQt6xv!CTb8_E)Y9*fn zHG!YKF^I>YKMi)JMI69sD^_9O1RXURjRq9?-o2vZ{vsKAR2TL%2OIxj!Lv0F1B00b zxlivZ1*GWOYK<_dseugTX3oD~=%d7_JsJ+>d~(RPrj|F0N8ev#@$cKTUrsR7GPXhy92lBaD$}wW+R$t4p1H{}-;P8edu$wN^8Ku^D^QQcXhl z=4U6+wh){%VVa1_c4@r9LoI0X<|`;!Dypg$fy;5B_<7&@FClw5B{%o%lkDz~#a64b z$rjMEL6_S1LYEo;47vD9FT^I^3OoYvLQ1u;%C_=-sv=M1n>;H5E#Hi1%1obzgg| zI__CPD`RnzGw-b9;d(Rhw6^x~@i8*a;=9^1n@b@tR4fR$M>IAdT?3!n7thLq{{aBD zyYL89${wT|CTZZlxe1_ggL?WkD@zPU%BBN1RH^OF59r8oH`b3uBdrSc*m(N(X-LtY2h-f8Iv0D??Lit5KqdzmG1&x>VaTD zlazJf`-6kgx5Y0PIV;i~eKmE&F=uBKL3bNV&~=FtuQF_MKK`eN ziaHdadWMnlBS&JJtpi%)xq-p-N=YUYcjn}Kn z5%X_LCQHgyl34l%I!6k0%m>VTrMhgtPbd-gKFcY@q;1be27VKw10JQH-|t7qK%%h7+1?Y|w;$<__w-Yjslz0{h|hKu_y(>jwD zl}*VX++8<$+U@x`ac2Ir25+#cfNN5BodWIlU3 zP!p5TEp_F5{(EAdYaJPZF{t(>r|`Isl`gNrNq9-wO{nD`ZLzN1=nI{>xdy6;$byzL z)7^kiseH~mryR85cLjSE$YcrqZM zNWXraKlyNMoMJ4pA<=`Fg|WQb=qx_<1qTq!d}7 zmW2r}DFw<2TB%vG_#Z_b$W!_~9VnO{=Ru*|N9nspHgyi(zTM1s^%$NERgR> ztIy`K`quP#$^-b_4??x@o-Qpt@T~Stsj_Z(Jkk_)IAaGMAX<5!HgYX!r+YdA8Pe3k z=S%~L(hTv+IHCyMYhT{Cw6uRmC%&wCHw9j70GW}$|6ica{m8rBt-Zt+AyxQ}T0o#j z+~@gnXN`SQ!hOssC4V^9+FH-m4JH>+;{#%&+f=5edWPM_39^+s9+JA%^*;pUn5ek+ z8f=Vvn$OpXrc(KY=;9wpIC}`Si;zO=j&Kz#X)-#TV{?x4UOi2X_o!4bpJs3^6E!MswC8hH1BP5-qUmeuIoHKe0Z3-KG?yLQ7Nrm^eS-k%ttS zj)#wX%!t^>`AqO%qqDIwXlKW*9B%)+p6%L`#H!f!^XQBtNzMb-)uqdV^}e3y#+NVH z`|i-*zwN-|f7CPja?1XvMz#Mh#DGkSwV`1_W#X`cG?Sv@mI9{b@#jQ91n$~72)(eljFQjCD;>q_j0lOC~KN91Pn%9>(bLs~H#+r&US8GQw^<#NC2iS~2MK&(F01#s?9-2U}Xd&>?^?-rh_7AUz1(e*T(ymyJ51h6wC3>$IEhbh9n zSs_t3^~=U+XntF=Jq+Hdi{9j`Lr$MEDyDZn*A@bXH_ObsYO#gsWKR5uHf_!SRrri+ zOE!Z*7tGYvR8+b^O$qGU3#Sh=vlA$W8?Gpz8!y)~(9i(PIQ6BPns}p!K)S-r#+@jw zbfx18=J*&yM$BnqZ^;zOh;1K1lX`il5X`;tfu?Rr)VlD zN5E?q7dKJlNVne44Z(v(W}a>}oY6mK!ygmN{nC@&cyGx$;{snyQ@&*V=Y+AfUEx$u z&#=&{fwH|_%5QA^3B_0`z=p5*Y8oPvsc55KaWcs{oW5yc@s}UIu@lmb?x>^WUsw(Q zc8`em=&FIC&6bh553^d5%SK>X^K)?qMxwM$(um7sZ#^zuef_%H`@z9XP1M&rBrIAq z{9|9BJ)mK1%MDNGA-~RZp);HH(Yq;~G<1VhZ|`9 zm;o_cmSEt+f^sFz)ZfDq$E~q3#8Tr{a-OTX&UKBWHpO;RkYc~PKMv6aC%?LS57@B} ztqdZ(ZXO<%R5)|Dk2!!6)jX`*E(WQjMKXATBPnU-)BTD#?RGt?Bb;EginS-n^*nHg zRCrBS;*!BauJzgm0Oy0*#cRAp(-s8H3yn`0Q3?t6VTP8LXBTJ_7`?bcqaXOY3=9l9 zw~o`1muYCRtK~a$o;@cC4qoL!?E*%cK^M)7c_UV{^o|+fd%CTCP(qG@QW@*Uoy&?E-Om4kp>P7Km#v+1gs66=H6{)+UE-Eja#D4 z?TgiM)Hyq1DEdC(BMK=Id4?C^$`TH7fLMtl{478g@zQ5ZU#2qK7bmdT~n&XTyw$&Fk3tYYW=g^q?GNbI>1CU}& zLto_+?+1Z=S1|5pbr_kKBU>;kHK#G|NK-H*jVYpJYpN#p9T4dIR42GjN*`v| zD@Tn)v9!GnqoJp72tGdp=N24HgU8xj?SqY>?A~1?pauf}*&n=!wACjZ4+k?vpFMpF z`UZjY0poH^`|hU*-^>^6U0v_S{T+W`5PhH2oCUD4eO+EKQQ<6^Qk2z<{@p$Lvk$9k z+G==XovD&@e4ZqsXW;ds`3*#4n5ajCkrqcRGA)Qq6#*nmmkDf6>+sV`L#B~E! z{>qIH@>Sd|9%}X{JZ;aWR-Tk7YgD1bSp<%W{QTFNI zzl2J+Fm%`k;$kJ{^$n&KK(w275OX0JUbXF7DSFMT>RyxoMuefn+0pS`U>tT<7CQ7z z2imA02K#k(*8B;UcNSoj6-InWLd2imt_X1nBPO|-l|DVGeKDvVkc8jsgSor|fXFK1 z;#b=#_skj?*`A;V3O?N@`0Qxt32LDbkjIQu6zca2X~nF0Jp+3Do%S%ib#-uf)h^JZ z8yZ^Jrh{2AGBR0|{H|X&Hn?fyaI`J_fsA?Hdhp>pyzZ}OROh|mVOE7UCgl*=68PBM#WNLl053n?qiOlrw6n@)OvPVn@O>ljayz6Y6H`;a8)X$Dl`F8 z4|Fo5%rex*1jN$+6OtOJ-NM4cGMXgZ!Cg&q@4djaLPOwwIqN@;hawO`$9QJ%iBJFS zk0|$WmkZRa^fmqOOt*!H8V? zIP_u4enXIw=%vvmTx^G01g3Q2ufWeE0yc;Hp=vlGhetjK1QRCw3N2kKxCC#Dbyq*h z$}?ZSpXYk^ZNGa}x3vr^82bL}U`5;jpslu*pjjP1IQD=e5`%;j27FrogDS0qLZn_@ zR8-XOeJI;2zLoXkozFf3?H`9!Jw>BPJ%5osI$7x?w|E?CrhzfK2c3)SWrX+1jLfv+ za6jjScWY1nOgKum5%Lup^5p&!!5Bpd8vwS2o!!gHeE=ukdH084;S91`fcJ=(*EFQp{H38k1zc-ixo+(R^eFE-P*?U97*nOQuh5fm7_(#pvpy1x5aGT`k^M1l^L`FAm zs9(XDquXk;cgf7wq;5h*s_O7o8|R28J=D*b+ebEvq&;BQJN@kJRe+~>`-&;=*SR^4 zoFn{5*q_(<`JW5fgekFEn>+-ECaO|l=6(9RcS|aJtj-_nDzvve$`B<$Br-#PpOAdF zNeD2L2tZhkC_J2p8Wb?Uir7DLtiJ$ip??zh z0pKqX+U7v@QSZKLI*joFWA*64);s?XW;`?V?MhG;!|EbW;;z1kJ~ z$;FkK$;m0QJ9R{+B&_=i<`>6&mVu+N%8Y+DbDq;UBHv08!NUU}&>uNirG@&^K&p&p z4Vn=c&3M*-r$0y97Ok-%^`Fk`@9qeqnFcCtZJw*ppIJ2OgQXj1K^qe|#T;E0Y?v3A*N zWgrspp}%C;i}3o6%TG4_;9Y6|zcoIDtuP%vDXI1B!@8=S-12qnTEU;0zxfPG+urO9 zhsdk4Dd{EGbSqwbG4T}W@qnOtVc6<-ux=twp9P73oFsSG^tVH)M-1b|IBnH7$^Cm< zRSqs{1}X0P2KJ8=)pW@4$-K^S`P(v#?~5@%Z9Ybn8Adn{wXaa}D=Plc_sJO1)2!C; zS#k_ZO;3LhP06vbF#se@vpoT0HOUr13YB%qdy5Wm&i^atccfV$z{jvm&!o@C` z)GH0#mM0-Cl*YW0brWybFPnt^T+;I74C{uKnL(gt2hR9DM*v9KGP z7O8evnA^e7w?)?2SQf$_v8}SzD8gmse|Yl)V$l{AE>MeH!6^J@iHbrOgjnuwN)c{i zDyK5PdiX*03C(NwX$v0{ZI6rgp+%E4N};e2Z{ySQLVy5SrNvf(LuFluqth!Cn%wQq zW14j9W6SY_YN2mlB~|Kf)y2xFS2zFzF@s8oo136*s=};-{uh6HbhnibFJqazs#2&y5{uyWVqPPP|#|_&{D%L`TEk9U7CUu3N>r9Q-664aKWM zRLrDaZGZ%5R{vO{EMLD@GPzAo@pmTR0-{?PzbI4QK38~Dab5Xx`;ccbZrK3_yzna{1be;9%Wen^I*e{;cr3(WPO9qktK@GonA!EC13*p{z|b zt7C>=0_dl6%j{A!%abQ?pZd1!_+%>3h?{>7iM*7E%D0W^^?R)4*6yDv#*nCe?+LOL z_`C@bf`a1Wzq=Cp>itu@6gyP?-)Rd!B>) z(%uJnGchMe{=W6atKU>otY=YA09{v(Fz*z~-k6xAKkw`vc^L7EaVgXK5j3BG9Q`Jk z3~tE$wjmzWQI)nPG2Po@42K7zb+SjzM?zxMk&(G%VabWzqV01{utkTmzc3P`TFM(4 zAns(`_u*z5cV7%3@9c0ptuGE|du7+){PaV+HIINmMd9mfC<#Mj4z@OebN#vNL}`!o zuV!V9jM-Lchi_`JDu1SU>e5C^WCUdZn2H<2*$wj2>T00|^RvbCv-gFC-G{PNE3&{8 zo+mj<>{%miNoT5$QvtpTzVZoC+U}Lof`XB)1kpjl{acll>;^p4AP8Z9@~tr4k^vmq z?d(=TGy@`v3vXRxt4EUpD9V*fj*gAff7nXMjRIcjud2ApdR9xd9I<3&HAF}F%21D9u=PcK#z-MpWMpRttR_fCnVga3$OqI}R$V0h{~ly#XOlC_ z{5qVr%2Nz(IeZC?!o^2N7T`A-M?pDu#j6tqNn%@(_P5D_ad$66N+N3F)!>=02Q>V! zsC3~#dz_E}O6>9lkIEI0mLj^kyJs8r!1WRbpQIja5KEb*H!N8QN5#tJ^g zb>+%XuDqFGI*2$sy4i^KtJ7#Frf&;GW0Ey*S&_Fb7)di(X;+tI%I@C><_ai*8r_yV zz&{eTsEPYG&80riGnBd3tGspU%#%a`wg1Cd5~FWySDVp zH#$zf21OY7)b{lB2$B`zOc8{deBLM8KZj-e52ZeOL*~0vQmZiw?_CQBq=C0~iTl z=fI~=`1#|Tbsz!+!{L+^>a@EcNd>gZKNP9EUc}q4HA`kaC-fHjfj|d4%%E`W>J6b(ErihyK8WuBg`AU zO3Tan#lqM3UVkuFCv96xO_lD2T&ZxL` zfNJ+5d?PwCawroL49Wg~P!EHd9=QGE=|Nq>#>@NB>&<^xCTNtiwssTM70j#JhnEcI z-4zvuw74TDG96^mD_ir)Z|F#(R#sMedeq8R;jKFy&AoW>0w$;B$@&Ohy&C%=99@8b z`HOdCxOsT`tc4(VRm;-0F#+Fq@GZ6vDubTgeYjsxg;qAvo@m~@dFU^{`mFUwU=F$f z8LN`}osrU94>>55&d5m0eEe^-Y{JnSJlen{(u>W7>vmZKi(cX6q^X%WZ(WN5*+^V> z_rj_3ZA;6_3!Po)*iK{JG~xSJ)M3H z4Gk5nfU|B}DuAZ~JZT9D37t%lyFg9=W`Ws7S>f;EPKr<}H4q`8vYk!ZfnYHW@7*rw zE4YjhfD(0hH&P08f{Bo2QzMaVv~lu>*YlOh?CqB)lV%d2zwMP2{i} zU}6OH1IMBOa8F?DeUqq@c>&bIq=&Kb@rG+S*x10_Y5~gu11NyjJj|JSzNVz7tE;KO zK$ipAYW@d@Ugitq1+(OyhQ0E5n`|s+KVH8^Qe0n@KK=7!_{*d;Cf>j?0GZ6ll%mn} zxAy(@sI3i}OcXliK<4h}vSxl4Sc2g2w-`>Pk-R^9JWCts5v*2?zhWH0Ol+$5Bq ze;A=zSi?<=Qs|uwmYMfmO#l~x#CZIy56A1-06^3@jJ^Oa5P&w1{vA$Vphqwy^HPXS z#;;$$VAt5gNI=ty-ymO?cUua1OT>h-xM>Jv&@P5_X<@Bwvo*xt!9hD0hRJbVCords zrX1`BegFmo_QPk~dVBLTMV+oNQK=Yu|)#h-vISwQe_()*ho_{fYWMT|s^ee6X?G>z>4EF#>FMe9aQx%rJLGg@ zk4g+29ACI2Z4+)*|JcwaBC3~3>X_EUi~A#EOjQmfuu_B9Fl=YG^u|ul1}*Ukc*IV< z6v^b5mY(h2Bixs(Opx7vrd*yeXFd!1sT`^}0yuTMrXdG{Kn$@Ge6IyuVxy3(UEcJC zIk~Yk&~$a?`-MWiV@B;eLqiDMC;!3$G~rll_5#&K;7$W6hHAMR4Zl^6?WACus+$lz z;^+p`b|6GR0mBIJkOeR##ryyDbLV;OuSfjBp5u<5nHb{309E^HJG;W*B<>cdIe|%l zfcOZi7x)5#yY?JciFCYas!Fj2#yEX!hn`4dS_Jsx*R3g7*P|pz=j(?0kve{)gIY~%b_;9i-O9@d^e?mrqO-QO5)N*n3zuY@5JO`i@OeMRYh@SshO#;r~ z&j4Cc#t?6QIJI3?OP+hFbsRe7V#x5M1T@MN-f z&zL=!7E1?u^s=fJ8E|h%y9@Xg2wkaYX{W}<^bHL!rk(-_^y~ZNmTT~d87#N0M-xQ* zBBZ1<^%NZHwqu-4zn*&L%$AjnzO`4!S8tlK&nu8U_b0g&Zb=>)c7}%zq&%0#fYK7o zUxI$m+Ful|U~I4n2O!k{wlz{*T=(ECf;CMq?eU5c=StTz3D+q7*t-Mqak-kB&u>8vWe?W2Q(Y;bFYx_h3I6lHPCU0C55SJd8iNr{@4A&G?f`$+BK zpZ z*g{&m?_)Rz={_s_{cJ7OhrpbD` zq((~%-caU0@*u3a!ilSY_2a+=&cz%CZGySqlg52R-D8EwxGLuOxVKNNYbWbN8PD>i zF%dIgClrl=ws-OMQ9WwqR!IfQ!hUbM_h&HX4cG}iD@*FEHYX1mB}>aAG$NLk$RH-| z(l95y5z!^Mt`Y6#O|uL@?a}U{m7d-Rq=S%3z@{8}E`6KF>##1u7l@|?b+oR{cu_S{ zER2v;0~_y4g-%}2(cYfQ`IztXxq2#FbuE$`7 zT^$`^tQ(PD3L9=j|f z1x5W%qt2!0$ZJ$oIIt1|sfJMpEV z=;s|m(9s2$gfr=|{US4jadj~L2;L10NS-c!^@`f#^EYhAT7Li-VKy{;ciO-jpHcaq zLGr7B<84V05 zh{ZrDzioNe$C1QC{TNs|u)=`yrvv`qV6%G;c_yO#{|E7FoWOz}*L@ir+;rxDf)iMz zL)zZX!xN$zS*Jcog!-+b9&VIFWz{9x-nd=&ncW-{xG+C|3PRc@K=q*X#}Wd@DGi?y zII$QS8%IY(IDKjKhI|Ul(GY-yfIUZ@(=3{Zc2g<+n%$;;M6}%(nLj7*gB2u2-`CTH zM?hv*PfHHsZL<&43s?6T+fX#dS4fM}- zitS9MMy7H<>{tuY$^1HdwMtj%_z!uo5~s&Yy4wO4gAzwhb?i@sY;1V0!YG!(;{y#% zk?Qi7)0Et^$3HZtr_XvXGY=u6o>D^Oj&*+oufjc6_g6_kWgOH;%pbOk2+CL5t195s807#aAA2kpp@bU4n3CYO` z^sKC0Wwig3TT@cn25l>z9}fqg9ptU6*DvhDzd)-9WhgQ(D5p^R+SX*8u)N*d2Uz>g zrOO|m=iHw8AgPlZe>qmq__B}_Mu{MJ)!Uvo6`X-8)st9q$s+dJ-;|pRzJF$o5+n-> zD&RHb`G@ot4h;>(4sDw)?=ar?Wu1R!z3+DVXCG=t=_^;RNJ>fy3y*p|H_OCWTDr)9 zVFU|3{nZzr61uh-7;6F~U0QR7ABMWS|5+w@E{zkC1^gSBgUDc~CHvuM_?!Rc0u0v*3y*B> zeRj1r`L(^Z(?__ebFl?fb4#ta7h4q}odh{O6xJ=r`xh?tys1US#fsqC00DyqA}|4K zAnZG=gTOfC(a6?*KR2BONjUy~!}AA`hXdDQ;yg><;-Fp$2n+Yv3ZWFbM2V(g0t!~s z@3{v?y)b`{D^>IV!86o({w-E0R(Ied`UcV2?zyieoO+;rQA>Jgrx)R+K02z$Gcuua zZiH%y2>#E>kl~*oAtnZ_o?1#n>+Q&<5MdwY-OKFru2whkL*Jb@VCqBmhL?d~9-}p-C)jF!V+ozR(U_ zeQUp(8^H9%A{sfC7DE`*4dIrx^KF=+iHWr1Lf!VWCj+ubBB7RYM2_iwBq&`1fMu0nGD(cMDthaR3L$ zD`hds)p1?-gP|UUOA*PHY+OJ`+(>(jq5Ic*CxDJM}K=6a*P-}XGtExdT>R98j=q7fv}h#PPB3=9P>`>6GZk zh*z93-BOiMvfK1m8im#pcS%YYYwj2nILdfUGd#nU%#LxG;-qUE?6yAENZU9{b?+3?fOmk*2# z4A?k0Ul>?WpH5aTm79bM+X;E|{ykE+Wyi(#)&>`R!p-maBuYkUP~ddQgM#LrA=&1O zpi_!r#Ux?7y&gv7Z5`DZV|p<{tH97VA}Z<^@6=qij4+nG|6_;ZSttd^-rubh3yPIT z_++>axw88duz0Q9_t+n~|1NEWZ*3^f{=hYWXpxe8r$*BMm2Wn} zP`UENjg1Y6`6#IP096vuRSBF5*j=8udV16v6SL>V6M>v|JD49BK&AiZn#WL{)nKRv z9vxmXIUL-Ipnd}T#3#oTwWjhgb_xbuL9`lJ`w4l4mr8P%`q%Xpz=yrT-(&J(HJYoz z8?IVXBpCP#erSO589sW{ys6>tz7Ljcb&iv~nOaj*hA>hJvbR4_8Ul{^FZlc@c%8iB z|D6hH&8a57D;ix|JXgcerCmF^xL}`epD&|P9-hMPFQBK;bp=ovxM5b&ds+Z3X>h`+ z!vcNG`}a3tMz9rlh{?{EQ(l>J1eB9tMA-yV7#A18i?6#(LvzvThwT3MwI(35mjUwm`Zaj| z4<%Xs7c4mGbnP`bpMixRfQ|~^I`DqXgnroVnW?#0@GA;91Y>CQ3XXY&b!u8#B8$8X z4-a7*^gJ)-4Fad~!XX7lAV7tgrdjxDp}MJwDdf28VS~A)g+;}rV`RsJ(W%MFtErj? zmnI#-m-V6B%4J2F932?e^Zo@JOP9;%o|x`N)wrd$=*kbTsjxYARP%C-SNCUyaqz3H zf=c#(M#CdJyDXfZe`EiEMS z5)RWzSD}6>hQDa2uNmxL0X5tjeHmShgOUSCub7fwMGehQcJ1nQ^)m(T?Vm?SmkDuy z7vJ__{dCXR?Gf1&Tm#Hz^M%=WFajDnu^31ZUcKUnU+#Z;1(Fas7oPM1ckldUKuIr> z8p>F6lGHzBIb?Z3k5i~II!hdY+tko0M=kYHT*Skh^CJWg;7%m~FHUu0~Jb3PPy_{ zEiu9}>81Jw0&v<;Fk12%48)qiZxDuW0lDGsEWls7uM$1GI3+I!pN1Ca;eSS*AdzUf z-Nvn@-K5TF_W^;6m7gn@XALNc3A4@r-2zX}%ga;8=CGKkb0#E&E;yh$$T1rg_IWoP z?M`;fimfRG%tH0>5V>poCClhic>9XR4I+raqW9R?+LVPU&@$a6w|IDzylabB*&>Q7 zv#lw@(ye-aZ_}zT_+KIkKR%ELF4(KL@eFw3p^q_L0f37%zGjx)JKpd;7@3~wo zYfxJl-o#=ia&fvPDLyODXv`cJ-&PTiz<<|mm{fdadCYA;#y-U zc&=W*5qoj1sgm5a+~ zAw{Ssj^s*rF<#t2)K8DWEWBC<1RA}!c+M{P>CZ?08^xp$ zZ}L`-nHZ%5ok%az`2|ha)&wjZB#X;Fa$qI$GMr%(jXB9K_7gn7=pw-vioXc0(gx&g z7H!7|f7Ti2J(Jvx0v}gJ2Ix+VUrn8PKfKb%=&Hok z8BRX;;qlo&O$|vgM`{F<%-8?^lxL1yECj)~!!RY2McRvyfSg^PIW&|dAs#t)*wtk{ z!uDhSesd`f$^ts9uc(|qM-i~%4!mff&z8hdvg5D}qIY-C5bYgrrvTN)$*#H$D(GbB zB5Q(9u7>?=4Ng^47!lQ8KX-=srMyQ`E4JmPkxwb*j+j)~b$itbH=lCpMwus`{~P|F ze-mSNUu>p+e{NlZt2c3Z(iqY`%m>=gP|O=#G-_-d)cBZ2>i+)Rm{jBnDK7TBC%K1* z)2S`$)g^e4^}3cUJ0HliC0EZ*_J4mKaft8bljLD3;NmV7W$z6K zv-6XbmS)J&Lqz%OPfc^Z5UertK{|B_2YYWJ06FHvhU&sPSO*L)@KiWW-~f(Qsfe>C zis?Id@?T(QOVIm}iqgy0{p-0%TF^|K>2^Qw{FsZ3W_gh&*u^)(#J>_Us0Bb_t}piF z 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 literal 0 HcmV?d00001 diff --git a/docs/src/www/PBP_dependency_graph.png b/docs/src/www/PBP_dependency_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..bca94135818b9625683045e667f4b1ddcb7f07c9 GIT binary patch literal 144385 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