From c7eb03b9b242a1765d6e077ac7178d8b5b631289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Vezy?= Date: Sun, 6 Sep 2026 09:34:34 +0200 Subject: [PATCH 1/6] docs: migrate documentation to Bonito and add logo Use DocumenterBonito with a botanical homepage, the original PlantSimEngine logo, and a matching light/dark palette. Keep the new dependencies in the documentation environment and document local builds. Normalize nested page paths for search and finish static exports with working asset, anchor, and preview-version URLs. Check exported pages and session assets before deployment. Validation: 120 documentation checks passed; 79 pages and states verified; 1,421 fragment links resolved; all 67 navigation pages indexed. Browser checks covered search, API links, version navigation, light/dark themes, and the interactive graph. --- docs/Project.toml | 11 ++ docs/branding.md | 14 ++ docs/check_static_export.jl | 124 +++++++++++++++ docs/make.jl | 169 ++++++++++++--------- docs/src/API/public_symbols.md | 2 +- docs/src/assets/brand.css | 31 ++++ docs/src/assets/logo.png | Bin 0 -> 470640 bytes docs/src/developers.md | 35 ++++- docs/src/guides/graph_visualizer_editor.md | 5 +- docs/src/index.md | 15 +- 10 files changed, 322 insertions(+), 84 deletions(-) create mode 100644 docs/branding.md create mode 100644 docs/check_static_export.jl create mode 100644 docs/src/assets/brand.css create mode 100644 docs/src/assets/logo.png diff --git a/docs/Project.toml b/docs/Project.toml index 41aedce0e..e7a9dc1ee 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,8 +1,19 @@ [deps] +Bonito = "824d6782-a2ef-11e9-3a09-e5662e0c26f8" CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" MultiScaleTreeGraph = "dd4a991b-8a45-4075-bede-262ee62d5583" PlantMeteo = "4630fe09-e0fb-4da5-a846-781cb73437b6" PlantSimEngine = "9a576370-710b-4269-adf9-4f603a9c6423" + +[compat] +Bonito = "5.2" +Documenter = "1.11" +LiveServer = "1" +julia = "1.11" + +[sources] +PlantSimEngine = {path = ".."} diff --git a/docs/branding.md b/docs/branding.md new file mode 100644 index 000000000..8c27e89f1 --- /dev/null +++ b/docs/branding.md @@ -0,0 +1,14 @@ +# PlantSimEngine logo + +The logo combines a branching plant with three connection points. The leaves and roots represent the soil–plant–atmosphere systems modeled with PlantSimEngine; the connected nodes represent reusable processes and their coupling. The plant is deliberately species-neutral. + +The transparent PNG in `src/assets/logo.png` is used for the documentation header, landing page, and favicon. It was generated with Codex's built-in image-generation tool on 2026-09-06. It is a raster original, not an SVG. Keep the original alpha channel when preparing other sizes. + +## Generation prompt + +Use case: logo-design. +Asset type: Primary standalone logo mark for PlantSimEngine.jl, an open-source Julia scientific framework for coupling modular process models to simulate plants, their growth, and exchanges with soil and atmosphere. +Create ONE finished, distinctive professional symbol, on a genuinely transparent background with an alpha channel. No lettering. +Concept: a living branching plant that is also a small connected model graph. A clean upright gently curved central stem, three to four botanical leaf shapes on branches at different heights, an understated small branching root base, and exactly three restrained circular connection nodes integrated into the stem/branch junctions. The branching stem itself provides the graph edges; avoid a separate network floating around the plant. Convey modularity, exchange and growth. Species-neutral, not palm-specific. +Style: exceptionally clear flat vector-like brand mark, elegant scientific identity, generous negative space, coherent moderately bold smooth strokes, crisp geometric curves, a recognizable silhouette at 32 pixels. A slight botanical asymmetry makes it alive. Leaves should look organic and distinct, with no internal vein detail. Compact roughly circular/rounded-triangular composition, not a tall skinny sprout. Palette limited to deep teal #156F64, fresh green #64AF66 and a small warm gold #DEA748 accent on one node. Colors should read on both white and dark navy backgrounds. +Composition: icon alone centered on a square 1024x1024 transparent canvas; icon fills about 80 percent of frame with balanced clear margin. No text, no wordmark, no surrounding circle/badge, no gear, no literal motor, no circuit-board clutter, no gradients, no shadows, no 3D, no mockup, no decorative background, no watermark. Deliver the actual logo asset, not a presentation sheet. diff --git a/docs/check_static_export.jl b/docs/check_static_export.jl new file mode 100644 index 000000000..d0db9fd42 --- /dev/null +++ b/docs/check_static_export.jl @@ -0,0 +1,124 @@ +""" +Finish and validate the Bonito documentation export. + +Bonito 5.2 emits site-root-relative URLs even for nested Markdown pages. Give +each page an explicit site base, keep fragment links on their original page, +and adapt the theme's outline lookup. This also fixes asset URLs serialized +inside Bonito applications, which cannot be repaired by rewriting HTML alone. +""" +function finish_static_export(build_dir=joinpath(@__DIR__, "build")) + source_dir = joinpath(@__DIR__, "src") + pages = [ + replace(relpath(joinpath(root, file), source_dir), r"\.md$" => ".html") + for (root, _, files) in walkdir(source_dir) for file in files + if endswith(file, ".md") + ] + + # Preserve Bonito's scroll-aware outline after qualifying fragment links. + scripts = [ + joinpath(root, file) + for (root, _, files) in walkdir(joinpath(build_dir, "bonito")) for file in files + if occursin(r"^docs\d+\.js$", file) + ] + length(scripts) == 1 || error("Expected one Bonito documentation theme script") + script = only(scripts) + javascript = read(script, String) + old_lookup = "a.getAttribute(\"href\").slice(1)" + count(old_lookup, javascript) == 1 || error( + "Bonito's outline code changed; review the nested-page export adjustment", + ) + old_version_link = "return '' + esc(v) + \"\";" + count(old_version_link, javascript) == 1 || error( + "Bonito's version navigation changed; review the preview export adjustment", + ) + new_version_link = "return '' + esc(v) + \"\";" + # Exported package assets may be read-only. Write a site-owned script and + # leave the copied upstream asset intact. + patched_script = joinpath(build_dir, "assets", "docs-navigation.js") + write(patched_script, replace(javascript, old_lookup => "a.hash.slice(1)", old_version_link => new_version_link)) + original_script_url = replace(relpath(script, build_dir), '\\' => '/') + + for page in pages + path = joinpath(build_dir, page) + isfile(path) || error("Missing exported documentation page: $page") + html = read(path, String) + occursin(" '/') * "/" + page_url = replace(page, '\\' => '/') + html = replace(html, original_script_url => "assets/docs-navigation.js") + html = replace(html, "href=\"#" => "href=\"$page_url#") + + html = normalize_local_export_links(html, path, build_dir) + # Version metadata lives above dev/stable, and two levels above a PR preview. + version_loader = raw"""""" + versions_tag = r"]*\bsrc=\"\.\./versions\.js\"[^>]*>" + count(versions_tag, html) == 1 || error("Expected Bonito version metadata script in $page") + html = replace(html, versions_tag => version_loader) + extras = "" * + "" + count("", html) == 1 || error("Expected one HTML head in $page") + html = replace(html, "" => "" * extras; count=1) + html = replace(html, "" => ""; count=1) + write(path, html) + end + check_static_export(build_dir; pages) + return nothing +end + +function normalize_local_export_links(html, page_path, build_dir) + # Raw embeds and some Documenter contents links remain page-relative. + return replace(html, r"(?:href|src)=\"[^\"]*\"" => matched -> begin + attribute, reference = split(matched, "=\""; limit=2) + reference = chop(reference; tail=1) + occursin(r"^(?:[A-Za-z][A-Za-z0-9+.-]*:|/|#)", reference) && return matched + target = first(split(first(split(reference, '#')), '?')) + isempty(target) && return matched + ispath(joinpath(build_dir, target)) && return matched + resolved = normpath(joinpath(dirname(page_path), target)) + isfile(resolved) || return matched + relative = replace(relpath(resolved, build_dir), '\\' => '/') + suffix = reference[nextind(reference, lastindex(target)):end] + "$attribute=\"$relative$suffix\"" + end) +end + +function check_static_export(build_dir=joinpath(@__DIR__, "build"); pages) + binary_files = Set{String}() + for page in pages + html = read(joinpath(build_dir, page), String) + occursin(r"Bonito\.init_session\([^;]*,\s*false\);"s, html) || + error("Missing expected Bonito export bootstrap: $page") + markup = replace(html, r"]*>.*?"s => matched -> first(split(matched, '>'; limit=2)) * ">") + references = [m.captures[1] for m in eachmatch(r"(?:href|src)=\"([^\"]+)\"", markup)] + for matched in eachmatch(r"Bonito\.fetch_binary\([\"']([^\"']+)[\"']\)", html) + reference = matched.captures[1] + push!(references, reference) + push!(binary_files, reference) + end + for reference in references + startswith(reference, "//") && continue + occursin(r"^[A-Za-z][A-Za-z0-9+.-]*:", reference) && continue + target = first(split(first(split(reference, '#')), '?')) + isempty(target) && continue + target = replace(target, "%20" => " ", "&" => "&") + # The base itself deliberately leads from this page to the site root. + target == replace(relpath(build_dir, dirname(joinpath(build_dir, page))), '\\' => '/') * "/" && continue + ispath(joinpath(build_dir, target)) || error( + "Missing local export target $reference in $page", + ) + end + end + isempty(binary_files) && error("Static export contains no Bonito session data") + @info "Validated static documentation export" pages=length(pages) states=length(binary_files) + return nothing +end diff --git a/docs/make.jl b/docs/make.jl index 0dd08366c..751e0f063 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,9 +1,8 @@ -#using Pkg -#Pkg.develop("PlantSimEngine") using PlantSimEngine using PlantMeteo using DataFrames, CSV using Documenter +using Bonito using CairoMakie using PlantSimEngine.Examples @@ -29,104 +28,138 @@ build_model_graph_example() DocMeta.setdocmeta!(PlantSimEngine, :DocTestSetup, :(using PlantSimEngine, PlantMeteo, DataFrames, CSV, CairoMakie); recursive=true) +home = ( + name="PlantSimEngine.jl", + text="Build plant simulations from connected models", + tagline="Write reusable process models, couple them across organs and plants, " * + "and run soil–plant–atmosphere simulations in Julia.", + image="assets/logo.png", + actions=[ + (text="Run a simulation", link="journeys/users/one_object.html", theme="brand"), + (text="Write a model", link="journeys/modelers/basic_model.html", theme="alt"), + ], + features=[ + ( + title="Reusable process models", + details="Describe each process once, then couple models and compare scientific hypotheses.", + link="journeys/modelers/basic_model.html", + ), + ( + title="One leaf to many plants", + details="Use the same workflow for one object, a growing plant, or a scene of several plants.", + link="journeys/users/several_plants.html", + ), + ( + title="Processes on their own clocks", + details="Connect processes that run at different rates and share their environmental inputs.", + link="journeys/users/cadences.html", + ), + ], +) + makedocs(; modules=[PlantSimEngine], authors="Rémi Vezy and contributors", repo=Documenter.Remotes.GitHub("VirtualPlantLab", "PlantSimEngine.jl"), sitename="PlantSimEngine.jl", - format=Documenter.HTML(; - prettyurls=get(ENV, "CI", "false") == "true", - canonical="https://VirtualPlantLab.github.io/PlantSimEngine.jl", - edit_link="main", - assets=String[], - size_threshold=700000 + format=Bonito.DocumenterBonito(; + repo="github.com/VirtualPlantLab/PlantSimEngine.jl", + devbranch="main", + devurl="dev", + version=get(ENV, "GITHUB_REF_TYPE", "") == "tag" ? get(ENV, "GITHUB_REF_NAME", "dev") : "dev", + logo="assets/logo.png", + home, + description="Compose reusable process models into multiscale plant simulations in Julia.", ), pages=[ "Home" => "index.md", "Start here" => [ - "Why PlantSimEngine ?" => "./introduction/why_plantsimengine.md", - "Mental model" => "./journeys/users/mental_model.md", - "One object over time" => "./journeys/users/one_object.md", - "Several same-scale objects" => "./journeys/users/several_objects.md", + "Why PlantSimEngine ?" => "introduction/why_plantsimengine.md", + "Mental model" => "journeys/users/mental_model.md", + "One object over time" => "journeys/users/one_object.md", + "Several same-scale objects" => "journeys/users/several_objects.md", ], "Structure and composition" => [ - "One multiscale plant" => "./journeys/users/one_plant.md", - "Several plants" => "./journeys/users/several_plants.md", - "Value coupling" => "./guides/multiscale/value_coupling.md", - "Importing an MTG" => "./guides/multiscale/import_mtg.md", - "How composite models execute" => "./guides/multiscale/concepts.md", - "Visualizing structure" => "./guides/multiscale/visualizing_structure.md", + "One multiscale plant" => "journeys/users/one_plant.md", + "Several plants" => "journeys/users/several_plants.md", + "Value coupling" => "guides/multiscale/value_coupling.md", + "Importing an MTG" => "guides/multiscale/import_mtg.md", + "How composite models execute" => "guides/multiscale/concepts.md", + "Visualizing structure" => "guides/multiscale/visualizing_structure.md", ], "Environment and time" => [ - "Read an environment" => "./journeys/users/environments.md", - "Different model cadences" => "./journeys/users/cadences.md", - "Hourly, daily, and weekly" => "./guides/time/hourly_daily_weekly.md", - "Advanced configuration" => "./guides/time/advanced_time_environment.md", + "Read an environment" => "journeys/users/environments.md", + "Different model cadences" => "journeys/users/cadences.md", + "Hourly, daily, and weekly" => "guides/time/hourly_daily_weekly.md", + "Advanced configuration" => "guides/time/advanced_time_environment.md", ], "Dynamic and advanced simulations" => [ - "Modify plant structure" => "./journeys/users/structure_changes.md", - "Modify the environment" => "./journeys/users/mutable_environments.md", - "Control advanced execution" => "./journeys/users/advanced_execution.md", - "MAESPA-style synthesis" => "./journeys/users/maespa_synthesis.md", - "Part 2: roots and water" => "./tutorials/growing_plant/part2_roots_water.md", - "Part 3: debugging" => "./tutorials/growing_plant/part3_debugging.md", - "Manual calls" => "./guides/multiscale/manual_calls.md", - "Advanced coupling and hard dependencies" => "./step_by_step/advanced_coupling.md", + "Modify plant structure" => "journeys/users/structure_changes.md", + "Modify the environment" => "journeys/users/mutable_environments.md", + "Control advanced execution" => "journeys/users/advanced_execution.md", + "MAESPA-style synthesis" => "journeys/users/maespa_synthesis.md", + "Part 2: roots and water" => "tutorials/growing_plant/part2_roots_water.md", + "Part 3: debugging" => "tutorials/growing_plant/part3_debugging.md", + "Manual calls" => "guides/multiscale/manual_calls.md", + "Advanced coupling and hard dependencies" => "step_by_step/advanced_coupling.md", ], "Implement models" => [ - "New process or new hypothesis?" => "./step_by_step/implement_a_process.md", - "Basic contract and reuse" => "./journeys/modelers/basic_model.md", - "Keep scientific kernels readable" => "./guides/modelers/port_existing_model.md", - "Repository layout and test pyramid" => "./guides/modelers/repository_and_tests.md", - "Cross-object values" => "./journeys/modelers/cross_object_values.md", - "Choose a coupling mechanism" => "./guides/coupling.md", - "Model compatibility and replacement" => "./step_by_step/model_switching.md", - "Environment and cadence traits" => "./journeys/modelers/environment_and_cadence.md", - "Hard dependencies" => "./journeys/modelers/hard_dependencies.md", - "Mutable environment controllers" => "./journeys/modelers/mutable_environment.md", - "Stateful models" => "./guides/modelers/stateful_models.md", + "New process or new hypothesis?" => "step_by_step/implement_a_process.md", + "Basic contract and reuse" => "journeys/modelers/basic_model.md", + "Keep scientific kernels readable" => "guides/modelers/port_existing_model.md", + "Repository layout and test pyramid" => "guides/modelers/repository_and_tests.md", + "Cross-object values" => "journeys/modelers/cross_object_values.md", + "Choose a coupling mechanism" => "guides/coupling.md", + "Model compatibility and replacement" => "step_by_step/model_switching.md", + "Environment and cadence traits" => "journeys/modelers/environment_and_cadence.md", + "Hard dependencies" => "journeys/modelers/hard_dependencies.md", + "Mutable environment controllers" => "journeys/modelers/mutable_environment.md", + "Stateful models" => "guides/modelers/stateful_models.md", ], "Reference" => [ - "Installing PlantSimEngine" => "./prerequisites/installing_plantsimengine.md", - "Julia language basics" => "./prerequisites/julia_basics.md", - "Why Julia ?" => "./introduction/why_julia.md", + "Installing PlantSimEngine" => "prerequisites/installing_plantsimengine.md", + "Julia language basics" => "prerequisites/julia_basics.md", + "Why Julia ?" => "introduction/why_julia.md", "Model execution" => "model_execution.md", "Model traits" => "model_traits.md", - "Collecting and plotting outputs" => "./guides/data/outputs_plotting.md", - "Forcing observations" => "./guides/data/forcing_observations.md", - "Numerical reliability" => "./guides/data/numerical_reliability.md", - "Parameter fitting" => "./working_with_data/fitting.md", - "Graph editor" => "./guides/graph_visualizer_editor.md", - "Common errors" => "./troubleshooting/common_errors.md", - "Runtime contracts" => "./troubleshooting/runtime_contracts.md", - "Dependency cycles" => "./troubleshooting/dependency_cycles.md", - "Downstream testing" => "./troubleshooting_and_testing/downstream_tests.md", - "Environment backend extensions" => "./guides/extensions/environment_backends.md", + "Collecting and plotting outputs" => "guides/data/outputs_plotting.md", + "Forcing observations" => "guides/data/forcing_observations.md", + "Numerical reliability" => "guides/data/numerical_reliability.md", + "Parameter fitting" => "working_with_data/fitting.md", + "Graph editor" => "guides/graph_visualizer_editor.md", + "Common errors" => "troubleshooting/common_errors.md", + "Runtime contracts" => "troubleshooting/runtime_contracts.md", + "Dependency cycles" => "troubleshooting/dependency_cycles.md", + "Downstream testing" => "troubleshooting_and_testing/downstream_tests.md", + "Environment backend extensions" => "guides/extensions/environment_backends.md", "AI agent skill" => "agent_skill.md", - "Loaded model catalog" => "./API/model_catalog.md", - "Public API" => "./API/API_public.md", - "Public symbol inventory" => "./API/public_symbols.md", - "Example models" => "./API/API_examples.md", + "Loaded model catalog" => "API/model_catalog.md", + "Public API" => "API/API_public.md", + "Public symbol inventory" => "API/public_symbols.md", + "Example models" => "API/API_examples.md", ], "Migration" => [ "From the mapping runtime" => "migration_composite_model.md", ], "Maintainers" => [ "Developer guidelines" => "developers.md", - "Internal API" => "./API/API_private.md", - "Public API refinement decisions" => "./dev/public_api_refinement_decisions.md", - "Public API refinement completion audit" => "./dev/public_api_refinement_completion_audit.md", - "Composite model/object design" => "./dev/composite_model_design.md", - "Distributed output ownership" => "./dev/distributed_output_ownership.md", - "Composite model/object implementation plan" => "./dev/composite_model_implementation_plan.md", - "Composite model/object completion audit" => "./dev/composite_model_completion_audit.md", - "MAESPA-style composite-model example handoff" => "./dev/maespa_model_handoff.md", - "Code cleanup audit" => "./dev/code_cleanup_audit.md", - "Release notes handoff" => "./dev/release_notes_handoff.md", + "Internal API" => "API/API_private.md", + "Public API refinement decisions" => "dev/public_api_refinement_decisions.md", + "Public API refinement completion audit" => "dev/public_api_refinement_completion_audit.md", + "Composite model/object design" => "dev/composite_model_design.md", + "Distributed output ownership" => "dev/distributed_output_ownership.md", + "Composite model/object implementation plan" => "dev/composite_model_implementation_plan.md", + "Composite model/object completion audit" => "dev/composite_model_completion_audit.md", + "MAESPA-style composite-model example handoff" => "dev/maespa_model_handoff.md", + "Code cleanup audit" => "dev/code_cleanup_audit.md", + "Release notes handoff" => "dev/release_notes_handoff.md", "Roadmap" => "planned_features.md", ], ] ) +include(joinpath(@__DIR__, "check_static_export.jl")) +finish_static_export() + if get(ENV, "PLANTSIMENGINE_DOCS_BUILD_ONLY", "false") != "true" deploydocs(; repo="github.com/VirtualPlantLab/PlantSimEngine.jl.git", diff --git a/docs/src/API/public_symbols.md b/docs/src/API/public_symbols.md index 04d068a8e..b4a5abae7 100644 --- a/docs/src/API/public_symbols.md +++ b/docs/src/API/public_symbols.md @@ -2,7 +2,7 @@ This page records the supported default namespace and the five focused public submodules. Compiler representations and cache controls are intentionally -listed separately under [`PlantSimEngine.Advanced`](#advanced-namespace). +listed separately under [`PlantSimEngine.Advanced`](@ref "Advanced namespace"). `using PlantSimEngine` imports the ordinary model-author and simulation-user workflow plus the `Authoring`, `Diagnostics`, `GraphEditor`, `EnvironmentAPI`, diff --git a/docs/src/assets/brand.css b/docs/src/assets/brand.css new file mode 100644 index 000000000..dcad12d68 --- /dev/null +++ b/docs/src/assets/brand.css @@ -0,0 +1,31 @@ +/* PlantSimEngine's botanical palette; the Bonito layout stays unchanged. */ +:root { + --vp-c-brand-1: #156f64; + --vp-c-brand-2: #197b6f; + --vp-c-brand-3: #156f64; + --vp-c-brand-soft: rgba(21, 111, 100, 0.12); +} + +html.dark { + --vp-c-brand-1: #83cfa7; + --vp-c-brand-2: #197b6f; + --vp-c-brand-3: #156f64; + --vp-c-brand-soft: rgba(131, 207, 167, 0.14); +} + +.VPHero .name { + color: var(--vp-c-brand-1); + background: none; + -webkit-text-fill-color: currentColor; +} + +.VPHero .image img { + max-width: 320px; + height: auto; +} + +@media (max-width: 640px) { + .VPHero .image img { + max-width: 200px; + } +} diff --git a/docs/src/assets/logo.png b/docs/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5dbff7185efb9466f1b6f74a855474d9bbafb7b4 GIT binary patch literal 470640 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
  • dvmctqM>4=}1%r~W)gSW)b0F^Fc81oxr zssveeOo7DAc?O1$*$L!pJ+45#x6MBwXmj;q7n2j0SQ&TgO#TRiT)a0Kv+%bGmALUY zIMGXV9_EuReEOiYS|?I%pF?nMK=bCgkv8yVZB+7lq5I6M{P&U6;JELe1;*n!Q=8*p zuI@1Rt1$j|a8SGdHov`JZ2tHlY}~0_xnHTR$qK-WSTU9cGJ%mAW!S)-;-YuMfL=sVlwCNTv952X1Tf{$=l2moox)IT&Km!u!^3Jv zR!HAMm=P;fTaZ8pKcx{@AY3?xBMi&nvV5b^W2(R+RH%DxwClm#Vi*C*NMdl+O^Q0~SibV;mEBR(h$?I*1ToY2(Y$@df z2ed!${L|NVCZ*&bq@%^YTxS_xNUV@~G*O<%gk&?{gl#2LO=|8?;<3`gNHA$t*cF}dh#AZ2^6KF?zN8|$x23O=6 zF%VDRCW4g5^sDji!?fR4H!+&PvE1s<`Kguo~ zb5m}_&~%Yk&7raf4+4$m{IFje4f@eI1XWTJ{tYG4Ldkh_qSiJ}%#N~`6 zB7J6l9s!T)Aj4L`0~>Nb!M}Rrx9Bww zlq${ypv7OL+JaA78Kq`s%k4VqdMYyHmC-QA?snmFtAcb4!wem#@?-`H;w+Hv3vfa_ zmql>e5Lk5_&se95fLH{~ZjSHu2Nne5A>SQdci*Ez@#Lg@J`6>|GeWt0XgCy1GD3JTcmbeB6y2f zx<(%MjD&9^^PSigkA#m0kC?kY4<-|t+si&@qio$H+*P-SUtQG;6-PL5N zrO>C2@^TvP8iIapyGb8*6lVTJUsFB3l|k@+Nuz>anu0Qjyo4z1^?Oo|lOF-YU8#x>4PYIb+(9Fy9d^m2? zx`n*))0@&7Sje5kLcip0a)pRC0F?fj75P=A(T*TC3$t_otS~; zD&E83X+WH^+10&H5sPafb%OHm-~t24#4v;$PpTTKDVD`8V{{9IObUfzL(b|?ZKlgUMXats6Ykx6*Be}s*a zHNnD4&b-BjS@RmYKX+KONTmiu@ClbhViANd&mdO=00aP#5EW8J`DqFVwg;{8G5_nP z@1pjFTKQ5hcT5Z5X!7r-uHJxgNX_>w?I7;!EKjf$6u#;sJlo8*`*rw$=>bZEoIIbs z94{)$3s&dAtnQtv4+o)))45$yALGE)9N8)T@64xT zLkf?&bF<>vn|I=oc36bSX!N)VLk3Asz*NBY0sc@4aBJAZEb1*K7T=H#Bf^!NQcV#3cdlf0nwbMzb7QDo1F80qs zj}$k}TVw--Bggj2&N7(d!&?MP)4t;7bL^x0za59ABw5KRjX&~MNUDf_J~7v^2lc}O zISc&ZC=*`S>M8g?Q_OmRz5(nXYCoCzw{7kp*aXw1OV_y5M@>$%<48~tIS?RS2zK)7 z;|2hbDg9=#_`^hg=d#k9OzgJh0V@n>Bs6qp3?ktr2(DM&)|JgB#n)68UB|T&7!2L| z;V~I~%*(}<%w4Us>jzB>N!pMM#^P4~aRSa9U$41cx7GJVaa2Im{)~p|r9A=km|@7d zld{8~W@F_r#lK3$RCOO+VOC*=aiz|+?OtB{Du*`yFN~eflv;nL;=Yz~bEhrObKp^8 zbafW=?+xR0K2x3gQpptzNS{2 zSH?_?f>t#x)_}${^mkiza-nB`{INSemJ{PtR@6R>P`uY>Z0UXoIWH7Z=yGEDovW`pUGB2jc7hK1!;8l&$8&KK@Wa!P(UQGpcQ|_#%(L2uIn#qbeR(4 zUf6@=&ULP-bO?jTR>xW|u-K;s?Z#%tU!^T*DEmu18y+P+Lde+0i)sR0;Qy%51lV+^ zH}AeRPX|+%2uLpH|k)`+4_|2GJ^#>}Y z12vigTgfJbMBUanWYPG`&y|3Oqc?ddY|0JBgxku1LS9$OV22P?YmyjkT?cWJn2E(- z%wAW^v(!m%>ZQib9xGo_s#HJ(@{08URwOeSjKpbWF41*Kxco;>W6YpCT0T0dSUKfW z81L`BLTAAr&QhgbNG1+~6UPb(}8C@N`n(UmyO)4NUpemM=Bnti(ZueTTfaGoAx zsVr|D=7`uWf|m22mT~#TthEjw&T?U;Wqaiw?u`6o5Tpf6Vy675=6AmFX@B1%vEF74 zTWRejc(9*jR(xBfgRHt~YFE*#A^?4LhmK)@1TShi$X?f}Dx(-yK zshaTiGEjiao9WPM+be8pJp>lV1a&^;CvIfCx*cU%k;b=R!e{%<58$2fcXfA%9(t1j zyEgxWC%=Jh6UrL}BD?YTHXiN06!}Iae{_NDe4&yW=H%pxABVbjwZoo2pX2AFAIu)Z ztk?R$;mXvB-Eh5cuh3(P3LodLrq*UD8T(S94GnhtdSgvWgLrM-z!anHrQE~Dtxf|V zMNy6l;830vYLVflQHFM^CMVM^)F_KXLBM8UNjMm93rm*Pt#lSu=AY)c?N8{K5?wTb z9%^5)-xVMco)#y7@Ts z@}Z+0EjLPW-tymryY@G9#$#7^dKKqdE4S@t0BSmpfh0h2a|?2H1$@p(P;)4 zvz8Y!UL26H+_}yZ->4Pi(o4A4pHlR)$LAJ6ZZsFlg+(+!&<-D?$gW|LUB+x<^Y*d# zPYsNoD=)k^?qxF9@B)?j;kkr$yLkLx$Rizq<+cJ0>0oXH_XN_#u4x81YQx9$znWQ( zhF!nWnsZ+EKqW*b#gm-qU+wqwVq8-;{Uu!_6)bkscE|)djw|PFD7z!9BVi1%X?YUHo20?!d6{>PC zh|`ik%6fn)z#XsjitvP0WY&d$KMVLII4{Ri-SVNijl?t3)XTFURtgEkR~)1yp3o@! zx~p$Q+w{}-YK=nhQ?BRdU0-$iK#UO_Slr-$L`4$v zXs~X=3{EfDniFHxBKYK3D9Vc7E}FKkfEwHHwA!pGBGq%}wi{$+gYrWHy{-gdhzCK> zyHJpW8&uYtoy+P55UCUz=V>s(h)t5LRaCQC>;anD9G7pI&kV$-En+D99oZ(hN`v6c z!6H^x?zewQc=w~5grf@GeF3SEeH~`j5bKz>)0!7}C}z%kwhpZ!>MO>v3eX~3eHeD2 zw{w@7|2qR}PiG*Fby|kMbe!h{J1c$>Oa)}K_=eSWZ2`!bm|OCzKJQb5H|<|R0Dkus zKi^k|4VKZ_)}VeTnnIe4O1P@dovA4>E1YukiBtsmwjoAFhfmdAG8ie*A+D+4-H?H0 z=wY&Cq%;aSwa(_{k|H2d@xAO899VqMebL8Gf6RS+@+-yp{9ryg;#TN$b!i8 zVIWeqFgTttCs;j#eOz?E<>KD8NSOV2hMCbuXTrb+c`H1eKfL|P$u#m@#!OKrza@)$ zk3Wc@=vzs3?v=^@cxw2TK2zOo2W|A6GDwgff{sVpnJ{PI}t+%1nP`B+pXRA z@6I4iyTL;k;LCnes`sAOh+QD3@~y*oYBjyGb4z&{veu)wr6vF(D7>QuPz?gD9MJDu zqh^Yz^rQsqV{F6yGgcim0-#Zs#O$Mo&pgFrdeEq1xS9WlA+p~}6{ zocP!6{oeE#PX$X_!R8>!pJS1w@+8Sjac)Tu)jGXAz}E{(Un={O)>uO1rdKpQwP*Fi zU0j!(Ek|R;DZujcb#pWjG#*YM#5yT!qbWzzdb1hD%x?1}@o|-aMZelNVXyZ_z!Mw_ z2>w3uA=?nf?A1}&dX0bUakaDS_WA9E%D+*?=M@n19`IA<*H#pPpIICXTwo~bODAz9 zPHBI}vzd1cgVFp_$bem3>8ucw=%FS7tQ3+C0F6n4rTBvGD8h5sL5h40p~Xw^stn4} z1(AYI+lv2XOk!a0y#aZkY~H-1IH}h!@6FT9C(pB*G(QF9ilV|oWze%NZ)c#)XxoJl z#^CV=Ld)=t<1f$!?6SPQsj|{zueii;0j_!gW~=(}WQsr5=eND4>$41j4Fhc5Zc;>6pqG z%ASUzbFWbYUdIoF*B4ROj-T^48BE`N(565PafS4v1KfCP+PGO->~lIlFwv7X1LRFH5Bh0_Hr9f)urdN z4u`x+a9Fw58_eH9+Ps zBkbD(OE4T&;hxk!0iimNW$>P+!*nkvyrQNWtS;KYW4{9tvR6hjN7Dq9m^CyjrKi|Y zOgl|w^+YCVZ?PF&16cgM1zei6puw}Nkfph0V;Jh>vTLZ8632Cv#gMECGfLOQk6pY< zDX-^<;>e^`i975^g9&`ZMDD+qYntj;>?Q8#VFF>j^>r+e$|RIOf2Uhjz1D0;{wGu{ zd9XKNOb2v87GRVj5CeO+q~a3cV!Ju8z=)0XGa5_|d0Xu^&=<(X!DaZJjNnzv(6;_` z9t7lQPP1nDG27Pz=1}wWk79(~=sB;%rX{+rYyGx=i(EwzRpKi1Q+rh;&8^AbuCN>H zYeg-$-U$`^Oloe0W3Go7vla?z`j3Hk<7F5Jw)pt`FVhALP~VbeFcqJk`P&nE9OsGy1bRjz_D>fC zqJH+EBXHgGA^>g_FgD$0N&}j3PgoUU-<)xjqg$&?{nBuqcbI!dK%C;eJH=69VQ9w`==1M`? zSMS)we}_n}KCaaIzp5*suq=zk_}n4fp0#O?r z93)UnIGiJSyF_b~ZTR@}1=05h008zBXpJOM6b`Tm9!{oDsc9MY23Piblvaj9rkk~< zv{;7&)Zrl83pkxf2W;Bs& zc^?&qMLj%V_b16}A8@|p20I30E5fbokP&kS$Yn_2=VQHo)xsuoWsxl(_8bO+X`j$x z@oN6#cQ4;udzB8)%%hoDh3~H?$|$RZ40mF5xa28=3x;-)IV_D=T_$_)Ne(4x!#5eU z=^C!@$ro@0I{(YBHn%aXr~Kh@Ow~bE&WiHwL;%kDF6uZT1HvOJDh0LF7$Y_GuPDxpLQP4eb{Yu)h6~AX9fVz>}K3#@1!T@pM<6m=D?-E z|3;j(zs?{|9dCpW_cpH=BN_})TyRR<$iS z>sePU^@GkgBL`Q+DYAF^1RT(XTgX`O z%mr@3_j1?6!Y(>BWlZU^cy5Cuu~ok1Y%IEPT!?>J|C}%esToqtwjs%)Vn6Fu4PHtK zd2S-#um6u_$YKpz@MJuSQ*uklc?Xv&5@y{iXDCE8pp`K~(UXQ`nM$^VJ=CC65s_%I z#$6J=2w&HkgH0!b!yCrctS>$}BcWXwEP}I5UzwACLP=I~9Cf@A!W|9g9vsl;Zr#u< zs;*1_yj+O(YX!6vQeQ%^?96fKjZ6run`cdiIGyvCc6f@e0PG-$DA3)I0T2NUzc?b0 zSH+Q65HoSC*KSJw>KSm|{(0tx8&Io6D|dP<9ESrkA>kxei_ch#Qy$$v^6EjPO~;qi zVa(#ce22ZFS>NKHLZx9=O^A2CK3gsUa{w-go!;8a_MhA&x!e7>c>WCXg!BD!lfz2u zglHbGO#4NwN!b=&EUxq)djumcS6ft%S56kNU);c;U;o|p{=4b<#2`_7Ipf)DCT6U< z#7$z`k_3h%9(*{qkI{d-F-#1*TVEUW2MGy@q`UQqeq79G9A8DFK%Eg@w7PTvc%nwM zrS&Bawh+*q}$<=N5iQ&~;bl$L{(#cs&w z#%l(K=4!X|5pinP54W!j1c0T(+R)pqB^Fjzkxs#Uf2KIDG0>e7APLJ0?^wJ)n%D1KD|Jx@+K2WmRXXsAX`GhJw+kW*jyr+YX0A%FIF8SvZ zp%P_awmCLCKBO1P5HmWzKe=TJ(W5w2W}k&N0*H-rxMeR_e@Q~SrXp0hpP@6aj7!MS zk^QEkzIBbH&F611_HxhzH5WSpi>v%Q&2j;4H&ixGuLEejJG$y=?(?{S1-Dz>nTY4x zVknFj4U~gG98((lPIFMLHbnbvmzEI9B#+|sJhiE55I}gjqh6S1b6r;yv7(Xan54DI z7tH}VjB?4NV~Xee`>8=OEdqx>IYo#7Yi(QCR3uIDKgPQ&h#BVnP>d=t-d&Ldxaps(FIHMf3TCau)5C^N}B!@|0qH;bdERjwj|9c*+Z;>!G1>5u3s zdF_L<($qsfz#yyOD#sOL{04_`fZKmejr+ky7@F-sU!aIEjMA(}0bMwK%C@GtKo<%X z!61B{#>_~ZyzM2JlwTmf!E-5hc0Q{m>CIa zG?HWPlt7o$(n2+k}|r`F@zi31(V)vl>YOGQ*luQ}V4i#*f+8X@ z>3dg7Epy1ZRkolqFdbJj4eOaw;)R_QJalKa%Gu%ps!x)R&QJyi6$k>%OquQmYmCd)PN2=>#_Z9JuDQ-(?ap zeX#5FjS7wbke)#6WCvH*O3#+3kbj)-bZUBK3`J3^ z0jB`5ssc1l)gj!)LqS4l=n)a^iph0E`OneDUL9Iwfa;Pb3v-4v zd6I`a=R!zFnB?W}Q`}8AW~9+CmJI2W{gFqL%{H|!x3^5ZiK!W8Q`0SZGYbhO7Nchn zxBW<(YrySr0$nbjS}m8;jb|o3$GBkZs-X#8c%0g;DI`H%Fq{51bh-;5Htd|6EYp;k zfLj6j9>mu+2{U?S_1_B_jX zw?9Xb;@9&Dq049{p_ly*_@5{>QYx3RknLEd!-M=bDr9P&t2$W;wJ0o6NpQm!#e{;S z$wv1b=G!#>ec#>{Q$i2dD^h^|Fo+2};7XW8bOsV$O~ypD4c6i2x~p7Q)G254`tv>K z0#5hmKf`3DqV4Y$zf)PwCP*|iw0c;1gRM4J@0^O)5TfOo^W;1Wu~XF;@MiA77j66d zWn4dQDU@96&nwT>P$AB3sn50Va{mHO15emd8Ck(<&V*?qvo5gm>fsuL#(sGb^qIK zvkhw1r*HXF9+ziCQe&L62y+IM|2sSbeXLgufU(uaDBCF(?Gm7GDh{&dv*yWha(V-L z8xv%mE$eZAIkdAm8L>Y!WL#!N0SV=fAmyhhXcgg(z_fybanaWZBa9c~{DY+?NdHIQ zMTBz=D*5ku2&aB+Y_kHi(5n33^rQILmGUSq`%OdkCK4c_2oNYa(%`6YWxGhdS|J$S&Z)G^zD(h6zIic?l?6J5#H9(V z{@iA&;d2OuS*@-M$ouF0_MJqMIc_lu{P{z$8vv z!yZeSY@8OBu)7y^`IpYbD{tL)UtM%YorsdYaVQ8`klbS2^T^@-uex6NTvHA|^&Da2 z&~gOap@ZDT0l&csNU}RDSR|1{VbP`QLgMA6~Q+)D>rTAvd&mkf% z8m7#@@NCnYe7dR=;ih8xT5VX?x;2>K_Glt55CL2$b#X3#5C6S-rH-z%s~Whvu6%4q zP{`Ijyf2Em=yx&?|5W2f7-k~2sZBQyWBg|5EDXQuzw8S_y?g0g~=TBO#jX~*1|yGjBwIr=%o;{D;5s|^~8G|Rq58ECp0HT-*OvxE53d{o2YNAeZoD=Qt((fNXc_d{8aWOZQSCB`u9!D}~ zLuwS5UwG4t$Qsry|vG%^4&fQT#;+o$2T0WKz16y$QL5hIZ{La zAocxXVmYjYot-`XV!aPna^`lUT+}Iq4{;I8#Ur27EU=0T2fU0qi}go;st~ym*z2fO zv&#hUrAI*^iQbmCSUIdXj2;sGaii(G99%@{9V9l4*KGT2?w`g1TiWT>BtrsK@mj(> z=B*oH`{Tdi9=pL%KsV8Xdoc0Y?g}QaV4ayea!>S$U0yDL7;{>xI~(D?+hIiUR7X3A zPGGJ=@}zKe9X)p$nkd7yow&qE)wipesfVAxdRVNkNDjn{7S*ozDK`a(PK}JGV?;|+ zU?o?nlg7-?2*V$|$WjQ=M!qdr+Fv|#=7ol?O|_=!Jm}F2JIX{u?`h=!Wzjqy0vUl(K6qL8Y@vtuR9Mwq+}KTBuca(# zwDI=bVt(;RTLMu75PviAr)6KAh36=;-c~mVDDq1?Uk5z+%A*kH0BkJujgOCdD zh}ysRxfiu6x^cB8k3p#_0ZX6tU&SsAze<1M`M6lnOM=@(C!UyZuQy0FX+ZB=Xc2~t z)@}NK2Jyo)Q7W4vXl8*{M;5?-?Yi|M@RZA^5@3;KOJg^0;?Is?{|S#2Mo6yS3Irkw zE@qt5P%p-8$BeLqA7bfV*m?P3c^DUYUlW1ZXV}fGX;Gx2lONlaaLO-6^O1N(EX~0-fXDp055;!>|S77L~ z3ln?+iK z8>vY^EjYD{zL!eEZI#!$O+bYT!B*$^_kSSH-YlCEtMl;P(XMBMll`92z8KmmfOgI# z7JTEn<0E6j7*Q=Szm4y;mOdK_fawoccl8L}9@{V&<<=#OH8nN&v5;tZC(;Jmj?pIo zN`Ja7V7t}sP zy7<>7GoaWgqR7l)(CpwSq!GILnASGtAY_tQBJ;qu_ZrNloeoP!}512 zXD{n;6o_^Z1HIPNOeP8ldy+?cdWHhY&J-y^5cYQR2&y8wwQWCK;v1u`;u;g2QvmAcI6qO)+Wi`DV<2K_LG!S8* zgctE4%Gi-1xmah8)+4d}0uDs3MMM^xv-n*{v&!U7FCsOgk1hqHkOjPe$S`PbK0^Xxv57%riLB{R*teB*M%mOY~R(kK3Jwz{k0`K-Kv{NR8 zI47qVO-&F1m*hPUJjm6IOu$O#q5OKV@zv!Dt1mC{@g#V%{7dvTmcNHQZo3Eo${33+ zHt^wGA!L+nPPPb}vfRix?=z`6NkMA)7J5bYwIg&Uj-1d>%Wdjs0SKQhIu3EMvf`xt zy>y%Qf5O;SSE*GNoi~)zUS-!n4|=dd#BeaG?o%*@zLOj3V5P7EHl;SU6(wiYd8D>F zhPL0LO#4;t_Frk4fhQe>v$YFWd?$-N9Vi-dW)2<7C{Z`Mr#J zifHn=g~dOGe?71za>YGSTRIigFLaXgi(l)t2?)$|i&ueVjB5EFB8%M(Tz2ys{E7`T zT$k{`7WEZ=%`KOnJs+ZOls-4WhYwPZfaklF8^0yv*7wi)R}%UuB>#uIDN|g1#lVz_ z$Vr(|a7wQqIyQj2NHxZ@pLCWe7ZoEbWgrdw%UV}25^;?DwlK~Hdqgv;M)0}hK}h3k z^VnlmrBY;Fj+2}?A~o}{E7b(&6oOU}=+OF4we${9M1z-eAR^0I*_Gv~Tvg9_n(ByV{ z!1aFg+!%Ozf)4KY;i!GOCG@BhzD114&_g)T_lUuaw96xCUhoH=7exf>qGMewqCTOu zrH{;xte$&7n*ZjsP>48a$dc_Nlk>n;wszhvAyDMR3xyW3x~r(8@3{4Rp$}N^x=m#7 z$EcE+xEVT(kx^10Wv^^(>N1<%IRX9eFoxzde z(7n1=ZTtQQ$5noX=_4nh2U7Oo^-@X>WR$2HK%rz+K~o7+*8&R3-zIv17E*qX=BaOo zX#y6!;alfOzxyA#v3c}twwPj-N5KxlJus$69tfQNE2$)(va7`x$`q&#TYN>wZ-nK! zVAYS0aX1F=rGFS|TamhTeIMQ-(kBUft()io@{aD3*+!9ktxDK~#BR2dGjWYzVsodt z?;Nz~_54kym$%rg+hHxc{-Rt)ut?BW@qTG<%F_vb|0WK*5i3GJ%&$%0Q_G?tEXMc{ z%PK48z8d~QBR)Rbz9?EKH~hFn1tt2(zrH}t!JN$dZ}+Oc7E}^drEyOkFVavE!PoHQ z&VA}ZllJ+YQhGnz(Ln0Un-N7?W6$gPa>T6njm^2N6B$|_JHJs>SOG#hWX$vr8rjlI z8&h%pTg#2EkISx_-;9_kbH-G{2xox}8wq_*3HEBvDbvM|p?&(PZM-mQSb~a!c_AO< zjKRodQ;qv-5#W_-0zQT||JpO*a?4EQ+x3@f^ zQ`Y@F_X!n)KRSH!czpR&kO6Ji>Vd67Q0o*SFC>H)0_$N#dZh5<_pC; zHQ4FyioomDgfzX~Y6|Odewc=}XP$65l{i5a+}yn$2>Th+^q*<5A3n-?aKA0jweUGh^$G^Rs3uR<7rtCD z_(1G-Tp(6!u*CG+1M2KMS&mTWN!pLK=_iW?NJ$mTQF&U>qyZLXeP$q(si}?$zt#lOW#X3EZv<_l9JL0(zSrFba#VvcS;CIH!R)VB_W7( zOC#MK@9MqJeZTIvo&Py=<`*;5@C?=+kZ9%Xysn? z>cjh_rcN-)K4#1kP_-b}YYIw{4Z%hbv^(KMb=b;K8i6%(ROIx;OS;S@1Ix-c!{@rM z#H>^28xyhcJOuL0Dtk)Cs_q8}4GL;|jTE~~cJH3tm*3{kZyi~W2-C2#y#aWkE3dvN zzCTkWULwR!Hqxec16afIx@3Qe?t=HE#$Xa6-ya3c`(KjYO=Jn3r)}|>6rF~=gF{Rc zmty)J2^#CC&Tcb9)AzVpyh}Y@ixBt!?OYwnvtkd$v%ZAc4nFc*HpjJ#;+D3`!>-&K zdu^BbWEwE4r7&SBgG0u!qK_+k8_B#jHCz>jX(lH45lZ54TD|V4qo^-e^{+78Q-KJ9 zkz3n=xMZDVOF)FAulHQFjUOPU92KS8TUamuJp&?>Hgh41Sq4Xf)6Ea7eB%E9QjRp+js1Z z-ugS<_N9nQ^Z2|t)?B!_tM~?eUonZQGgMFDijLB8Yb|4n((f(YWEI*T8YEGhZOyXD zkH7y?ftWG55GI&60Sa<8D$k=Sthz|jjBfSS zB}>{UIe}L}yKdB`8)6! zrhjqXyBPQDg6aA#3X}0uq_)%X=WSWsU5W}mk!Z%-oFjm4mSnoqcPK`mrFN#ta+z6_ zD%@&cbDE7?MLRWEXgtiOtiE~MZP~*0grN&EFlW$BChwQdQ_%*2h@3yBve%wddjPrf@v9zeayKt zD~%n5)3`}gG_UwKG|eEfm3gm0&4o5|jNR;8Vd3i;8I$!j-`xCws8A+)KI@|nTh6DB z32tIivacX%y*C>o<8Vmg>_?0@ly0EihtxwEX8Lj4)u`7~gtt=bnvGNzgM+tWld=oCiFR0mL7mgG(zX}4cfzmj6#DvUMt4-tM#ROo`r+EpuM{5 z!KIyWZJCrEYqF6g_%(m_%mBKLsqA-lU z*wlv&ka*;?F>}B{iQ~VGoIC4vmw=vkwNIs)QCE+70a3c1(w@7_}_R$-z`Q&l(0OgyFH{r20R^5H2T z(Sr;uq^_#8^-`({=jexC{(bT1t$4*bSB2!R0W@_o6k2-9+!9Zm9mao`tBfAscod9Lte(Dk7Pd9 zX0$RXe8Rzw)^41L+C>n+NTC1+UGc}!BYYXsZ7jx=Imz*2tG1u|KKb1h5+P53&le*I z)6t|=5~f|MASZ^>Cx*i4_~~h8yJ#;bhNWu1V18^9ylb=<1_fe(hCoL9ogjyR=+|8) zTe$00H=cCFFUts4!?jl3MTopc6I)bmeS`(fu-09;}vdg^TobjiV zOm`TDpo;%l{_wl1FSXH)L5K)i=1h-D+}+;_V@m7?KgVm8(eb0CFL?3fdH{< z0q|}#Z=${?0(TLEtiSb%ICaw-RTi?NWKQ4tP|x#6DVw#@8M23s-uv>}WAR2hDIzX1 zODUiRG}hsBK9``>?^^UZ6|~kX?DiHP^HABYRitI@EaEEhQxSL@>ZN@KbMTt;Qt)%e zbzJ}O4IfzEkgCMb%1)cb;Erw>c3s2 zja~hOKg2eU&WmmoMii#@91Pq&=)Nsu(#kOXa^O7Tbl(+3gnE-#bTlXy#w8>}PKpem z4#wwQfMT;Tg9qbr5i7)J>j+qQ0t9(I0gzZJKm$Sr`?VZZ%eEUc=XI{RcNMdc1Yj+0 z+#gv=TH;q^dbaOk0f8`c=H`z|{m%ahd$)j*@$T3rwDxcDxG~hZL&!h#1b1sDRQ_IM zp)DOrj9~Yl@+6=}trntjYVR1?dD^}k)`>=W2O4|cTQGXvW43vv#1Ca_(}QEc$A|e6 z?}~oJ{5k#m~#0kg1f-<`46;nffPh~*z&z54QA!M#l<@~8a{qqVX(xJ{hm`<@>VKo` z!t)z3ElM9NEsNLNgMGc;0y-LHAn+x+JmqC_3GzV5V4|p6!&Ctv7(XeN$Cq=u!|1_^ zTZYxyYGS!N?o;s+R#LEB-xE-cC$R|?e@U||#SxE150{J>Ka;xuFrKAnJkKuPl{zAP zHIp-1B0LC4ct5O|-P2PxjvgUba3*fT`}$0@#ixQ@+QOmK2rEQnyn01q`?|z;p55Jz z$uSXCLOqa)*QaDigP@K4ftV%=Qg`=u@<9aroK)QtmXD@K91zVOfuewZk)N3`sY}?k zF-1(c;(YBP+L6uVtU99(`+GV6NY4W~PMAnW%H>nFT7 zU}DWz5WE48^vB}qK-d%&*}cMvVch%~l|yt|50mr3D~lruk##(hTF2Ul@^~2A6GmhY zWR%w5b`sk&!my+-d2SDdmo+VxoP4P)4dxWKCvJecaa@ZaCkA!oADD|Ie|jFd{v3AJ@l{^9^WYaz$2JfGm?~u1Iyp<1w6UFLi~Rw$V+b^3brp3rx0O z9|8L9iJQUuw>~J~(o$e9^IBtx>d`N$vnbE?ARr=7sEmJul0v~lI0^?kC|9TKtYdnj zFQ@B5xZ{j7e(sQ z5{MEb_El{vN{aS0bM3esfVkgVutF3MFx(gomzg*0X1g+xmCTi_xZi>5<8KIqRuAehNRYPN4q7-! z<~Y6W){I+#BG$Il9cm6E>qrk&%PR5&@CM6jc>%m;pX7Kust%SfZ#rL}JvNtYi1)HA zzKjSvYIgITT%)@mQQ59q$G7{+C{uT{2B@mhr?&ivR?~*J=}^a8X!RHMyX4O4P04xo z2!KgX3qk6`XRxJjMCW-dW%oHk-9e7`b1k_hrJ*{j%c3g%m2@L2P3F5z z_5NV<5J{Z0C5{PRv^Fmb@P+mp)=%*e$rf8LLc(TpQz3IR;~q`GzAe2M24Jv6#EoZl za{J@7%-2~Y9FKLoni`IkHk09L2;A;qKsvcORGHHKUqu0-#Wg=6(3u~7{pETnjtzkYIMI8rzz9&{5 zDtYU}yLORTKZT9PvKgOgm}ZC`C!msSpzl#t zVcBkMtDY@C+2p~&CFC3bW37(k6pTLT;!I=%#pQ|J{^5ihVK!HqM_ozl%a5{M_y38q zcK*Bz&D>K*{yfY4iu<8_OrC+UNjANvR3*q5uh7u#)4`hvH}MZirNRkl)o=->N!11y z-G_|WZ0u{%TNf5qXFH)0@8$T`7Z5$?$lG zcur%z-{0QGkwg8ZHzA9QZ)^ol{G+~`|ju&DsWzxHlP;(qY0maoQ53ADYBVaq zGV4feOB9BI{4PqjAg2w8Im*7CWz<8Ywcks`gw2c=L2sL;9ZZ;cLG*OBsN8p{w1x@_ z3e3kbL`HyPis(H;c;-pw3z^2*s9*1bsrI?0gr;PVJRb6d%9WS&;gT(gRFkUVzFb^GQ2 zUOm=dhn(4WQhlC#i+|ZUjVZQ01uK2!@r=qwUu|8EdZ);>AdgsU<%~I4vH)|=`MamI z<(M#zaQ?J7&IXn498+$i&@&ZkDJffYTTrL>ZQtWs1XPEE_H|>fllFSIfBeB$8_&9R z8VL#|RGLg$T2M5YB43&3Ob8*2C*mR1fA7;!_x1;gn9ylkI~vb&9XifBU72Rvc>e#9Dh z3sSN1D$@pLh3_mVJrIe3_SXo2e}zHE`G*c`BbfEhW6fmg`PTdIida34j$PDvktmTI zZyJ)m-%?x)Fu-c$f)ltb`9RK7$EW&t-Dz27tn8>49_%i*U#hv&XNmZdl0{jtXg~co zlPvVFW+L5YT0HEY+H#vUYbEdAqms+f4?yUdI2&QkH7k|a2qi{J-%XUw1$#M|pls^N zCY?r$0;Q4+dEDR}Dw{Jfn^qIyfvkkmA9G2=S%O>NYw6#=8UKL~oh2jkx#SNuHC7C! z@{smYJ`CVoZ0IlCa*;F>t4LEG~? zpWUI;&9UayxYLuRP+j-azU*G$7zi}4LB;r_h>tj|th_mcLF)A2lyROcV>jqRam_TI zSwpN|3=dbsOr)_1_UTd-rz$M86Hnu=l3-3FHfY~5e;N>44?R8VK)@|77rfflGB|@3 zU$~5GH81ty-?KL$k=T2_Hx*l~N!+X!6gu_WVH5Jebea%IgjOIX3H`G_Tw5CV9Jlh^ zM~fa&mL2>xF|XeXf<;Zj-~aC$46Y*mvuYtqF5JVVW+7EuVlO9u?Ur2udcBSdFUk~#V`_7?|4is*?zcp2`vb*+oU-w!rcf1+a zf_7ta-HtyOz6{)Wg%lORMn?zZvbXI^38_vb<)KL%4y5q#1_YtQuzTSlwO7u~IWmWQ z`jDkGD=-h=s7;7hsuU@1HU)DQE&5_|mqPiXx-i;S9ZdNc0*g|`4(mg~FNRYa9|0z7 zBdlY^(}ckf#P5B1K|NXN#%hPICUpi0h{q%27WF=qVZKvTGI<^#agD}AZSlqK1v$im z3=X$wX=+~CAhm2ED&4&(E(V>~1WyCrYh~-LzDRA-NIWVD4?gZ98rcyxFzoN=KYrW; z&Mo2HQ?p8;cAC_&8u%VAue<4OewAPQqOe;^CzQj5g?R6B#i1hn3*#?w1ZDb=aS_Wp z^=loh{@4S$+w51{j#oC;xmHi6%{a(fesXV8eMcCA&th<@{PX+M7MV4Z#-CD26Y;Hj zCxm%0ctoZhf~+FGnC3EPFR)S9dHI=T?Xw4=*egtC?09YDsm5fU9Eykn zP^p?~^O9}f%HHKoew2yexe%e!3<4!24?UgguD?_rr)99zxE)R%V|!_LU2DR{Cm|$QhZrN?XfC4Qqk2H zkAXdKRCGob^yU~Tnp;=^MwZWf7+55E-*f9iOlJlRUEYE833|`D&)(Asd9cDWhob zhsp-1r#?k|)1VB5h)B)0c5Zf`3AfhFsn}2#rj~cJ+E2?}v!`W|{!mEXS5V$u%~_0_ z2e$^0daMQZ5#eK(qDpX-&{hqpJ+P4RygDFG}8m=s=es1>0V$7YwvC^OW9SJ&Z1i7dNt)7mdS&bjVC42DAS zZpS73UMB75N$=4V(fcIblh&6@(E^r!KFEU5s6A(|f+Ubma4)~&3wj(a-YJ94$VA>q z+aDY1KR$BbOJ-GW0Zbuy#=HX_Zfe`OIBu@B`7V2hK~J)`1gu9NPWp zCPWDBuvp{hlIUuvsNnL0H~Gkf@Up7X^g+-i19L+`c=Wh!S?nkJ`a>%+qCq*Y_3^pY zaL`jf)B2PNOcl8aW%6lGh*Of8@SEAjqf(<`z&=j3ec5kfS*s6eZFP2yh2HxjI}7Ev z73CErxMdJZp7!tmQk(8?KlQq5PM1B{oOVD|t$0?sRTe4>)AcY4!oe&03SrI~Fa zA|%Jt89weTaT4bJwni71zb)n=A;RVnrM!e}pTHHo?D^w90^WhpB*!&Y==*!CkNr+T zr_$B)y-!cS12dDyIRkJVF`#`vjNSI`)B5!%dodgGA(r*p_$|te+;4C=m;BGZKNDse z=j{i))yHvp!`$(07AJH1v#JAU(H#s{6|XxrWL&k>n4yMxVKhkk3MuCf-fJw{Aj~4u znFKN_p^>SMp%h>vyZVv)JE4ZGKiFH6G2I zv&ER{xM}k>08U#cpRO6~e8HVw5ayIuv86w0N`VZ`roaSpm%o^K#B@lUe~%1i0-N>y zmlLl^P!9P*XLZB#3neUvbcwtQ2}JNTR=z{&W8C8uu-0NrJBKSku z<3v&AWg~k!1}k%f?YpcKWo12|b9JZtoqkTtbcLZjIcPfx4mWoQbSmrBp}}QPCoZXo zx!{g2{6hX*5OP>Dw_>Y;Iy{-Sk^1~j#`M#IAPG)z`bZzH~3}edsSp?16uY9jpOe~fxG6M|t~azvQ`3@`)M-Hm3Avnz z21Ph+gbr6`G5Plrxu8VwMQYQ7Xaag2M+rPxGOeG}U4t}gh63}0$P_HZHRMG%UxG>D z#xlFv1^a%kD6!`D;6>AyR-4P=jTggf1~sDUUZ3wPlAcHH36t)}Ug4wBBX9KqRV&zHdCH z$$5-?9F#mlR<=Fq%HR@`7~1-uUAyu5E5RsRiFo#X$m;3dzW`k!`_f;3dqbm|N zF%HK^7td`V7Il=bkqn0F#4F)LG97%H@j5JkPD(1^Mo4<9{rC=dJ*E|X;9)9&Cd&t}60d@Oa@^~s8) zo!{?1%5kSJObX52gnx2+1@=($hE8Gc9ko!FRYRlbu|tossGjqx47#(kJ$KU|XqEj1 zeP7d+d#o7A;)96E5YCzCm`hhtQEM9mqM~5)sX%Dw?qaam+#5ZP0o7|>EHArb^3bJP zzL@fSzDQ%UsGp5-q!$BgRkVil*-uo^j`VdEBo-TYlI5iZ6`@-@)R|CCl3{d#Yc3b+ ze+a9)ZSy3mW!B!LcHw`@{99Cx8%h6{0v}oN09%%4atO5-FEwm*t9*Kb)p#ds#)gfL zy2HN^7bXN?F4%1*x{;a6+H!0I3?fM0>6b6S^ZJ!TP*csy3Uk)GE$&MYOR{b-juwpS zTuz8gp>LH%mNXPKDe1XREt&_dt0nJ5&m4H+W7FTF(5k9IBx!@^+1y5F`@q~Zzr4i@ z%A(bJ->_p}6m#;f6js{t;g?+TT3|%yxV=)saK@8Wgz6lkif9LdqVt#`c?iV8iZ*i_ zAE72LmkA%(?8m!$$Dv3@>=|7xJ+kyy=hChdYK*tA7uDjyC+-I!!>~%e} z`P}d7miZf2qoUzwVWQ(3rhZe_O946Oo8D!SIq**!Rbw#P(fk^8AtQ@E5ePJ*2H_$2 zyKXsOa~pIYKsq^}M53Ml5wGJTP>1!u1NazDxL0GRTl;5x_=U)0)DvN%E`=PJ2MoxS zFmy-BmGH@>X-lBd&qeFMypa{${gThqQAt0}$s&bfTy#53roCS$pvz+xDYkEiEPn%vjq!*2;~`W}H}1cb!AMK&@5C@8qr8hK$T ztu+wGFLofrzjs1YTKY_;q}`5aBS`KV4XXfO9`s@u(0+3|%;vEgs_Svw_rqg7loUGZ zfN#}F=uXDg3%nQ~*nQ-dQ2Kru)$3~|1O^#FDuz_x z@klu#L54Xw2%sXgr?wu$kob`{RfEjAncT(ACHXrnQ>2vxFVWM|+O7Vz%>FfliQ@b)~b@t zXepAsUzt$8vd?gqa?q{L|qH95G^xfck(QN6Y`!m{93R2oVfJx6jG0s(3;%Z?3IodZWo*563qLJdD1 zp*3@D56lvB?R2Jk&mNQ`oP$Xv@Y;YdlpL0YiM5-F@-9-X@qfSsnHufCwF(83sU}cxxzv@R1Giz zIrekZiEfwStyPIHw#T~OW)#Zry$N9Gw>OK z?k6;CGxOJ}qz?+EO%i-EL3{=L+vH#j@k-6Wpn>F3kMpsqC{1$G(Qo!FCz3Gu&23G( zUn3mF8j`Ipz?ImEkC)6w&0rr9VuOq{811 z%3Zv(%2cv`V`Jr}OE{#YpG*tOeUg&yK+t(iNQ{Y%$6+y2{uA`WNVet4gcN@R=g;?Jx0(j>D_KoXrQx?YnI<0*8-Yg$ z5H7mB{Bb2lZmX$Kx+{FtpuPl`apHF;oSf*yug$N49y3L)n+kUsdE)@mJ4o#su?($@ zijc`HN|27w>k=d66U$fdgkUkMn0SaT+D|aS&fmZ9LYMS^DOoZnWmd`?VtQ?%p{UHM9kCzA~MCJ(xM3M z_%C+_#wW3AER-YNt|;K0@`ieT)gZ7gGKNhG0uBCA zi;T!<>(7jna@tVi;pRrXybMO#3gE^Z0 zN2iN{z}#nx*V|P*hoFimZs#NX=k7fVKsNE#ZiP$f9nHo4K?i_IC{CM>xjIz(A8K7!whZ zr-r>h5)x@Jw3$B`M;_%v=~}qar@6O_3T@6aG%{*2KXI!U2EF9uGreXoH$_#la--B! z_q1-~ZHfwBmoT)jn8dvUY)MrMZgYXcZ9KFOV9_2UGD>I5?)-tnLCbM!*keKinOjQY zsPx#3LuEWBM!4q_k^a|wO#Bbl@f3p*^S>oxC)8BM;A>zTHz}e4n$Pk6tgtvv6JmiL z$YD7TEyfGXs?V#gk1mY5J}Kyx-D6H=+rF|*f7VbmsY^Cf(O;~MqUP892HRgDMzwz% zp~h*Y-}$^3yFmGuNf|B;)r7uH2q2~9R2Q>*T`!thT*SBVOeXajQX=D08Mj2dJ1unD zlv(EC*?ebhMYl1MU|=(FB~`dhgTcbAfF$LCsTn5P{yNGc8ugEP??<}c=f=Y5q}Ym- z52@xa*;?Yu^d~Sm=V;}1?<(N=5Lpk zyjNA;JdVywKW@>a>(f)S4bsS<^o2*!#nzYWe{a*D*_P>mVR?0kljZWG>1WzaEKldT z1dash60|oviKH&}T_6)htweI-B_-uNhI*-vF%2j!@h(XN^L5$Iq5Y!VvCpgWy9G3L z?B*F}4hvsW{r~*I@TU`@V4ik-QtMFP`=!2R+E-{nSSGCB*OUXnIFxyIhnZKi#1_p( z9AK$o7E6=;FsrwpJ7%Y>Ph^z(B%hMJnD|Bx^yu)s(UnTM@X)dh1Gu~tz&klB^s9#+ z7*b$Xb6dut}=CrnUjrlEbpmKwi(b9N4r!mk{>K z?f24`IMVek9!2#>lnu8S`xtq0vMkKC7df_gLl$+F$OrrEx;D=9cur+Tn2KAQ{aJ$D zga+mo%tjH)@oPSv&L-lu03!3luPS1Zmfyv~k=u!{PM-CA54%LcEz{v+7OMXcf((K{ z2p(>+!u1@}-2wNWy>aB`DV1iJNYKW|v@EvT8V<1kA889L`4_7635UQR=}nO2 z+&*|s)iCD4??Wp4fEO1jYA-I;EgXro5{6L3Uwb@}?Q!BWUD}avAAv;zv6w2A&5{#F zsoQ1hdjYi9#wmR6kf;cdbP==WAn6V=UKf#(_sN})?lTO|uhH^GWa0^NFOsr0 zBx@N@hT|^O%%D!9)Uf1!N(;QD??^{Dxn$*?EjQfXDRs%s<@Lki!v?^_CJecE)<&cA zibuNlv0s_~V>40}@526jVW8x)A?Hh%vHP4NJUjc6TPpYtXZp?h$6OgLO+)s(5yU#-`1rx6E|>qQGss5bTK@yYvEKdYz1YnE+nJp9UQ6%tqfn`zqLc@5F{Va1 z^x4MP!>~3AHqOm;ffb;p-%cng-Uouk?al`Ud?PeIh4;&$l#sCdV^gjhodpphUzqM} zz~;@f)IeWaKfjun&1lN{lMsy4qWmW|knqsbC^I~On0}K9HgX0vnJ`Gg)y6mrE`f;S zc{F(+JkeHcW#eOMtpnt>&nwm}3n zdf7pp>J=wJz!oM(S);Tc0I3uRlkCT>aAE!joLlhY*3qQ29-u%3i<1vrI+j4@zx7yp z9&xihk6!>_OyQEvqxs0g0Yg>Z(-Sb(mxiMD9RKS#uVCkH_uBnUvp;TU zw(gR^&H*3Qzgtxh;Dh@gmU1^Q|7MWCcMzk(!oqX4xUt&eT{qTLoDLcd*OZOq$mi#^ zCNjRY{P(8wg*EV0rcn}+HyUHJ{_7sI-s>r7;=-FPQRSq3Tm%GRP!hi+{3c~5`5uAh zl8#)nka>;}BhRun?RI!xTlgsgUaa6p|p%zPxid*TjnsSl@tBA{+1~!xdTC$Pgdo2(Bj#+F7 zY(OPsDgQUDR2cj^FT(581M0lrWtC_bMs$rOs?(>Kzta1-stgoo%LsZ7@j&AhM+f#} z>Rkc1>K{)!iFX&sX@=n^8EvKhO7o0sm>ok(p^hb%5<+d#*U7R8*BOhFD4XW3xIr89 zKL52dY`X|UMHuwilnSayLb-L;2-E(Dc&!&B04kKnLF63EYmt&=laT?euB=Q^u*_as<=whrlS!_UF!xk4Yp{zF6tdRwv zfqrK36O!MG%tZP;I`Fpurp&v~!`=_64R-w|%UhHjW?T=>WQdSxvx?s6W}U2n^@ z<&Ady7b=GD|Hngw=PZ5-9+Yms=|hWWDQ0}>aTO$7+35hFo>6qBqp6IDAk=&K^ zWyaApI1J$yPnNO3t0Hf-_xnETH~K7Xy^sS;{JOlDFa?rUJfJEo6dYA`Q`{X{9EZuj z^?MVw>96uh1j883rjlMvTw&rK-zVNGp=_<41BoY!W5)V)zmJx(eCS3Xr40bELI^5V zY*kSUlqh2oltRb9XD#zdLwzWp0Pr1aYx6rMxNs}QSa*LQUFT|^IGN=Z8hBe9l~Yrw z%w>y?eO;*?$iy-LjG%IR*G4st%^R%M*&6Qy%YT6>-~l^e@$)jTZadS5WK(VV!2`%6 zuE3lRQ89-g14z|-h~R9HY95-iFgwe;w@&<*8e3X~1m;-3|Ft3RrqTQlRugydH@^g- zQJBmrcLhgZgxmSIP%;Em`ht+g0yqme)5!r!t!eAfC@7}1JRGEK?d>*I8QxtE;Z<|mHbvm0MTn(f z)3WGN!qB0nSW9ZUK$JA{jN@8B4J|DS{ekHpRJ;}C4;7o{lyiPXMa(8=RYA0gm;3=4 zldODgBf5oC(=iyd_w44^p)q8hy}cFO5w4PGJU{khwic+~0tU zAR5j?-(xH#+Cwzp^2WFSSU$||%`4k0Wdrosl3tab+|iia+mg|j#;J!an;1oLGt z58~&LQ5i5&pdTE%SIYoUGhGvn?W?|Z?Yr$I;|{)xjZCV4!J$b>@=9E>Gzn?t)Oa29kGez4q zpAe#SVjxs_V4&JSOCOqqqjTFXMKFZNd@<>LLyKqZ~Yr)1|H~ zp}iHxB=0IbQs~gfw}_~OSE05K)q5ueVoc<{?1oWMlxm!rBT$13awsAd}Bb3)=mLGcv$^(`<^YC-V+ zheI})0+TQCU`x#FU77H3pGa5T1;24N7dQZghQ@lZ4cZx0Dq-x2{&rltECZl+K1iST zERr5sMr96kDndm=kSY(lj|;lb2=Muq{Hhw-wbHp2x7y6g-bUukK51&{{FmSupHlGV zD*8jU4joA0K)G=>y?QC+f!gHITxYSXF&q_| z3l?G|Q}apPys&!XlXU-JD`4(Zt)wQs8Bn9+p7X9Ahm(DF;)(Q_w?4rLh&)zgIZdN{ z(jUTO%Z>k;*OS7?K(^o=3Zw=Q5}lu$&t#_kOd57!U+~cEq31Sx`>XL#Q&@_B{u9wY zHsBfo?<+2D@ zxeugU8;(7Hr$gP9>=YHq72eC?xa(Ez69U0}4({EzCzaJ0qq`#QRsleFCi2wOPULRC* ze_G6Bt^rSD{BbrQSf}_!@2U@KvH6TdHhWs)-9<2F?K*4pmGHV_Vz0kI(glAB*;`Rm zWP$+^q#6)jtWFXj*Ld>*=B1^3<@Ib7NyL%m00!@II6maJ6!l2PBpP8HVgQ=w^HOuy zEL;2Y$;+Qu8&^v{4naneIW{RDVO&MH*#?knuFmK3OtV0QpIV9db%PX~?x{hiK}F>K z`E4>cuD5u&{Rb`f9y?(yIMnP7R_URD%2 zUinDdw_HEZ{1rN?abBOr5`PuNR`~Jg8tI)af7SFKTZ*WkQ3vzqeEu#TiN?RX$V_&T z>7nU>Hr5CCFX)nC4AGx7^y^x>ThbCuFP_&Q8Oqed2=mqc(>88WsA8b1*7!w@wJqKF zI}OPsydq<=PsBzXYQU#FNWKa?#z(X2#dgP?Lk>Qz&G)Ben(gTmF-7huP{{>qYZbo# zQJ>U)TXDaWzwFx?Wv{vq1jwe~S%U+Wi^A07e${|1)_(~y3ygSwJ!;as>#syFN+?Ls zD+=CL>XWVr%e0D65(fMhtp65NYHXgLzb|X%pvB^k`nB9+y>X948%#C;A`?$WYyZqo z#AoQ*?fqU3zP8rx;M%LBr9tB;=i6oqt`djyZ@|bXDhxnbfeC;IqR;X(h&y#Z9&561 zyeub_HM;VWl>n(*tngplvOwdzQZYw~-DKzDd;iIz)y)0axtq^k1ML6a@!?NMEU8}i z^>sDNgn$gDCdYwOyf^sPJASndCFxL!3v4Ui+#Wi*sjBgn5tlF{^2a$xgA#laRd&eE`Q>$62*|ob}AQ9i=jK*T%%I(5F`58fT-&JzBH>S@7etbBvCUAerU* zI^QZ)?UMCV7>Cz;DA&h;)--9jvb)ns@x#Ecb@O5jW#jfq_~&6O!Ut(Fl1qL&EocdS z;aTGv>hvrzoj1pzAkqRW8R5AMYjI5b+ctOEY{fmp_*B1dsqxw`=sO~y-TmX{$@^rH zsJYI4Y0}4{)gIjG3>8<3**ACaW-c8#S;nIiD6sFd6)oWK&2fVnlU1T*2}9x_B&Bt2 zR@5*=#T;}Mku+G*94Hc zz7vL{>hO1QVfR1p>vQ}uNcc+UG4|T4psc|HJDUfK;JF%}iIynKTa1qGxpTmwHUOL} z{>bGSENSYTU?8bt9TS@JyMdPOSJLA`1m8Aik#TaVHjxRMx&l+Fny#J&+uFqQi|A3; z!+|#-1QyYbZ{Z8mI{~d#+#K1vQ7!P}@Oyn7n(+x5k?sa5Kftm(GKCLN?=AFd$JuKV z{sPR07apyAgqlkXhvRlHd!c*k8a1?+TT}~vBLcX&gAS|U-Z^0Uhi85bcg4v0tINYz zP;w~~*42G&AJkzMvKhJ15u0Paq>Gs2p7sh*Rxyb~8b1%SluKuf@I;kThx`gOmKgXt zb$VQfR>}-@uOdrHe<%D-!TcRB1pg{^z2Wgd^d8{#erPAv#(kXp`F|4mh6LLQl%RvQ zR!><;4SKgQ>6?bn!oapg(8SJ!^7EDr9Yp%8E^E*KQr7T|VD0#U30p%Q{NCN{Y`2rL zW!L)ul<`jrMB%tbUhffA{!$rE?)IH}2G2TEZ}e$<%<+#ULpnGJc(_vGVpm5>IJ0^4 z&kR0S`Q@zYo=%riJ53`fRyQfp>}C}R<_a8%fo4O0Jxt;!u^jbi0Eubm{3vJ~NS$A+ zZ(h%lJYX%+oMmv4C<$Z22G&SrmhYkQMO?B9#B@{3JE1hU&?~iYENYY#Q5OE7sQ7==~P`|VM_T`omsu~?O^SR&VblDj9RU`+C z3-l(Ldm*>SMLX+2j7bm=WAYRwIxwXU^A>svhgrry03G06al(Y8Rq+7CZukLmeBpHT z6$Gen@~rWu;@GGqKcrb+BuU8ubAo#f3Z+K0VzChY5jYC#GW!B|27B3>J3Q~za~|}^ zyE6n1{)cA!Q3!YW`G)@rlkHcVgf5@DCh>L$tobWs8$|e`5{9{Qals@ml-Vu;XD@=j~U?U814ZO8bu5;PPty^tSz3<&3m%4{Y?8 zu(fjlqPDznpqFcN1((h#^FS@e(fb;u z(;3o4PARjgFXC>vQfq5SZ>v~YSAql`$Tq%=P~SS98OEW-6<;Po4J(BwAiMW_6ybdc z&_r@1xOxqvuCDZ1$T7fiGcw4}0IFV(l{dMS8~>fqsO~?UA+2|U7L&fX*)n|H-f!Q` zI{z)wcN7=lJoyHRTvC{N$aUsdNFZBEtIC2Ekvf>K-~W=3i97O@N*Z}1`e05vgH<_3)vB}eo22GniEMTlXeUpAvT7}+gy)2&!( zBp1-46eSOjO`orQPC|0UpL46}`d#4zK9XzEYuJ0rr{6gggGplPoOtoBaR11^5mOwz zfWa!g0On(Xz0B_AyF2b`l`re``FXGsQVQ%=X^E%^SfDIT_8%f&BY52q>rAx*Je;gp zeRg9$^^Gfk5^BBY^=|gxJYMK%g)WHy8 z(<_$6y+KUa_KMCccZyaFHvz7!=yI>&6T8i6xXXt@SIVdx=*{BqUgzK#CTtkGGyt)8a^dyw->c_~6yhy3e)IP~C2)52=8vSUL&DFyb_zpPEL#$Wu z+e-6O$&dmLM^O)6U~%LW@#xY=vt!JjBt1*Q0rKf|=X!2-v^TT|Msg4hCaI4j01i4e zVBm%tJy9u%sN~io4IF+@qhuhplZ&SamPFc)lXww+ghpb0t{-A#p(o6*ga#gshnuME zZgVFuZV7u>j+0;QyDK&{kO9=Ngg1D)C@7Rr<#dwrv}Y)mV3*bMF0~ z`w!&VnQN`N)*R#gz2h=oPN81UlPV>PD5ckd2t5IME{02hV-oP;C;L$>-XjyP&MhNy z+NlH|tO0=Se=CA`P4>(8yPM?N&jk8EKcD9DoRcAfAkwe@K`PZAn}b4nu8{@-bC#sM z{&ys8*Xr67Fgn+8VJh%iMMqU-Ds(lOP1}6~Im~8gryaHn?gAaA>fx<>0dmwU5$}8y zzGqu}ZnndFdxC&6;uQA1(k@DYUv<5+?t-joQt^u~xW`*rSrii!$jQl>JDhB&w4Txm zSKp3vx)rMQx??S}SlY4?I8iMiN>{|?L843Tq>`St?8K!2O95hvL|BRoWGn(oR_0=W z5_N+iax1^JsjxP6Z*6W6+G><2PQFA3cc}4MI0nMWU>5Od^)k4+N{DDCBW8(72OM6t zlb0uWmX)OVZ26H2g}Y8u%jVFQfWo}QvTnyQGz><^CNF(ywAU|Yy+53?(aY;@bpC3t zO3u6lY7cyi9}p>*uOm`TCU(^m!6h*a{~jCgVGbfyD1qlnczKvuAPdt`bw%~d){C5h z7o>E~&`_pL=;E(5+31ALXz4+BwhLd1<{p{v1w*E$ES0kq2CftaNg$UrT@Y2`=H>K# zv%Oz}+vE>G--hnb$ByAW{(P5|&W}gf{N?b)XeFF_AR{t2Ltm~;EiI}?g{5F*!OZnX z5pOD~AeL_2fV?`CH42zicw_WX6KQ)65pnKVb1K{(%pQ$p?+`;cXOc@t(s+)a@WYnIn zNz-dq^e_pXq%;QdV-NqF^aQsD{B>Ig=Y#_lmZ&C%2&nm42Cf~ax;{b&RF14r#T4JH7L+JA#QR(C zKKS^IKwT&aeH<@riK!~2isnQkZ1SSawB9P9>J%N`JrMmvZ`N2s;<)6FUY;)%fp_jF*?8oIVD2k)N=#kSrIa8eeNR{q7h2uHaPh$A7N` z>DLUry#a*oRflT~xZX8XQ|}gJr6iyn0$ar~zCp1Y!I7*t0%?ODqg5*KC=U78kZ|+2 zsMkK^5=7$QaBx{?NQ^ZqKHE{>H#;9U?zBJ?wcl=Ll~ru-dyO-ZmwmnIb8_U5KK8n# z)|+r_wkB4vgSc{rFAQZqLF3qHDw&oQ&kk<1Zwug0Fq!SY-Z&OEUf$O!^LWrUsT-x!!<(LM5s~TR$^^e z8eHzKR7>vKteE+Q3kkv;YQ-o}s1dCElG;CA<5=}a;QFB#k(CoRRNE6LCW>p5-rJp? zmvTBl1~2;kA56c8O-VorV2B<=N5|Sr1T5d2yzp%Z3%mvu^N`ZuCQ0S5`3t$w3uHoo zt)rd9)`zEB4ByJ|W|9?h)D%-t;TE^*erME5Vkqn+8EsMaQ$yvs$|^VVb?ftk?=6C) zkTX&qR2S_X;AsYZHo7dg(iZD9qb4W&r=?k)--9T=@X-WCK|&ivc2k9)d1wPOn3QXx zeN^SU_d|vPj=Q(bU}&VgU>0TNXfCP6q*MB3wD6G;HDu*|BE(LEl7sE5`G@0Nlov)Y zE{rpVLZQo<)RLY0`M!*4#6bE9e8D8aJuUK zEl@*Pyt2GpqQpCUP$p&fMe##1)7KNK2YHh3tsp~xdXsHx8fnSEb2j69WDZ5~@LDVD13$fQfe7gjoNbO+2_<0l9!qG`QS_APY4Y90esM2uvwV7^bjrDz{SEhfJM(i7}Q|>1KoQut?xbOSS4!J%hhYQm%PpWuXL;`)|RLYUfJPVLSd0&P7F6za$aW3S= zrL?j01y}TBEZVlQn#~C2+b8zE=^QzS80nC*jW95R={&73yfj$Fnv#n0ybI#f>xzNz zgL4&#c^D(|zlu^EpkNu|=;ybTP1iiuEiz*;b;|xi(q1BDOWYFeyX#rWqG-IE(%se{ z3+)g=2q_xpDw;R6dKOp~U1HLm^bK(oR)1l}?%2}(qNS9|W z<;&SZ6hM@)^w)52!L5D2NR3R#L*dvO{}XU$GC4$yx&^b>my2FnN>59gJ~!qILE2qL z*Z2-x{Dp|a$w78zlOSCv*K~~R} zT-DTwVF;)6zD!fueMoLbu(Z;ow4F8;WrL|9$W&ezYFK;58l5mSNEVW(5h?9Ut}~*- zlpbXStA&&h@>yZjhc#U~daY)|sCAnI+SxV9{EWsLipkCfZ4M0wxn)wOmuC|=O=g0t zF1(u+36c7DUWypOa>d9cx!~gP2ntSd()ZJzF^X5A+c;|$kejrHdStoKB>sQMa3U{TsM{RC)JPdKK>fl zq3wd#T};oD4)taKH1yDQ8?U4Ba5girWBDLjb~Acxt?6>S*drX#YCgiHur_?!PSDMz zdt=p~yJNq8%bw%Nx+gw`o&7>=UBD&}Rw}%xp`_MmaTrNf%Z89pb1O5@i%2MXorQTE z$fS-D)CQ7lM2L@OiZ?L4X7j@;t|1>}BL7LAbyhdv%v(B+cM)|{9KAKFD`Z&fh-iV_ zUo55BZH(j^{I(eR%EJ+ryG31EWgW5p-DlG3yhm||!;^Xe!&G(TP&@&<5MM=>AKLU#kimSk8`STplH%z@+z2eMUkd1997GoGr_|A-X zLzqjEN!*3 z0bH)yblQL)+v9<}!O^&%HOw??;m=-?FB@z4<%Hg;yTx@z71{CiXn}{QZM6aUmD$UR zPVl6NcHpJNcv)n^BKm&bCm*wEDCgG_=Ny(eZpx~KjpV#z3i}$9j@05;ALh0d+;N(@ zWKP<;lF4!=I5LJ<3A#O(@e4Ly2#!|5LEARgi~B{Y`!{8uA6AjU;i`<)<^JW+SQO6O z8)S4)`rTLkMDKU!AZzmfqut0rO@!_HD?HB1Pqy?w6DJO}`LGXW;D9ZX;}nVouFaU) z*R3Id*@ASgQm{6i&C{&L`SW+(bEEws)Sg+=2@VM!dX}xWmX3$}n$AM{2m`YG`N~Eg zj2~Yo{@}>#0!IxC1wP;kUZs2 z46o~ann8ipLglpoZ=W&UB~Zj;Y(Ibs8@I;b*0(Jh7`WVOuIBs|0?O7=%V0+?x%Hj+ z14`*4C5>W6P?d+ZpN&_U{GKjBumUKQzFd%N;ZI~q?XcxRfr%9gibff4vgJI;LC#Lo z%)(evptnQMs%+6vsl`qp?l#p6Aj3h7vYZWRQWHFrPkmiJA|5QK{VHKenfqs57E{Cd z@Zvq#x(gDp8vP3t;}Qm71A$Q)cE%lVhXGeUHCirSu1##47ul9=Z{o^=>)4+4s6y$H zP6$6ag)eaLv)Np33N*xpr>4h0Zoyo)&Ug(d>xLpYW$H2)W(oN95gxyPFJ81SzSZHQ zZ@?^0f44!8r#^?2S(`d)(xDr|nkzpRIh%WM3a_aoK^xl7XRcUs&S~^KD=buEBVTHT zSWj^{c2bpT6219s%3$scYK*oMIg#s&luFjQI8A{~fenVGoxiBBruQ@darJZjWm*6A z1R17wjFNR2dX?p0D2wbTOS46qNpcYdxWAXidAEHkGZ72gCf91$U-h8yR#%DkMirrXpS&Z3Y|^5$Plf!sO2XbN*xnGweA z%vH)y&yoi1T|*WP{?@da+1sdt+IF%0&%6&K`!9jW*=;U#V9PK^Sh_?Xql(~6UaJe~ z%N@6~YYf?DrEh{HY$oy^v}m@bGhc<T@a1Jx9ZmZ zp6S}>p+&x%XFgj9%2XW=y>`EmNsuMp<>~=VyebqEqbduGZVpIsb^Y}73U^H9`o%bI z&V%1-5K0U#{N)~#BAj*?&04H}C);G*CBh)vuJ6HUA9ul(aI)-Jnv5=uT4m-G-!b|u zuYu>#qEGl+7=Mb(C*%j1G|6ykNlD%-r{J!De-%0wfU=!kx6ZZ0q~mr>ffy% z>$YLH=-kw0sS53OusdWhU-uJF4h05J6&|Yp?BOE&NqHyckWBY448$TTD4E^};!BcG z(&?~Ya;k*E)RYdfozbnfL9^aSu*2h7ccY8ripbAWMUA zDv=n}IjC@qzFDenm=kS-VhSkY3%@ie|5RM(U^QP{(>F1*E}T7ZntHaTr9k6$OltCB ztgGUH9G@jcZ-gyY;&Rc5OfN5ZzAtQgXoaB&O7K@2;S*T$1dg(e_78Eu_VWD6dieMr z$d=P#tB@8_OwISuGiF}vwq4|fiDeHO90A#6Z+}ETfuCS%bor-vvpK4rW%9S%zevli z0~xx2hpX#t4-B4PLfaj&}6+%7lI)6`Uy+e6~h^J5XHS}M#NG&jNL+9K+{h2OW8 zR7mrKKT92ld04MyfI}$5(HvhEmJ(?5WwEhUox#*dACRED5Ians#faJ<{pD7CP2q|` zm*GRqAt19ioJvI}D#LfNqRFD<4*oR#iV4=2Rn_Aw3PFi67Y!Q}i9)W11W0RL5ro=$ z3ZHmMeKKw=waA!@jBnpd(^Srk1)E8^ZvU7!s_m$=aLFZwC9VhNwSj}Deiwjcv*9d@ zd$X24;3Ort<{%vkla%^-ukU%Z7G5>_MaR2tRW|>KZ!Q(Xiho-l7(@-Ha=kN%7wAws z>5|Zg&XADv*`EXaSriYGFLv?Bw0ZYa=o#8mV86c@xHT`2_(Nc9R-LfqU82kAUtT&6 zDh^~t)j2v&;p*I-vu45W3P@E5D2fP!Rgc83R;fAgi`9SWOC_(GD85*5^i|MTXMnK< zJW=I7VP|{12l{y@!)#8hr&2Pr zp`O59bwA1VV+ss-J(?SbIK{2vHe%)XDPt;BKndXvkQJtchSfA0KIe4L=5(c|p(&A+LtehHLRe!YxLOk#~X z$NP?4AC{~(MYHWE&J)PuT&2gR z#5CR5-UUEF?+XN~kr1(qx}uI`gsIZ^ZqaXA)r9jL%iq4?I08~bz6Ub|{n8Q&Bg+Ys zhVFwz7)4K*YtBh9`qX^rxO(@45C;AX1(8$zJo{4u%{@5gfDBFqliiS2t>1QScks0y zEMrrv#J-udgtqc^$?zhyaO!n{Ofp3A!oICK$#e}YV%&o4MkN5P!lyF1Iv-kvCMoWq zOr+X7&x&%)s>kUPFvIep=AK=K)Q)0J4c?33EP39Obdc-&`kt2a-ePy3Nr}v<#X8=p z-UWSB361AkI=Bw<_+B|eR%|KBZBa!@zS)xjo)l|asWCe`r(H;?nCU+jo?#n_uz)E#{lmI8xqVai1oF0(T3W^5A2-JN|rPtPB3V!!Ep8NPgu|pJ{TbINQAdM6r%6kCj9xi z_h1l9sQn!CN@cdBw2=-T+cwHh()<>+pf~l$8qnVaQ+BvL+64g8hYgCK-jTL;(l(HC z8Xn1sLP$ecr7#Q(9rapT1uIp`fQqAU;oXtx0;*B|x>ewAq+`Jt>$s{j4||y? zN(PaVnT{0~ruUHPkRu=q9<$EB+Kc*E`emBr07DgOT2?a^s70yAzptOrfR#25 zydSIxpd0#+;lx5;d5w??f$*jKKMj_<9W-6w@88y(gv}q#md1PL7h^jBHOkaqTV6G% zy#7R~U#Px2PYjCuTQqLwfpNCW*6O$1shCA$>cG~M%9LP zrP0UM7%x~>8*;u!l9~wm3{?o{A;t`p=B#qWm$+Dt?+`s{m7LJxy7W>0B&zuL3sT&< zwCSbbxu*3<0|3fc#`xcXz^zLmM)^eyXD~Cf@~14JN5@T$VWH#KQg)`@dUwV; z9=*afa3!b#fFTC$fx0jF${-`S3r8@q1mv)RBF}G8m(0j8rRx6c^hLRuc9t>G5JAT^3gzR=@xF zyKDjHfu`dy*bA2?`z=tfEx%hKn$O-vV(CR%^;`3-oo!~kT9j=y9wPUK=95pn5Jw)2 zNtCA;XK-ErE<>C5&RZwZ+q_@QBZG%`rp*7KP_i`YiOL+WO~2y$Cu))93gKLpf!+uM zcXs}w>{Y>15W0EUBB2h;mDPZ|?u+$`GC4-?S@GuA z&+t<=^>csS_#*BR<@{8kYLba?_G!afnT3K9?Nm7e;dGwcSZq>h8MNOE1BI!9gGB=Y!iGP-Wmni9**^YnQH!_UPE8i+f_idC7Y_ zA!l;%0O{wrIqMyouxCB38wrMZB?al(J`K!(yd{mIkPMroHZF!nypUuuGW&^l!&JY9 zTVz#uo`&M0yN@mtCS`)CChnKA27t#w zfdyc=eS2(i@t4Xy-^-}6z8*fnF+0SRAC0iVM74ijSYVJyoR(OIH=6mHg;c|`^ek+W z{^qU=XwdVFN37fTh$#QDVezm%t9A?c9iSZ1?o-+Q-D46pIpw_iV|iK75nRTSG~ zAT9~2TJFz1eC2Gq%hF?t#%C~!s zsbO>Ybw)x)e9G`uk3pVYlnpc;uln2zFZ=_FL8@%;`eA(y(_ydGyL^?3UQx0NQ|S>J z63semz1NW-d?5wIEs@)`$`|*Zc7FS>I6M(2;xKvg!lnDNKa}W4YAlCA5`fD!$gtqx z3l=u&!j2Qg{*~Y|@9SAg1h{;!Om4n$6pO#FTdn=`k5{7C6$*NWStGZ30X}O0HmHxW zr0Y4VGZycs4+0cd!077mT8pP*OrHvH~ZQlps5 z@*!-C%wOUp@{nO=ey2*=yv#%24gw3y6JdWZ%DXaw`b|tmu1yeb^FHpn^G8RrHk;)w zm%Lh^FOf{<{r2&)%QU7vh8p0@lwl7zFh=s}F%jAeWOctTOP-w?B%elz)L|VpdV56t z>q(sUaaHC7vCKp2ori6`7h|)Gsh0RP zTfL3cZ|C7uv-8EN!d_ZY8UuE7Rh)`c(-sD%1WqbM&8tj!YZlvn7q-h{Ru7iloGI^n z&QXh(ui@`d=l`a2Z`mjc&RB$=!7MP=A!Jwmw&Ld=wNxNRhA(rWDVKu~u#K%})4<|V zkS^H~r%{#9$hk#PwGDA4j<4EtLX4rienHS2tBy$Cah#8lk~Q}LWiWI2VX*sh7P`ui z!h;Pe74Yc(-wmD+8f5VY_eU76Y6mcDg0U-wk${U0loVL<_5=VKLs0W*CP*Z)DDs$% zY$4UeBkU}5Z-agPt|A|=>fDUYB#Cy7zf{+ObRqiJ;Jqc)Cw94$-BUJOhE)osD-keX ziT$(GMfBoC{j5rPKIGsb8tZYgXS6fm^5lHc3$>wO7wse(GRZ}zfaw{4_x$y=h4Po` zwOyvGwcD!C;O7hV>bCSze2KrFkP5uv*Skn`pr|##@HYQ0M-_cAHlzLtj1mi%?=*EE+ay*lC&F{#J5;7yP{|#O3`vOlh%^jqC$M?dySP}YkVugV zJj(a4-7Sy6oKLcj^B)So#)BhREB-MF^#?oZ8UMSdghm+eO9lo9=XG2L_>s)|XO}z<{@!sU1`0Y|kF>Vwq8%T} zf^;Kzb=Fx$lii9ojOfZC5kE-hu_vRs50zr~hK7W|99NRyw>n~0zZa)L(fA;LbvyNg z39=|UcP$yv$iayF?0Su@c|ix5rif4i)p}N%q8&jAg=dcxKD+fMSF+-rl)z^p zlpB-qR&;9-@+1unwXj^-0joKgq$lbLgu;AP);J|=(MQLy*%kM9Zo8tYiy~fSoe2nB zJ1(ntL_7zc8Dp>j&x%oJmCg*Y$K{3^yNhGV6#U0>P6D&=_6{FL(SibQE;Hc8Qq9L@ zR77cd7YYlNjgySxc5{QcjTaIFO7(0=+-9|F*I8QeHIEv`aWWpFUFrD_udd%aa!F2h zjY(P{$cQtsEBXiS_kTgf*T8@5&Nahcf?fOVXE}MFa1+zUhZt2MkYOnJn7suOdC=7H zT35vE%D8=6aMDIBkLzV#uqe=qaV$*W*COX@E`P|1p|Db{r!_+b99Y0Bp z!K#U;_Sju35j78q$XswbAVNHPQE=0OMexgJUH;xQOl8MdeI7_1$L{t z78jO(XuNDAXlCqm0ViLwQXFxN zc2fWrpa!9=gW$d0Ps}Qw+txi7BA9P`GvG#Nq(BVoD$*0~qs?Gl#Ebo$Cyk!00?d5* z5l+3TwuxF^!$8KWs&|;HPxf|&6rK+eeY&q^{wTZds3gEBKqB+0moepi>3`r2rj~h?B#GLQZpxWFRm5b=*7Qvu|`V&l!aWdi-%+V`)bU-rdY2JC#UD| znWd2@UOXd5lXIcdK~TWxS~4oi&(=K6hsTN_U%V~d?v7cN^nsr#9Qms3q%M<;yWanL z=1@Q?(RMuKjB8bsbf2~ zMJz(b>LI6nRtRCzGt4o>L4RWiefW~NX5cYoL;XmCJ%Byz^&Ivhq)^L)#+kT4tq{!#qgUh>!ZRk{`x9OHE~V5Dwv0W?iN6-X zt_#}AgFP<8V};uOs`okCYbt*%JD}0Tu;Zr47ADK60NU9!_H(kg@f?|5CzKvhe0B$G zal+v*3-#y?1Yb?O#ti4IE`DFSmhCcEs3J6|=MM}td0G$ug4T~dEA-5p%Xv&SVL%aD zU)MUk$eS^3Q25~_zsCJ1=piH^>sNaI>57T|VJCE`7GJlEX=5D(=ul5w8nWR9i;H@f zc!4D72^yUx$X87RQt^TM+IP}G&}h|vvnYrdZU*z~X3X84+H7t&cL);(xcz69VH6fP z-Z0_Q#LB6qEGkN*`D^z(*>Y$K71W8s@ajFNEFyo=9&LOU{)qvD6ds`;cJFxuM8JQr z8pHQNtQ>`4j}n^6R?<05<*z}2neSL4^dYkwnZ*jBM{?Lq2aAZ|wK;Kc?Jq#s(sEd= zU<%P?#KX4~_&LC+IWA7c6I%AhgV%v7qQ;#`|`+L$bpgE zulc5#1lZvFF@--j^Y9|X_$ZrUdr2}f3C5@{7U$f()Y&MwkGK?{uK=y>#3sL1tx_t^ z=8I+P7R0j|Wh;C<$tCLu!+lBbN8WEszSDEv9nvWwRL$YMgfpm+fq)HOmRT08t(upc zk;6FyQ5g)4LQySLwa1miUXNGM+?K0)9Y$d3Kev6Hq)zw0y8G7dr}lFoz-G8hucN^T z?IRubPW8_^26KeA zI3`Gls8U!wGI-}If)BZJ}2552U?9 zvHHzF>2T$G+J7ne?H;8OtVI~SqqVOBD&&W2h#pY%{P)|t{wA>iX~>3os_lss_X7Gw zFsrGwcXwd)prz^L0CuQ?>)qlW*Q3w^rTNwRHhOiHo+Ux?41Bo*K_<}H%d{M;bT|-`23XDbU)rw*#g@ECAO!r36&YM{r z|47aGgqWDb;dl1&w{94x(X`F))om+PYhJqi4LlOVXsUG`i$s1Ic`r z(LlaoPCMr^u%lxrKT_B682^Z?AY2=h{VI z2FsP*kw>bE;Mo#f9ndloLe|l^XZI}?{7HuP`;f6e`L>qqK5a}fr(rJJ^Ta4OEJ#r7 zRZ2qHx-WkmGIt?0t-J4H3u7W8Y$J4~XTT7x+S!9KrQ*XJ4vjVK^Q(q^@#YjrFwL57 ztj%||fgM1&=FZ4o%b}GRLLKY3tTT)?UH3|$d3PmJGX31#-WSa*28>eIyV zVSfGT8T$`*w`oOX;4-RFwD%ajOVntrsTKrn$W`lHf!w&HaV%X1HDH0usSRou8{)eW zOlU0=3cvQ|P}>;6SCq$i`?Co~Y{q3_6JNZNQOXE`D7hE4~ zmttjQMe9Zi6#b!a{%;=C7$tj4Wt|(R--?`DDi4|@a0dq*$0eIrtneq8Ib9b`Eiea* zli3*4R$+#hW@P5S5tx2fc)1RBvwkq|LzendTIB3A>PG|5eTS7c)iIZ;)r!~5#9ZZL z7WM8oNljbnsm{l44bHcJe{NshE3u|wBDqmpKTZ8GI$9~%Tt1o>KC+VUWyUO^AMaV8LfP{21`z{PXnNj)E?>HN8vWkyx%L?B zcS8?)%*=7v*y=xq@Rn!W_K;HlVrZNTS245UK<4`H=n$DeH=@EK_K0hW^OHOVFZk{> z7tB5@3o+3`n2d?aK)Dik*BxaG*th?Vnaof7*y@|z*6Ll7jXwIi#BfsCCOFh@fcQ(@=v*8jv z8Wxhdr*CH(U^L z`p)WdujQ8&WtEfD5s|Z9i&%s-s+1!!9z}>rh>RiarrHJL={x1)w3FLsMBnS$uMtE<;zfyLSU?W6hZ+BPz~8 zJxGG=k_;Tf8e9C!xJ8x|=-K=!*#N7mKxSxLL8>1AG(P|8Ke;c9UYDzPQ=nER6c3Fn zC2&P_mZTY2V(4fI0N^opxo5bJ=N!zvQSmjHmyhArW0#+@mf;%HW?2p59)qoMl3AI1 z$OY9)VjUb(5!_5K`Y5yB%!r@aMfCYi+H&!C5BM5Dp&cNv2RdIQD{;&*5Zdr!SGx3ynL2kumkeA__?+|a7%6-^3D!!$tfcS#BLaRRrbev?C=L_@ z#9_d1Q7X~l_<}x4gUeq^RO86`TbldW@0}jo)Kt0g5_WB(=BvOs=HZ3Qs;e_*(vHTY=Z0IyBp?e1+ffH^Es`XeMMc8suo zRr|6{Jo&#VZrJy#(kLI?L@&~A)(3hVT}VCk>iEB?;MHR2|5Xn zH=ICRR`poAN6QG;c3MH!8_u*TA{U6O2Ow^MRIz6{4ntH%6Oo_-&aa@1(?Z#wIaT%u zfPtnTx5Xvb%NyCSQe~die<1&A%fbg80d^V@96@nnQrYOlD_v{nI%PA)tW~y#q`AkYxWTY zZ;zW-oiEdGjty`EAXrrjQ?$mswqbz#ssjw*b$bq^eC+s$2ezDdWPWd!p@%PW{toO+jOkxbGQJx&W>0k|14F=_1jZ@*b&EUZ+%6jOYE#w}`lj`z)Wf3VMINs+OVc>dcg#WJ-{)KI{KjZ?Q#u%V^M!9UuAn0$Cqn-2qQ zu_O(fgQjUcIWzKrY{^+gX2aU+*KToJdk28B*w-x zvssK0gzKRK@Ly;A{TJXiGOB%;liN0xy{j<3zzzFO%BobRvxHsR7$dw(U>lZhstYAd zAA79cihU$5J_!_aES#%8DIr#06ihix{QjI7@z!_wsP97nz#?pJv5?XMN$d$xJPqk5 zG4uLP(<7M#f5Hm5*(<+E_a3e0dzQoI&VZp~h`^b-g5xhA(#RyPO_dObHngXiZD&A# z2cdR21K`ScU#jZ}qaCN!KPht_oydA;MhG{S=hl*2C1wd<5vV8} zY08{S%LtwDAw&d2a61MU{RZ!`zzEfr2?zg%{1TsCuw`3y0J)sBbefF@FH_JSC7GD6 zhFjRu+7Qlur0m0@|nXL_A9)&(OD1H_4 z(I)hHPc+{ZyLnZ6jGF%C5EZGT`%-q(^1)CJ`sA)WEsz8UzD38Ei(EZk7M9dg@aU)* zGyy@3dwHXU6uDxBfL|fe1SF%gW^DgE4BOvbrm<8eeF*Z!LpCC7W9TenmvXM15Dp%q zKv74(ZQ|AsHA-c`lICNp72mVw^MatBV$idS)ZXzn)Oq!})9taGe$ zBj`V{4!*rzA8;uuUJ5#l(xp8us-+&s(-eaS>y(9*V=5VwG89DQw8YQNe1<&DIXFK< z^@g%Hl~_NrguDNah=FkL9l2sr5M}1TL<~lg{E|0wQtVfh=a%2kld4&9eBV?U=RbY=g#QanLv-MoPh^4)84JU)G!(; zN{c`2rFz(GeUt+@&4y0AQ)2o3Hy8xSz8@iMv#tO0uh4>qTN`YiY1X`ad@sQ%pVLD3 z`~*K)KN3?EMXMz?AS}wsh)~G#A^~(2FpiE|S{H}1XFSiPC0gZ}-al7gAKkxImn&d@ zR_m!x^Z3ZP@sj)u@(S=LUTG^|VGyCV7&8SjXqE zFC^p$4gzs=q26S$>%oYh0nFsZxH{R8dqo5F{Z@Wiavsb8CKEI&CdS`v6(>Osye{^#rKt^(275c$7mmmTn2cEG zUT74aC3eDwRl-@vH|fCx9o>5+hx6FNJTfF)t+@}TX$C5PTcWmKLLSHE#TnCzY`a3c z&Aw-s^(SI+(^3(`x^biu5$H532XVAR>=wu7f+`$*A5`j#Trbr+P%UxaX;AOvF_DC>6wTo_ceQ7KXS=!;U zmUz|UBwSv8a8vJyG0%!W_UXsCaC$S#k*dZ%vM*%a0GXOUZ--BP2!<}(c6D;+&{Ay@ zzRIIT{Sl#L>SmdJv$9C}-%RzTA0dAf(vr11&n*2J9qH#inB4nNj&Fy%m=|afPEQ+t z+0O=#i8b@hdkK3C?-Tgt`QXvKSg7@IR^oScAW*UeCERfc%c6Mzj2E52piwE523-!q z(LUC|PT%@h`QM?RQitXCqU)aATG3@NjF9sEjsm5;6V^=8TJ1?7 z>`8UF0R_@DJZVUVW`ZY9*5`~(*Ug%0==M^Alh-3{Qw`|nAeBLrxE*I<#1O0x)R|n& zRn+*dIdwWWX0UI6jjb`{cHqnD^1c}j)J(vpNyb+ZB@GC4pDc5v zMq#zwmjmBpRl9_9iRnwl9c7=B6STahSfKckQ1YFdI2bGz-GkCr(IgMqm!Eu|dl^nA z@wfXHlv&T<&Y7^hfpk>s-#Ch7FUTlnQ|VneYJ#E+uVIL$SI;=9 zs>Gtoe;)wQ(o1#ym6JC!#DBc-zRaxXJpfv>c@X}hI5Z9A;|L~9fpNS=FakKdUsj%r zsO?6;)x2)WT_=`IIQ&rx&O1Ss3)?PM4HXN;*3@h4Tx~il*)4zfU9v+i_({h0PSm;6 zacdv;EgrWh3yR0>MrUNC>|vnL81>#+ms_ZUauU`1an`l#S)c^5`#zS8rn${C=X~Lx z2cU;FOQYp(>g(&HSI13s248j8%bp$-$Q2TEalCnO`eFd_1pD9{QoT-o6yeWnSBc!& zOWk$Dl6(;6rKug4t{csL4nJ#N(y?s1*nwt1Cn0(=_#qy0Vj`Pu4WSbA125pC?W3N3 zHXqbrOzSO_k{V=lvSN`lp`Mj+d<>HpQu-c{0nL<*BtbH|#LKhKxs+4xk)K#EHyo;A zw2$gTAztA`9_hp8+PQ(FA_a|pDNtn6CRe}q>jikE+a#}^ot0Wst@)5PoU;E{MCsGP z?-K|AZ&us*hP_haT(Kf)g7RCUdM7*y9}axF>L$Cd;OjWsX4<2%%OaG0D=Uin0Wf)w zMf2asA_Co`P%i!H=s|4Yep^HR_j0oy+>p#!z ztEC6lRL2zUPI>4rEk{+XyjSd&nsFaB3 zC`V~>I@28`DrSzzJj>;zarR!Oc8(<2)Q;2K?ykp-&KA&bJBr06M3Vic_{z-464S|6 zIV^p%pB*@7Kax`>9#7e6J$+_%=s_icSV%FMs1UI4w9GGeA64s}dV#M3?(*4(MslEg z?Rk}RQEhy=#liRf5i9<#S2%J^5}Zo+-`*6#KSz;&m0 zf91|qJP#{Ca~y|H8BwQFI1;?EOT2W)VFf`7_H`T9)b>m5~t3$qDm?>b+OOb8UNw3^2!y6J-{MMKT?> zIFnIzyJPb^Yk&5tbu|uwfWo|J(=QzHN&f%d2+#FxYkvCmkEXAqv0XK%M>Sx*_CJIU z>}*Mh^-@z6x6o1M=mL|42M+jKGLUy={b8{1jq!QK{{)JmHE22PeKDFQ?Az_X+k~ol zKdvtO-~l)sgOR3W;>2TE$V;l2X1YgXlbI?R`&D6&x9S9r(Ru9OOGcVcRJ2*`yqC;2 z3AsQ1v+Smii;i@S$j+c$>3eZPK#dkl3M20`joq!axb${?zwng&&e@O!8SO-m&iNKy2^VHaiY^Gkx>bp!hw! z{_tAm;Q-ya>NOU*dx_IxiD@c?D`me_vhehBmXT7SY!s*2_E~&9{Sqtgr)u>3+Uz-z zx#RahAsaDYy{FL=0i)xQf_w^_nVPJlqfd1ER#K$IL~P*yUJ7l@fSdp}H>w#IH_fR< zf$2hoREvNZh_|;C77g80JtV0Ps;jh}KbCwootG@!j`{tw1wJELNlB(7V~_oz$L)44 z|A(rtjB2wBx(!mK#ih7A6bTg9;uI?m#agtuyG!xnuEjmLySux)y95v1^u6n@@4G+q zBU#DGJTrUt-ZSTp|HS>;Ct%AHFK5=+DXzjfKMB7(l$zKX20b$3{k@P~70GGb+6Xkv z=h(Kag|ptm`8RG8-*6Hu%%{IS>-|G#T^J_aj(lCA*n!ZG96>BpTn6hdy#1=HJ(J(1 z-H;s@9D>&Q)koCpRSbuKbsGA@gKQ~Ifr2ES0#@A5hDI(r?2Wg2lG7z`G-Nm_#su_ zXoS8uYg!q75kWCYQe2PVaz6b_=_FOMhF4X0pFZ?KP==fQ*j?ch?=6g$WlNu|jY{38 zzLvkkA&Q?1pD+MC1{14FdxRZTkD*OjZ)n!-ZesHF(ERBpL$Uu4f&NSKQD{d29JxQ= zl&N&X9 zHBYHoC!{}PZPar(t|7F#hzr2*zE@C%&|^q*%WsM+<)K>G_hN8`Duri{T>~+OIX;-3 z3J}TX9x(`xFIf@iF=3I2bzw){N@7~hh>!(C;@+bbn&lyf2EcztPq9A?9(|>jeXd~% zNfK9fZkdwEcT4>n#^!nYUd2Ol#9TOJ1k%TyZAT zcRC)3&b~0xRP>#4Msi;dH4x(&=`c$cp|3sCJQ=&n@@cS;498iWX8vCL#1_13_k<$b zz#m=O0(zbU#}PF|YSc@|Ovpp==>IFYszbVkkTn!<;f2V-CKd|ko1bMxTRDGS89lG1 zVPSza4%Si{8v1Cym-jwdT*YY$BzLEFn!4bBAVoI4bRDoy=el9VHyK#H2i#%#HRLEC zwApAkljc|)^Ep;~loP{?x`onxqqzVEv)p4s*#3gG<4m${%^*{WVjEiS*^G?)wzS)g zVF8-jX=l)zjPK9g_Gf&8QuDKNOT2~n6upJC=`KQ(>2`Jcx>GBo$ry#*f;n=eIGpBF z7^m5$gk^aOgfLN)ysifa6Pmn~Y|pC2@nZwx8Y_>gMZHct(~9VJzT$xnJS!(8oUJ6t zonccF68Ej`tn@xm2>1BJbMxW8*KLw!+D9`N3$Cz>Vx`voqV-*V6IlySuzMOM3gXQb zijE|4u6!#sO1s|P*UWICAz!9NIi*vZ|3(y*P-s#ZG=XM%gppW@)p;5^`D1;346Oqi zPT<|&`p~k<`oGF5pOIC$)LPAw+4XCM*9!ZH3Cg$x9yg}n`6tQsBKd4oO`y0x?y6Kc zLYE?vH7lUMP^i!D=wQ9=Uk28{gxdf=m<&u$U^KDbuV>lW%4Sy%=13^MH{3M}`pqZh z=7oABy)SZGi{v)Q65a;c?i4vWCKSJHx6rtF-RtQ90DdAXQfxtwoPKvR0oA&rt(&>Q z5;pnNb&QkM%Gv7e9nU?Vt4_C7OV=m~3^oQY0?_DSXtgDLwlQdRg%Lx^(c9m}H_dE>!BSW3b7CK4n#5xyu*O%V)e0yj!5IO~ zTyAi~Hr+)FI{hl|hfLL5s&jn+TPxIRKl*SqvP;crB zVXu;m(@Sp*b&l(T>87UwDh@g*MCDW%$@z-kB+Acu1KKI|inX zIboquW}HM_J4)>_dvVtKsaCL0O6`yIWiZJxAMxkAOMl_xP194+-$WVI&LcUeb9#i zr0I2j%KKiD%q;h(!5D1Uxl+Pa&zpY3x4t=kE=5J}X*ICy3TTb`FZwtd+u%Fd=r|{w zaOqd-=YQ*I>`&^_v19QoIlC_vWYZ(3xNzwAX zrWCz!&A1CX0d)EfW7mRoX=)m3i2UbVojQJ-$wk~{ zL%<<+ZE`mze0Bv0ExQR3dlX#DE+g@Lde(R@x4NJR38uW~!fWrE%lb1mk8@eya2Qh_ zlx#w8#whT39wYI!9_X@Y@3I5)eFJ-H{2X%e#s&;3k>4Q0E81}*+V=Ax>*Jvv=BMTz z0C-TnmL0-j-_`w?@8+|Ku~Uz|o{k-mkQKsvRM}hC@cTyOs(GYijLgcw%DShUv>hbF zD$S4h&LRpZI8In0$d${=Ll!j^)gZ7)4O8ODbtE4)S{k0+dQfe@4N>n+MxE+3yQZc4 zcGG>0>Vg<)`IMQD7&$G5)8hJR_D7k=osL9Bqpi%cCuRI_aTHAg#TN>9o)`9sTx+}m zA?XwQ07)YRUfBDg#W~RN@*THg#?xdlRN1;`F}MxGc}&m4_#Y&hv07!Z(AF}M-kgBM zdfYbiGAQ0md*Hrg@9*=A;8vgMdUw_N_%cCL>rpC zGPn70>u91;2T79cnklJL$rG2*hME~h;8^+cBLF!J_0(#|JYMsMhQMS4%gbe-^qG}+ zDWbie-JdPb7A+UJQT>!=4TdW>IyY#2x>37R#&Ls0C5Qe)sVp9E#%j-0C9-yf^Fot0f5lS66e_FY*7& z;1P9onJ>$A7IA}b8TTa8eL+NPeewlQH!6Je&0Hv&APR&<%n*{PSSn92h~LI%`|%*_ zZQ8y1rDBlR)r3|OyT%k;XrAXK_cXnjotdJIozGzu|JqIFK&A>y+f&VOXd)q5Xy0}_ufFjq_J4K(==STjAd5T=?JnVVa>_miSeb@p<_UA`t03LBE6*jDt-1%mcelD z2m3~?lvad|Y2s=B*>Ab5qcToYE^t?F7g|EdlvxSP06{1gjH zkI!8^Tn&%BA*xu3$(JW0wj-DO{a)>8wvBrt{R)51EAG858inY)=aLeO4QHP!UVaES z;E7}|9LLQ!-}QfJO8M;or@f=iI`PxVCPZy>5_EiI3h`$(w4#ud$$R}mcAN|Paeg>AUi)4@SNI&_%%0*A(VOpe_0 z6Za*Lw~dL>mnh42aCsIQaIfCkfgXDJ2UKW?=;}P4uI-+exQgFJA?CEL{;5yJ0%;z1 zXDGI~YU1fg@UT3wJmH8Hn>7inYVWp_YN^|W1tK2nMT{1QS7+te-b6Z++t6o?zFf~@ zJ>^Yk&{9#l;WK3Jf^kQ}!Dl0!+S<1I!h*hk9<}qI3Tk32nw(4jyf+|(P^HaoIj+u9 zVR^bCaE&dB2;54kWRTc&MkC9Pq#^dvcXL2_;8%cUQShg%Nm@v2zUAC4R_V?6DwD4f z4UOSkLHeW{Y)-2pPLv(va+X!ABKEIqX|iN5JigTtb%Ozvzt5PVzp#k{l6m?0LGK>~ zM=xVjR|f!?uL=R4qnK)Pmndg5KY3Y4!uEm#k76@)FWHr1%9asjORD@lHcK+u z)rRN^|E9!t$n4vn2B8?4u#vAXQ_`TsKjB9*USx^vw(uX`X-%-09sJ0lnXoGR?nZ(d zJUpB)SLIz+L8^fd2U5b$?`mON8@qZ+HccL-s-~|h{%F*L(&nkHp;hN}DukY;Klg6* z$NspQ+Rn=3K+3Xk|JIHJF|YDZr@F&lO7un?0}zzx%|*HbKfVe^$I_0|Cw}|ysDpIu z-og0VaG%p`27o}%f^UoB{K20j2synIl4(9)4`psQ{qVuO;ez!-cCq@fp$5TPzRHfg zR_uoLE{oz{U|q=hRhAqs589gOiguMd98}}PCp+g_CTl+^Xn3o@Pyt2sL3tA%(PRZW z9&gV`(~WNzt>>v43x8UsSBhaRmDj%_6OqC!zcPj-KMNma zl@V4kluW3;LoD0-Aw8$(B);!*J-#FDY&O730lD5-(9Sq}@*n6yTmXgqpPq zG-e@Emlh;;Qf(@+fRJs~oRE#)&_e-te06-Cbc};i{q={9V?qE^u=-&yF5O|I_QhY7 zq|6*9J7YQfh0GQtz#Z{MkIR~Kl9I9q%L<3y`qj%o!eNa-WJ{!u3GP~XXQr&p&JKWO z#UW+G<3vwSFfXE66iOW^?{4A2&I14=p8Ikm{6Z*F=i(H{v2!lrd8B~<{rKM%p2)TP7j6PA{cAKBXa(4cjBIO0gImfb5pLUsv zbm*c(W82YF;~s@ujFt!V|I3?SNg3qecW{)216 zfOFMTJsOMCgx%lP{%Q%5*;`bmy5hJzA7-G#So%b|$SZJP2&?i~ z<(}S6S*ugMd+^lA3K_ccJpc00D49yHt8b`gzw-0k)(oQkmvhnv0yYHW*=Z}fVt)lFnIND<3@F$Yt+%;EU;xKhyRe^ zKrrB(&mQlrsgw>Nd>M*<*{7j1>S41s5{yQ&4Sn+z1+f98L0RV(M(l#lNA-V?o`eu< zQ(YAL%@X=rP5W~So>%gcEJk{*Fhgl04^Tw;fzi&sw{seVr&peFtsC}w7N7mmMdu9l zJuo<@tsf;yWSOBlYTy&@cqD3B#3*cdqOCb%x&_Hp;L2U$Mf>fvJ`{TPWQ(#$irMnT)G2C=j#w zWv;TkMI(eomguXGmgwiY7_~K?0#0LpIKSn4xRIt!D?0I?E8RH5xrqsSm-XL zvS~ru~eSb@0ht) zQu77JrpL$D=IYJW|Gl*0oLgZmq2>HVfQH(j(;}mSsdTo)53pT-y5Ki3u-{$csG;g~ zX+Ja3Y2gq!NDMuw8Mm1i5N>NaS8S-ca}2ijisN4Jc?IK0zC)+$B>WS?8%Je8#)w6j z{fEbJ1!LKLTCRENMZ?YOPVgSuVG3$$G4NM@WVjZQy^c-Wd7Rj&dk#%%@12{w&jD53 z-aMj=%85G>QVaMIb2vD9%}|S7BN_Azd>D#(h-DjCd0q!2w?=;?cEfMd><6kv8XT%< zu?yJ|Q|CKUBlMG^8YPA`7Z|i^<$j)KHjAIxBaDswR8<(-GfVHFkYs=aRCAust*p&v zr0dXO&e(IMoTIo4*k!s8HK-70?Rr?TlJG_0HNO`iwE{1u(H%afM$7`j|u#byRG zXrbFG>4v&}&P!$y#x+N7A)Xze3sx3JA&eB?{2i?K6ras?KDi*Ntjw8~o~AYux91EQ zRow7cC{`;g&tT6`${U3H^j{jt#z8pBFPLb&C4dJER0u?lShtK;kZ7LNH+yajx9_-=+7hK*R#K}qgjxs&)%dqun{HZBM^}&+`y-G$ zM*!v*ErfxDwoc1ivJ>lH%B1Yu$dK?adw+eYMH4*<*&nrTqvV6@=7Tx8sb4#66-J$G z>-CiS;6wL(2XZ^PxLV&BT5N3*gzjjs7Ia%+-tZaO*VK0CJ_iZ9^yODUYYWo1JYAN5 z#BJW70rFrb4CT7pnv=c;=XpnrL&ALqRVOMl%VTi0q6L5`SDG0v9HxX9ADB|(?+V>S zT1hhf)Z;EqRb^`U*#GsNNM!F#WbD>&zb|J?6S{M9AuYl}4im)|TpM7}%A{1*|LtV+ z%F1}sdA?|BpE$QZ&FQPN&bVav$PP6Lo>-=GAnZ^s6zKpS0Va|s8;iXW9;i| zOW}olk7jQPo&Y##=>2^Mfn(qg?J4jiZ#4R>xdF-HM-(=Jfr2n9&dkiO{CfC~Fp)ob z=I-ld(CzXugXOMp`JNJ~%fVb&y|LLewjBN#hJhuNTw@%tzYxA{AgV(}vcSjL%J^q7iy2N~ zNm5<6%i;a$wj{mEwk7W`HF7P9tJ9>E4Fe0WRpQ{j-9!HZkc)q1a3`MIM9b2Z0$Ub( z&Y9}Hz(?M{pc1xlB-e>4awC%mp~ljrAu3m2T}}SqC@Rv2vlz&U$w;7uVR(!fD%cnY zcn#iYD%T5&mAU7pU>p9Z;S6WFd)USi-u4aPQrM#>tNZ{`168aPi21&Fpk3G{d2V9~+4UjowK)B$A~e?%IDiTe_8WaLmw2nT zXL^cYABuy_>H3>_@K2#y58ry7xNN#iz%mnBh-BbZ?uULVCS~SSpRf~bDyPn)Qg_;tn8F=mhU|-t-Xt7RC8WbQi*9++bVsH=GQW|D z1#RLF7n9AuHT3oBybDkjMKleHq6omuP-0k{L*1UdvA5dVSLYKy@ z1HLVNfMSfdDTudIm-632$m&cYC|Te6%4p*J?0}n}Y$4XiUbksrLEa0Q*QJWe9i``N z8{{E(&?2aN;Gz+in(koRg`E?1-*vgp0l4TQ^+n|1@G$5MpQhOeep0|Mk+K#CX?T6@ z(^OFKDg1T!=*`G!&YvGmMQcC)Ta92>=dHpau*4pB6Dd=m)wDK^Wb#Tzqa_N)kzXY1 z=RW^(J>T$<*yI?lzl)nvDuqasr4o?D&_YkzPkuv~wo@oM!Ti#GI&@1seE zvC*8$i;>Wi;}HFMIr)P20t5{CSP8!n^#o6AUv6_y(SCm^fAQXKudz1)iqv4R>KvU` znDieXOrCdfaaUbxa_JsHg7Tx0j2aFH&&-NT*y%G{)6jiZItz^p7*f}??g>C}g=a{6 z!4Pi2XCE_{LlomkSIXkJ;U0O^2tMZ}SOXU3r9gsy_iZLjkEaH`*0e}+qC`%u1-$4B zj_CBis4XAuRm~EJYKk>Vs?F8akpii!t2td~W@Qm^rL z4HZhv2S|U+aRX2NY4$}EtgJXBevKiJyoZf(6-z9b5RwqBMU}}jS>Dn)=i9Eky7%Y0 zO>!q)XDiNR3x%g;!yePowK?Hh@Y$Um5CS7WeeASh+-@<5TL09k!2?Q^Wfw<%a6|{c z1exoT!g_~KhkiZoukuZmxGE_?an70W+hz9fP&pWF(lAh)B*~IPAI=AacbYVg%iZ7e2u3 zn)#vx1&B@WwqdEljOWJYVF>lcdEV~^4DdK0C9S{N_x6%Yf$Y&D({EtPXKnvF&v(PQ zEU1B}mYWovGKFWxmZKV0y?=Na-iQ@)FGu<8@9xvBKlgNaDKFUn)>u6ou&D?vT7h;q zT<}K+)jL-I&{?P`z;$3wLtiJO!P9U~?)Avi!6B|0MDc!oz&(P>SXW$3g#czUFFmX$ z-jh_7&q|XzK+5AiHToZnMs53f5E=X6$YR+a!s&dtE@On#lwy!}iCK5elH$78j{Mtu zrqjx=-#Pw?%QrF5%(UJXI5+3~T({ueXQT~bUAhSm0bY%o5-XwJane7*GMF&`zEY%4 z$z=Lg<~c#iAbkM14JUeHH5WK4G{zlExU-)dxE(Jtr$-Ln{Y%n=b->PUh=IVVyrY%* z36oq9{8~-tXh_MN+%Y(O{Q4Kt~ z$WPTSCs3Q*e>Y&u@M%<#oj^QO*26v+-aR1XTh!qt9sJKgT?w3(VN0(CAqE4axWA*;V`Kxwhis9FolMS;g*lPGA7tM1K43sJUiKxqxwW)!Q?30b zGiZ%n`aAw#D#GaQ3gGzta=SV&EWH8%iN?Gb ziKq{u4U=*)wlb*+lPUn^N;WGv-$HGFo}o_nmsm`>L9t)vKrH&8!S0+XjJqOQ)tri~ z$m#``Z`&R0vFvXw!U+TE1c&Sw?KNorcHLr_%I>->gY}>OI@~x--U9maZ_YoLygEo6~kIp)6Z`0>mtT9xIN zxJ=J^HQ8^Z@M48mWw@C{@@dS;w=`X?qY8nyeg;6vPupF;PERO@Qyuta22LfIq4Ju_L8txdksQoRgK{uZOM4vWFLJp%!!O_T{j7u4{YXZ!sl*mH^3h4FB{5L&Nt19@$IiT5^ef3PCZ(|GoXfCilue#omwhdFH{~=WGUG9zp z5C8F6%pSu~-4~W1%5dI*dCoYQg*xc|#Z2$VVwd(C@k{tg6>ez2J+1~;+UWl_9QfXP z`TO9^dsSisMgzhD@Myaj=Mj~N;QNG$8oo^;x~k_ZNVB}|&pPHnP5K4f9%A|BJ$ug9 zU&>4b!_aqT?q(DiM|DWgwS}smq%gUFLaDv}BMb3g4WD|YH5g%K!e*&8$#XsdQ><(; z*~5FX_D}o!C%+{fX5k+TC01W7h+MO!SaWFHo=7NneHcls501&oLrF9EmiD=zuo|}o zN3h#SNT^@GrkPTGLB}Wk7US-YmIub}ijLN2P%>#G#tC{x_zKp;)?c27Hs% z*3s;jbE9J0%9tl1C6G_^pe>nz$6s@LMq9C{#*n8QM!FIj7+zuh`tR`n1>jd*r!%0%+`F$9go|*y_8Tt0Z z_9(J4Nk_VFxya!rXiPVLVHAG~$L*m*FfDUbt$nW~J)$dow1_CMU!=&(_#Kh$enqp= z)Z|+3Z`e0Zr?ZlhVnKoZn!EL|%b(HD9tf{N+OH$vxD%fnzbLP#9iJ-qk+x>5=k=P3 zex=RB>|EW|&76Nuo{urEKx-(+qIW?D{&V;e(FvZc{Mt)wM_9OHD`_5>aOLqO8@`F9 zPazg;vV1rj%c?K$ggt?3VFX^t>kdK6Y;pjV5Htp~Jl5T}A2D}?SC9Nt594{t$^fOM zudOE_k09-mjsXwve8W?NbFR>DFsk5+96AG7{v2^a09It+7{8odgbX|yLBwK`b*Eu7 zT8I&5R5W2sgmQ0|v$kyBhrX(je$N1!{q7Ds6&U$xX?yv8q+-$3A5l#h`HJLe z61=r9hF4p6ZQ#^B*>R}zbP4EYes-vwI-A7G%Be^@sd8|0clATStXg1cOC900 z|0|iH=?oZ4PRr4r_^SKSOc7FY zd-1sZ{C+j3-}Psc;}E)&k|$r&w`|bKt42%J)q+cA`o^7h1*9&jpq`yXlOLLb4$+p) zXCnU@o^z=Ed)5&3!C2@nf$29tE~IU;-qZKUNa}vwsU&@__L}rDl$1#G~Uk?P17C|SAosacG#xaOLUTor(Y9L zTxa1GxbBi)*!>as6xD_X2gK`HF;yRXr9aW+s@k6j&C_#r#ePLNc@IP+@86l(+wG0r zoFBw8W~QX@wj`;QN{uN~{Ct%Z8FK_Thv`;ffyP8rgJ*nDef_&NZwp>XMTgAlI1y>L z-4q*7V0);@{IjPxgO?)5*J7=lFx;O+6-wwmoeGl&enJJAWZ5#fDTBLQP=tIj&+P-D zZo24_=Uxk~KI(y^$zr9r523sL8qq`<<%uk-a(})iD|JuyrzIcs6IFza%o@t!+_1WB z(fVcEf8Vk3YRYgsfRJ(w(Ry}TFv|< z;b1OBFTyUvZAdPG@GwO_UJ=KIOID8HavcekGJYG|6@fT9G&J$9^dclPmUNz1shyE> zo27cCnR0z^sD)4^S@&$LRXX=vS=V@M2%4Th`h*_#0Qdll6VGeIuL)1Rg^HKUj$?*^ zgn!j5BnE#nN@ufNIRZ)uEGXnvieXLdf zu3IEE(QMrl(VswT=`hs0b6ZbqgN|QuR4wUW$fe*f2*xpi_jZnf@i(JIn~NS3s}L9= z$CWk!y=yni;S^<7z$eN@(yQK;rT-F5Of{4gcvJTdQpzwW2m| z@^t4SnTm>8OOKDqpljOvLs)`)Kqkfh4!E8Ne!OlQPBnkPkxl3*3UapOs*uDYocih= z@98nI=bc;)O+yDMIYIo~B8%Jh?`EOc)bq^6W}Wat1_cC*vG$s2mV>v1QjVXKeMYW1 z4?dFMv-uj~a%03Ot}?OB{pLBow*8hCiP?&DC~M5#NgjnU1%k~zzj)`tBfO!raantm zJUFNJXC%pmMStDtRh#SAO{C!1d58O0%)-l1%2G;)fZM{*eZ}-K9fnDN0knrFxnR)y zXKJ4A%jK6)&9WC?X<;h_p{fcR_3O78bC7RtV^&Q|rfbz^_hC6Iw0v_Az^1c}g`sf+ zBJh1-m)Bz@B`+A}T=RUrx!1G51~*=XuwL0i;ZEys+r0$NNRqS*0oO|)uG7Xv=qo{! zxhZ#+P3%2x_uV*gu0Xdakn8gZl3SoR16A~8lnU`5Rp>Oev%cJlCc3P?CcbHCs&v7S zD-ME8HJv3)mBKtWkJ{(l34^vk205qPpCCA3lY}>GEvi$!!AoBB1xi~Ta1b6jSN5di~`6S+yaQAc)j z=@SY?baI7IRi));Zn!5}_tL?EJA_L8-&_+WMK#VAUEr0z7c<9;OW&(Y+O3gRQ7P6`7ph{+V+BEj=@J(YBvd1tOlZ6B zU9`LqJj-y1B-Rzs(t9efKJPFqTzQ(&COcpi*M}*#~TJtRzcW^jAiAxV!2Rnu9~_! zu|hg|S^1cYzQ>ud+->geu?Fu3?}l?4Jm`6i#AvBy;?PZ`JMQ-6o&8sT)Hl)iM1256 zGp0TqDE{(GfaBnoh^OnrXFQCqJiR$KYPTm0Y6$OHR>35Nasp2#mcMa2Pt5XQWzs!1 zM$+qv0K!ZBG2GzdqCOD=F(+-5MC=YsQ;7ICGc1MU98p4~yGWogTu8&!mpF~Tk=lgZ zwwtHifFMQJwZkm&%={hahG`&Xg$>#USWOI-!+BuPxv5znr5Yh%cgv_y-3v7(&`_7G zLecNJ+JX}%Sy*^@{3nHzQ_u6$-TFt!2kCLQ1y$^hDad)slBp01V6o*MQxPFU?7HaU zq;K9KhVr0&a(MWCM&ma8S(8N+zo%FSXWL*q_bLAy@`3PHiu{^D5AV})bMd|ZVT$gG zp!15`NYUWWol#nsOzjIVDS$xh2c$t>O4#4vSau8CInzvG~PA&RxcXRk$8$fJMFa;n}7Z+V0se{oDI2kMJRG77@$OA!EP-v zjILt*&he9YCbhYjrk(9uyWN~_;B1-HkFW4qy-!68dUrXah%@rn@iF|o3Wv{cd`iW3ygOF_fKIlq=NEw^zMfmJ?wAG8j?bLZr zP`~Btb_@E(t#!m&G$ne_r#xdatAFx_zGgQ$@mC^l;x(UIsw#tmqJj+puVc576&8z< zeAZGrFs{{#7pHF_*9f@U0qw>uTx(^#XopKqotIV>z+ZXdQh_&NQVy6IjKoK!r{6S6 z1>=71_=v)>&$t&@7^iK_6{}Q`H$5^s*{cIf!MF$-Hl!Xs@#iZv7(+6znop!EMJ0;G zP=K$HQ%aGv%{43yY`2H`#143Wg0V=mSk$$n`G(~Vy`p==w>!?%v7O4EL`_vgP0$V` z%5R|m@I=;j^RV#>C1kSTI=hI_xm?bgM#xe~udWC_LH}&LMED%@$L1u# zrs9TjHTjzaZ=n^y4HiQZx}cF(K%9r52=!<>;YLDL0qY!=6{aAcS#BU8GywZgre{+M zHLdRxCTgSrTsvjMy7O9>hmGeACN%NS&kNL(7C`Y=Z0|JQjM!iO)izLez!_f2}Fh8B8f6WN>NTmx&-i!TsR(GB>VBus^}|Vby5Aj%+3h z@weLMXb#q|%^p5O5wl}V!vrd7!UUmtw+j*}p*-qN>(o4yZ6ym$zwS&_YN>>q%Re?v zC(Vz1*hn($^611iuOG#GkXkaFLVUFk9XMx0HYn4C-O)Mm&1r#+OrLg#c zf~uQ^o_4wIPVTBcpksWz_kmxSf}dzg zJXn`y0WZgXbB%6rzQ@KL57l%R&9={YewPXIYF_dOj}|(*XU=tSd_TCdY%=+rn)kdn zND%vbV?L~Wz!2$mzgNaXuva&VMLAy*kS(EQ^_To;QXF8obX(}(Epz(MpUe~8?Lx`4;kxU zKTkZ&6+%E8e}(nD&X3nbC=cfgBx3Lwu7A_kww{tWIS_Or^)>|~qa(Z+-I{-(+wdJI zDCAL>gcBvu2hvbs+2Smu<_p|vjt+PFBD(F*Lj}Wy`U_f@ebhw{COEvbRH!;(q}pF_ zg9J5wYU*a_+s~=3ObA>}mmRDE`4p{O%#eJK~%b8HjFlVMUc zddPEJjpoAnS@_8?-*GWVvTxXq+(#LcBBA8`M}_8g<(splFJWDFW!)iSJ%KDJr^-G(bF~a+;5^^ejKxVORD2yuH5iN6qZ{VP zvX|o|R2vGJpbAL8(dD*jdbV*L;8(a;W7T}6hy|)VDKS8u820Cb?^kcbpzfSowC|`j zv6QUO_pFOGkk|nob@|jn?zkb2&=-w{QT=UV!FF?V$C^h^E}MzVQ4UOAwmJCk1(nY( z9YDe4?s)W;-V(DuPU3*z&7;#Y^{mVUEcvjKxBYJC+(-YsqGV07s^9oJIFS)!@e;qe z4Q?@!krO%nDb#<=i|FD#%SuoqN7_~yS`iH(NltaVhYzuL0f?ZXLmXD}uDw#T=%o$+G{DgK>pi52r(OVq%3m^+onbXT9~zQc@R(C#ufHq8+W&Z~kN&z` z`gStpG5#`j?4ThG2VlD3RVCA13N|bqSZ>{gN-|#ve7eY->vq1mUx{QlerbgWtIX%@X${I6VifJ`s_aev0- z;~!Uz?mpBpVxjGy^U`rl+SwPieQlD0j$s5&|F-q@n*#NZ(0g#Il(`KZ3Aa(TRJ$89 z3D}~koTSQR-%#j8+HvUzt$eG3HE&hexnnc3Ehr}lrRqlR3?e>dkD2#;$Sl^88Ys)A zZA@-)SLQz3-`{XLeMJELH2w`8bEhAWKyS9aYdU<%mHgLtZN)db_572xpvSv|r3QG2 z_>4H2O)I58yi6j#7HhA zyBNAZ`2fqnoeWd~&}Pj6?a1w`Bd{N;YTPZ`34T*Z41sho%akaSwo$N+@s?T#Ookoz zRLb5Rg$i+|)aO;ewMj}NlpH0gN~!&U2^miMO6^G>BF`t_Wjqw^=M4r9t}2k?>866UifG4To0chr{^;aH4B?jIkqZ$)SxE^rs6UK(NSk7BEEyZ?d`4}ycse>yi zh8UMD#;t#3Huiwp9?9CugX*L84c++aL*o6;V9a|&wD>X7Fq|eEgGrcz8A1z-G}#Rg zhzgUUaJOy6?K=MjWRzFqz7(5y2rGkpT;HLPt6HNKv)0#YQAp8H*Y@m`3# zLF2tER$ysWiqUw`x8s==HT=}Uk)Fx?3o-hk?vCvXOFsg(3Y z7uIm>fY7G2ndE4B=|=~{z9v8A134xvKF(TjN6`R)M@NtW~Q>(;dRtwrO4dA7v#*_r8 zcyf%t){6gzq=7UuBR6c55d+)p*rmr|bX;Y#C2&dRnCS1iHrn?A(;oi(+~~j$CYHA^ zMD&;r?l(xf>hD!kYqWtEaQyGaZl)G3Q7RAiHX;B<6{Lz-Ktorzams?2+Y-1^pButZiu)KL7)`WtSu73rlA`l@=kl7`Hn)G9eq`{A>`Vk6y>n7Y zDMO9k&{$(g(MZQ^)6tAEa-$~{(ru*$yF(z2`-{zoTadma|)hUS7nm3^IIm*eRcHd9WN)KG4A$XKCpmS*)XL7WJ zwzg2?@>aAopDvZqV_AQs9mR20tYuRjT`{C4kL&*=E-ITCf&g0>rGJtw^XbAJT*qVJ zOB*ZBp3gPOmS^HsUG`ak90f-@ieohQ!Q5Yk**PjYnLmRb&0(Sn%YPPh zeg1EW@wg23p!Z^l4Ff*@p9mkHzF20>9G9eBnfv+&UGDT4)-=L!C~LB+CMus1&$67|N-(s7Zrmyht~sJ6t{1a!rY= zLpFJ z?3u0u&yFLyX6stQ#fa&Tk;mw)TEoxX>7trni@1IpD$fS=Wu(G=gi8iT)*%`XO@U-a za~q?lOb4mXT+bNGk8cS74Y2gkKrzep4`lf$xiu@7wSAELlS04ugP~)Y;76$S%IxWZ zWJJ~@iEgE1nt4jYN%#H@+EV`pfu^jSZ^r(NaWH$J?=Lr%P#{t|3nEgzya(sWMPl40 zUUyG}(<5oEuoAykilm9+Z^n6=Ou{FSaBi!Lmdhdzb~&#CejMtY#pa)ilFl>j{~}>9w6WdTwvEQNZQE=nwr$(CZJ*ia56q`$ z-dk&3tF(`dGR_Q9+bE1oh{fLT8NWHRblI%RWqf>{V!O7LulwwFvbg&bpy&DM%kz2b z8CwOsQ5@Uj4gWWB1J#8IHjP*lA`w~KbqlDDztYSZK8~d7rD=E%hzRiwhD0Q)N1Fr< z*3wjm%(#B;v!i&~p0}@o2DvZ4`Fg%t+rFRI1ULF4Pxw)autoIpY-XaI7k$3ptv>#zF|KKcSWvk8;hyA7CpGyCjurKO~$>SXrl^Rw(Lf z^d~cya>aR;loQxjIm#%h-9MddIzE?GFa!T1;=qfi2!VLG@kVkBYG`Sw7IND@h@Vl~ z7KiuT)M2(>M@MB_!Q)3Pt;2K~1*uZ+(lad$ap4UkviGL zB-4ki``Kz<8r9aVimRy7xg+Q$G3ygDzN}N}N9nxK`hA!Ybp%eEV$m5i&mf|roY><( zk|pqs8Mp=xQK9O^=z;F=KSvp%-gy{3A%l-o&Rw=#O4nX1Y%&fd!&jkJDPD{59PYHz)BxHj@e0m0jwBlvmc%>peeRx6vcGump(m)ZHbg}eP>%D|c?JATA0 z60t!Dbg^*Q2LNA_=hkEOn=0U(Jz&E`hnrt$LJDN3e;upWH0USSlExTQf@o5x>Zll! zEZP+6d32wBE6LFK=AafO@u$Zs8>5pw6-duhla<>~mEU zy1K-K80OW@P`;UT3D)B8xAnpt9o5(KmKn2hOYQ9lf0<(XnVe(q)toZG?mA}W^%p9T zmHj1U}(!s)6>@eu{BpOhlIOdoH3i=XSpXb7tLL>RH?o(YkPg zhPjUM(#-CgGF`jCQ2=9$7|dz|1Mt;#NWnxuG^4RZ4Dexeas6YdIDRhrGkS5@Xyhbe zOJqdxpJRxLN>STZpp%jo%h@vwt>nmuWXz7a(l17^F$Tl(6pi_hkeh~wW9brMOW3Jc zR3BIx8brMY#6qmkY?*&bTb(E*yUJ+?qoe8}p6L=#Lauj(tFSHVgB*|tpsn-dy*GB} zsIY7acaZOx+?b#6gR@8z86rrLH9=-#8yN0A;4$&||GS>S6t-4rJv1#VGiCm^e{vg6 zS1SG5U)OtmdIs%@Clv!|*>)yJKL4kc$m9U=SqDbA2aQ_2E|+`UvFdPdL%fT6k^}R7 zi}BQBe5GYVp6imji>7bc1lI1+4-+ADCJ(RQ`=1hI5X z-*QYX_X-6Jelu?>%zg6JbR1b0cB%9$Ba>H8nQT-?GOKIq!IZgoahsCd` z>lK7iy~F^SR2!SfElxE7&pHe$R+`mN!-cT3iS@io__X5!M;RgDq}5ru{gU$7{I!;A zWHR`}(B{G(D0~x429n7RfO)|!-YdsXgj`2zYO%qq?tLo!_KS(Xy)IroDWxXsLEeT{ zWb}5mKi$;0opjcl9J~3%#vzo6Gyx+DvE&^%3W+DT8RQSC@C!_QVq1d%4~lc@RAw2E z)*d3S;MqY*&Jp*idnJKBKXO4^8eX_E5flS#EE1UP-wY;YkJQv1n5;~humf^S{HrHr z>Xqs`B{`(g%PU$&sW?>oisyS584x^mOrVb7PPV9uzA+vf!&k{oPyoi|Cm}E6cTx%& zY%TMD19vl-pshrA83^JIwH8Bh#s8)bpZ=yQtM|nLM zj0yqDd4fbD(_ZgPs?c4-&oljBVZ<3d3}_2*BL1rcS}1fgiSv>93P`P(Zfe&4|H44J zkqJzNQ6UJ|?RmGgWhhLhRLyiKz=i5!;)4%~g-^3P&JK9wVeqni(Z_ zjSATF+f$L+O1B2XxG<2tm#g=pf>$F|yaxf*lIsXgnMQGCRxqSN23 zvp&_P0X0ojg|C9)uamY4_k`FFMKN+wh_0SVtUo$}n@$arDrU=Nen|Aw0_+$5((17! z>0}UKg-DXR7zZiepSVWY(IICz53&}P?`+g5p5limRbp8#4+YE+hKeE}A^+*2(5Z^Q z#w01e!LvNA3?M@0mA)Y0@s|UfT~!YDlkMSx6iW^boOMnt%Opydri=!~2c|2cnQL)m z10S3XENYf2g>%osp_ejl$9vrRFe{f%rpXgzv*Z1G;zNq?bw$kQI_0|iaBGhjc+2W1 zLG;^pzsWFLTIg6;Sv<5}o(ykYGDyGRi20D>_cIpU+<(cSGmFCk=gg8`_2?9nJNT$T z$>O<{k9SqkX}o_zc;0#`@zoUvvMsKf<)I(0TsLnrQk4ezrIdAMoP9(mWC*$~A-?4` zZPs$9Pm&ZuNV+Do7~*6AdyX)=b1g~puI-tf7(x$0Qlj%?@MR*IAz7s0X%~m~QHG1) zJaZ56Zsra>hN5$&A(x{_@*(PuJgEpyOM(8?qhYzxC;9{_{ z-h6A+lEk6hMrUYIJue@{Dk^8Is$0Rm?_TwqmMNcaZ_k^b-Cu91_8)HCBOT)Lq4NJN zbuRkyPW{W)J&l1AEeS%-lNTNDyU2EzYn|h8GRExv`PgNyl~!9Eo`Iv|ykn#Wzi1t) zyi5$0f<0a`2r@J)FTr02Gvsf>mlewZ8|cggFvhKgI6*CT=MM0TYWo@D2!>(VPHv*I zR(cEbT!!r3e;jY7<<_Ir+1Fb0^-@Fq(dGi$gI7w<60i}>n`YI~#%5c`MGbh=arsy0 zOW=_uFqLWYZEt*s16jJFcy;KkgsCg2bE3$#e}9-u=qP2A<;u&C==xPd+6GDv)0FKD zPwY9I97vKtcI3V{Whgf^cw3Zd*~(3>KdG5!*6aDKFWSEU&3n$Ftv!z5fo94}jsSrk z*tk;b-UEgNUAt7Z4R~&YYM&yGce;q-3x*}RnL}z!4CE;y{Y4S>Oc1IQYLh=9_AGZ9 zm>p=MgKOL0Z{?Y}Q9Pp+*xod=tKU$=>!Lao@dv2mSy?byNSDz1JyGUQS2Gth`}H)n zbJh&30(yDzNQXk8oi2XI@o_4qJT{%*TDuJxL7Lw2v$tasg)fZ5g>e$42%T*WGaq%C zJVEPs^4BwwG^s7q;Ad{AQj3t7AzkAP^IR`^vwsgI+Iov=3~3%1qyp{NBP99B$rxM_ zg;!WH&Oi~54%Rpzwd3nYYPRpJQPscH^XkV5`n(};`+DU-Ue|i}_SH!($eoea5D+m8 z#^-s(i5rRd!Jgs(@xEO@&H-MXX_1iPT)2SyQY#i|CIhZGZv~-i5`g&)Esm*>A8DaG z5`)K~kZz&_;3Mxt5k(>pv1an5HtY+2)_hOPc)18(W6^~^HPp4NVJm?$C5|}3V6BN4F@=GX266Yhv~JY*o0T9?YdCu z_H7KepH+1Zun(t0?Q=R4H~_tQUDor&Xg~WY&;h)I9HrqRGp?MgoCeZeG5X8Sl=z>jX-WeKtVtxgaws>iZ3oqWODgXv=2k(v;oK6T<8pV;^ZNE z`~d5uu-S^FQ<&%&F=)m~8H6>Xb$&j9adM-9I2{+<;Z1+_9nO^pVtQ#k({AI`0o=$J zp3(ar2dCK?>)Z^+7^~a7>gd3t7$=$JLhQXx<7C}cty4~HdS&tETFoMl6+8A*u1`~O zKBHf_2@Y>~jD|#+PMI+J(;jzx13p&a0P^ig-)m#nvp^8P#l;_$zz!$>zuVR9pZU{; zP5a^h8-xi{7Thbqw%>%_37X&Qq`D2)r;a#<7rTI){dVu?Crh?Wqot<$$N5Zr6dI7| za_uaWgPm?XwSQyCyhJ>An<8l4%z5hzwJdKDbFju~ssgk3QdQ!$C__yevHq%p7IFc- zF>Zi)(UBS-N@YbAMCSwPRWX*32I7FEGE2O7N$MFsehWGg_M0y2wwwrBBNIJMFU;{U z$w=0o(aHf~<5MlN9P1=MGD}8SVY}MG=f7P-B7|~<-4|s0)k~w z-2RCLwFJp|Vgx?`i0wBNlMQ9}W3_1|JlrVUB=hq0bCi9#If}_Y6_0~XIpdvZjnsMLy(PN=T% zq83?{Nw+cIo!G4{s$Kt%)Ws<=$TqHBF3cDn<{}-0VC1uDn#ac#ZoqQ~J zQD(g6SB$ZWy0dbj-sC+;uVwNP9B;Wh} zNoCq?mHA{Ski7|=2eRu!M2_#f?E63?HP`hkhQ9jzO{=`k#Nx1OGuMJ+WSAu| z4S^;*mY4=zreESzgpKOpXR;KRxu6)+A>>-$TC_$c6-dc$@+@m^!w(Bo@ydQ%71baR zq0QtJ>!7hgzlKPIzlKYS3U|TCrir{Gy$CjDni*NY>al(W8}VW;i(v5GMJ{xL+;ug+ z7O$7SNeMuL3s?D$dDa{Xrg4i&B_5ivudjUM-miX`@jb8;zEuM6U$Xx_EsyV6@udIM zr~ZjAin+f1jn(>U+tNK0u3Z#p*0{4+v!V)B>mVaTE9k>QWQF8E4p6jm`WVb)hLCY7 zSFD<(OXT%QzHqdlMpj=SxV1X~$D@<< z8SC0xYK#4kwEn{uTX~mQSnT%l~xpUN;a_$9a49b@cN*#Z|9Ge z+qYod>Hah0PwR0-;jI%?*#mj(Rz@RhqE42@HQhUD_@%@VW6rzwJUYO{49>GG-gRbH zMKotQ`C#Fj^7;0^O0+|^6Rf9x`Gq$hseax@o4JMT+)p7_hx_1CpgFty_qcjw7Mvo_ z5y9sFzgtbmY_1nKwXd~C!*(wr{VyTDj|izcA4PDviwlX>)s&0u%Hxwie_g6O)K9K@ z?N6j9=8URLvHEFMLB|M5<6&IT)hR(p1=PSFn%c>j{S41hn#=1!mBMa|*T7~~N>twW zi-k1hdZWfQE&E0<;GkKN6NVwf`2>zFA~*Ismy7W0ek z|NAxacfxVvppyRJBCC%O4uA1LwETrsvjCfWj=iw&lQ#jE_CD#)A1NP2dZM)n8aOyd zh>ORcQF(tzJk{wNkIVS~Kz zt6$=oM<8|w;rNVp&D&PKdkZqQpDe1XW2iu*+|XcG6fN!Nbz`H=@Z+*?Y5C?kXxB{h zC7v(>vV~ow3Vh9&nroC4lD&xxNLk=MYZTeA`qt_g7*W3$S2-gIX-*@DW?y9%0y83o zvDp0IJNexbo_oeiA*QStg`ijGkl@}GH|~HTQAM(LqLf@U`A5AzKtI#{9hGs`-Va#3 zq!})E_|E(oY^Qk*yQ>zFLf&V(fJMZ_HeoGcHxKG`+M)y`l{}tH%frc(WL1I;(qbg7 zWamFE>T9q_pf5w{^~<4u!*~E|ud5%imK*oA6*EASC7TA14Td|JRJXD7HNKM-E7qIb z&|P!)QLtX!W#8rZ0P7?BqSWIu-StY-xMytB-{p1pbw+z7&ShkkzT+M=B`5BZh6Q;~ zR1su9=~`TL6h$)+et$?-@T9+Vf4V!&d*^v@gjg+$&>f}S8hImVH7XWFgF(T^jb256^GIIv`y9LMDUUX zgou7Fc+&03jPX)`X$`@=A}v%5QdP>DmrS` z+~2RB*B!@Dpm}cN7}Rye5A+N1q=?O1V?ZQqJCg3b-0H8-4o9b2+jUORM1hcDk+9ys zi^k~e0SpkKo19c$#tw+=)~evQqXKuLo{e}LV*^8OE4%U;SCMS zt|(~)%9@|bk1Gk0@h&k9wznqkzgMxOQHs%;*(KxY6x>`P!Jl;@RrX~t%{xLNtE4>UC*uv`(zvtj{X%mK%lL9 z=i9SWWm{G^f%Y?NiYW^WRko6pQ`wAnOtf*_Z%ef0ym!%`j6KiuM3SqQ9(6rI|RYUrmpGRQl9K=yExr@P_7O zu(zYK6YQ13LnK1!;;iS8W6`I229{Z-uxhj{CSO*CQUGlk30(=Y@7g*_s;QG)<%_s? zGx(mYXBNASl<`*>GtnWz(_!SCT|PK2;mAOAqe|Ck1KO}&Gq-Z8j8>u(j1GDmrBz}X zsN#3QA75|w21nuDf79#y+V*}+%>8OyJg(a6yvs?(p{0`2ibQ|)BJbGde(f{4dk}U* zy7SrHp}-OPZj;;N#T`t;PL5Ad5_O;n1SK&Ip_CuI@l|nsp}yGEf)67R!Ibh41ZGw= zZ%L}_TuymDmUm4qK29^8*mqf*f&6;364xANk9l{y$-Y-2<%z6}F%7yfXOeEicqfcO zjkA^4-+442`lO6)xa&gZ+V49e9rn~3{iHlR01(Sg*mRrmxuRbquSIpar z{28Ki^n`-hh?SN<79%@izH6f~Wc7uGq)?b(QRfHOsTXH?T9g|ZX>3a;Eg9Z}tJ@yK zU)XB{b0Rq1H4nY*2_6Hy!z-@(%NfAX3+xme`2X|Rws7A$Ys}S?EdJRhyGPo-G# zM8um_OZS+Q_OQ^bSNemCnL#PdB%T_pm_o=AMD{>!g6ti2vl5DiKf!aznyWHR*;zR4 zax^)}#T(Xo8FmHPFoI>SO~Dh6#(hCcZrrx3E{o%bdvndGHz5T(Xme6U0SU) zSl@zxS-0vMy!h+%Z@%eq7|AJiY78{-zknZ<35@6L$=zg=`L(ZmM#T$?7OvS~$u2CU;=~iu=ti8I7#{&UCcFT#E&O$;EJT?CS8h@G>ds_9tI_cBs`*_Oyb?qtmd zQdCG5IoM-*e<~g`_LO?51zjGL=8|;?jDW)c9B#)A-IZb^@5Yc$FCNLG5oEz#rKEve zv=%Qs-PCi;aG~q8LOccZK7RzC={3I07ia##KAb*9)z_FHG~ugjWW?qNuQAe#EA()J zb%5V=?_OSoQdv`pv|{q2!z4sE0;XEBf_GLHJmjyUAl}K&SQAOY(PIV$T&p-t zcHf$zWI*>7`ePP#F-Mbs`04MIl9)~IGK-ZDnN|8iTI~rLR9uNSDjke!ED|S^f}p2% zK>3-++~kq(Mn!u!p*9c{+)wpvKR|m2jp8wd#A7y{nDbIZzh&RsWSc9Ukt*k6)81me zuF<|~)1)I+<$w@QkH^^YY=0JP%*YRY@RqH53C^PYG!(531Lp}U9GxDPGAmW#qjhs~ zRD=e$TLk5t7+2ApMKhW5D~^m-)o^^YZaf&x5nVIQpIT<3>=(R8@?=sIgV`y5gs>4r zLK1BFD@}=XlCQoOJZEwjr2r*}BDjAo2TI{*$?9)!4$=n0_4~u#e2k-}lNM4o!^lom z&jS-81wguKIQ60yxWYU$ESmX^Kz@-qj}#I>CxEp^IgbaWWi>NV-g6=l@RfhbGyPXh zO)9Z?)T{n8^7Y8;agyHm{u*rOG~S}oMZXgF6lfH5h!3sun6y0N(D2I|_!qU01RTo_KV7_LQhxTA$cZBN_*DZkS~9y|uZDTQ+U+0y z0eSs8X`?hi{(QoNi)<;P>xLkZSGx)1>-#V(c(25JdM&@o4Es=9b(7KS$2JuJkiLh| zQQ(8?$s)Lc{IMPs3nWTa81cJU4uL}pkP;ofG;uZ=RQnIdJPfG>Y!ul!mAph7tu`FAyj9$xKEN(Hm%iB9C)gW23_rs5~zQXTLs;h%WlbJ1u@H zC{e-@k{&5(Xv(oN&k9j2-HWp>I>tFgGjbX~DhA#6zsDPPmw`PPq1>S)Qm)!ZPw&14`c$NTU%jjpSawOIr^A&RU!NX|cpMbvjh zTwGi;iuH_Y%arrXYVYI5>dohnpV{5O_m+9rfJ0P6A`-&Hm&MmstmcToU+l2>I$@q1 z&Yqwi8&M-<*N{L}Jia9f21O6b8^(B7#MJims4q9Avi08WxcL^lI{?#HYxEy4!T2vR z@uADs%~`dx@~4-`Lr~Ry=D{Zf3@qr(b(6j*exY!!i(oA2hpg@m)-XONtYLCASz!>f zv3*3Z(=)zw#+_m$M?9^f3+#v0ItvBMfR&Lk!96tQR>Xr6NDlf%Aw1=yYJfC*`4o|4 z(00I!o7s7r@|5A92@;(m&qRv|{{WpA>4R|0g;dOsvU~R@5Aw*Ir|B!*?Lshav9q+S z6GZ~*+Ab)(+;0(&>gnk-&Uv+7#N7$ZSy~s4YO(Kvn-#~*v$&D*v|st8c0j1f`g+P2 z-f-;nehC9m?g0K@9SoEP1vxmoKf7E{9gTm#uyXW4&1vQ?xJ)L60vma}|0xYGCk;VC zIi`qJ{sRPBy(KPXjNcGvg z*ExgiteqPqyXD?Owhkb}$Pz}jNO(ptw068BxLy+kx)X7n7RDi1zaZxi8r`h227p#u zdZl+0c4WYbLC`)b_BqQ5d*IP+Gn+L?dhAT9B!fVF{*E`K{?#L3UKr9svW$8BmvW!; zEW_Ltn#dHqjW^9OBo}vq9A#<7I{cRZZJNK13DjC><$t@ODL~A$as=D$z*tJ$~_F2r#Qp zLJhck%tdiFQ&1uA{7n#=7FbICU-rde68BB!2x4n#3EJkuP(yuGrbDaw2HRxq;zwKa z%t92|IdpQ-sa$qmmj~|k*9m9qFL5UjQ)sOb>sRKRO@u1E>75$D7J}!$=hbzy=HF;z zViNrIas2gOy5+T*>HAXsHT+(E#nz^mtDGn&1Mp}CY`AbxFf_-YieN(WkgUQEC*ZAm z^j&nkj=lPGeD@wbq_Lr|(a}ojQD)FsdId>j{WBL2%S{Wsz$t20jbX>nVQHSSj}u0biFhUSirtP;JK zt+!h*+f!3B^*Y{4%DX*)jNZ-vP^TIi)J7ihB+nvqbg!~5i_FANe+XoFBk%+7g^gl< zdUGjWcgh++%KqM)*I|_riQ@OnaO@87ez&ZsbT0h)U#Lnd(Ax$4t;e(STv6xydu0{9 zGjCVOEx9n%k<0BG7GlVI5g3c;$$qCkR4m(X?QzPjVD&>5h`u9kV&7Gvk8M0f@-0d| z=mhg*b^jmwbIQb&EgoKyk&r#Xe*%kM&CSQgJ4*I`l#Gp{*`3TbszXvg_zQR;rtGhk z8VOb%MFdMY5FX?0->ECz@r5Nfir`97*ZNaOZtNP&GS|q9&f$J^zX0-Y@-9!Kj5)~$ zp|0^zr7Sd48-KYF_d_vFD;<7~^Im%_^Md@ddXqoUPN;kBFBDc)Q~Pw{s(kX9*3|_1 z4st(Xz7D2!KN9J?4hy8{IcDhOz9bG`!U?jk*P4DPjUzS+Z{6^|7THO`&Wsu8PdQuU z;U+{IiNj>Gg;avW+aF=PotjZ37kYMdb*uXSfW&JBX+r%aPF?<$b_dKEt%b{9A9#AFCo~4hO_?H8@BKT6gwU3 zjHrI^l%UilT6i2zKXd;%sp)!og*5@g#Kg1@dMdZemy8;siPSHelB0-=s64to%wJy+S2uTfVT*6U2n+zv$~U9`6P zAZzXFNZ{a}pgnd8FbcMoq^)YvS-*>fk7~=IcOG3FKOcv;9ULQ18<4L>BBG?eA!SMt z|Nh;4`dj6W$C{X%5AcY&Zeu?AK!un=F0Ry~5*{Etd3}%_V)O$e9ST<_CZ-hCmm(2% z$PhG4O3zq##GxH>a|kFC`WRwvhSgssJQaV6b`Udu~c1HGt99wua<-hLZB-3gx zc-&GSitXW-0ec_H;j0S2Q-UIgo;wt9G3o3rBn=C);xoR0DpDh1q$3xFi|8Uapj;8t zZ6j>S%I3i46LuwS9(JS;4o*Kg?kDX}ZFlhBX8y%v*mxyTQwtF5T4bS|IPd4#g% zS&@#FF5iCH-F~_3uWmVQlk+~9=by1@`d;nb)L?x~C@%wRF*3bQ$M@R7b5MlyZ(`6? z<;IJf<5Q)=B0LB$(5mMn6z5vSlRD+iL;``5lp;=t-a5+}M2|HevFoIBRDsx?nU{*^ z;l#0C6|#zyxoL zewJK5`uaP%Fq{&H3(lrtYjI^=q^}`azHW#bdN}F5KElIXN5=+AKH$376%>4Lu^jvL z3M)lVqo$${U}G~oSKb|WrK9h@P44!%j1;^PQ;T&m(fA9LR>A(5X11H>$>z-&+iC{E z>uAh-WA`pcp~f|$!WA7Fx-<6=l2L&gpM9g%QBh#e{QerH2r1)6b%4Z5F1?z};h6{o56YaI=B4T&BQgglE!PV3T{*Eb`bP z*WPLhv9FHTDMHH}@~i&EmCV4U(;DRbIw^N1eRZgG&Jic}E;9e;Z#1e9`70gzAff{- z3vIQy!k=5J(S^MT2A|{}&C#7kyUk7Icb+`*16mtKt+u*W2&NVEDZQMQ9UfNjtbITwPMswH5cv+# zmwy~olq6LP0;yG4H7%!Ntxk;j@kjVB=1{34Z1MJ=2Z=!b3W7whoiaealbuel8QX$F zJh`_vR_GW(_xwwpM1B!(Szk5Yuro|W7fe*bzsbteLp3?pWH1?j%oBCPwgGn1uj`w5jG3GRIfEOJA zy>Z*TPn}wK`-p96)4lD|f?}}W5OexjiPFjEc7s`di@-oCsf@JBjgLS|G|BZC1seLH zsEZ-+Lg!j<+xk32`6VYUC;wk&mOzwo|9`adEvWuRl~ zlnSlpq*_Pjh52kp^y2!00^b0;(xv@Go8q2S)ubX>r9Jh==)-csO#7T;*ZxC#?mAN> zD5VHvDC=VULNvrC-TNX~d}?RubIT{mLT%bQ%|!-%t_brQb1PhNUupy(k95qX=&#IF z=Vf6kqaNu*Py9{&huLr%`K*BN_hW~Nac!xVBJoEomxRZm9>h2j64kFZ`IU9eKh z*=|HNvf6sK0OcTh5yKmWYHddBgzM^BTK)g2j(y*b_&d*+^_pE?s=x1+)T}og--feY zuWkQTodGhCTO6hFQ9w#V`5#}zdk`gS3HQ>Z{uo32p!XFZOiW7i4vV4Z5)A!r@E16| zvl}|rq-gD0u+RHYOkhl7BGJ*2WjbL{c1C|Lz_Lgzb@^hV( z+a9C`k3f3`&XK6rXcv_#1eY46T;eE}A%Q*#Bvsj>7US}>%4hTNa*an?9B$&r%u|9| zTOQXSfWcGl-NhH)f9UY&#*tR4!+g5Aar8xC1Ga=%F$uB*@bz3w<-fX%>@KwglnZ!K zyXNqIM5SADNM?!$`%* zCJGGQO!QICF$)c*t`)QNgmOJK$7Y6m0u;p`g=YhM+QHI$>4v zT;JX|G_3)GE=Ak;r{=I!x}Kmf|B^Ci-qDs@z@`|yL4sm3`Om&;FqhF94rk(K^7~$! z+4z30J#XKgQp=UI(&Vs-L_p-<6x>|}R)0Y{EXE^x5xC;FGOQbK8Mup4N38%+^A&Z* z^mJqtG%Pwu|0-(!mx@F%40kHhOwSK4G$$kgdgwu5F)>Tm z{o88>7TeaUgGopoYb7yWL9yFQRq3qc&2$!3;dW`wGKi^MvV#B)&59r)>GQgQYG7}| z;43VB^**crZnjArm(+B*T(Sr$+8T|P?PNPlk6P-d?3lq}R?0HukqzM3Zef^-j6IgL zxb0yu5x`$=q)4pgL8I`cqI)3VVGH><(MC33wttCNo0*qGD|_x>h+u3eWpt-bnL?&N zR8f_AXv5~&Z2Q?r>3gWg2X~tV%wfa(FX~zXC}qpHxNoWU!Rx%$+HQdX*}irBUXFay z?l>l5ZJ6%APHWzz1BqV~A^3wX^1@CTPemnOY&4MFruJs=qQYGq(f0t7I19#5*y-&z z>^jIoBqGyV!-b3A!-??vW>{=``30$JD>^5hlq0 z^wUZhy;U8{{t8|dVcO#FAVd9aB)2NCu(ee$m;QA2&O>Vsrb;{gNh7BL;7UQ^*KIWS zb;jQuC0o5O73ANCTZ{in{CEeo0KVkp0Y6P_7m#xw#YXSg*CMmgY@(0V_W{B#@-E&3 znh{9Yf!wv2ZH?F`4J}53zftO={DiZ0QRFO0R+dcZFf-8&&(j@MG>~F33Zzyn ztjf?d=t^Ma&}OVMYW&pjeoA`h-`ttq7n|Y1Y=W6X+)9{->jXjT}1nS4ZZ$X_Sn0whmMxJzxltG2sb{y zu6ewlhlqzCziqPq;wgYnZ?Or%mph~hHa#JEy<4xMwNw$$;vL85F>0Hc}4Z0uSiYgNXuCwLj zR^piCMxIfQsLZQ)Fxqn^r2Gl0V|x0XF#{{N;uYd&ZYQJ53mt~VXDfPDNHcAt$5q8n zJNkPD?)MzK;&;Yaz;3v4KblLT`?>e})aPAOap3xt-WwAAYjLXA>=_Wm;s7*i**XUT zmDT7YPLd!s=N59F0o6B^7ws`&+GK~A9mH$e5>j9F_gRW#3QX5FbM6w13~0<~oY>ep z$L&UDUspocJ!6v5r}2NrFsKcS)ha&~S#;aJ&Pv;gk4i^o(N;ZUrYv|s zjl$b=_O3p_&AlHjLsnE>4iEOSwWR;GBR$jYZ450;K!BYzBa5KsT?U(em}Kf=0AKnb z-fTQ(5KMXho;5l;l!Y1BNYk#pyTAlTW@m(Pn(_E5CzUm^G1lPUBDsd~W`z%$G^?Cy zC(1x5gl1g=I&Jj{ew@fN0^uK6oe$_Bi_H4BDdxsw#590uAzheNRvQY08@#8iP3oZ@szI^VlnP@BoEEEOMkK7V~3OVZhS>X{{v?ZIlq zXW~`5lL%~BmY}xkxUSL@moPq$zYZZnGdWZhW#VBpbLMsWUZ=-H z+As~zgXI}e6dncO~vl z_q=?oiDoAT2})iILzfpmNb zKcRDt?sOQbOjdckIv9^s?0P_1>ZjFHyU$cL)qnb;aiG2^f(|pF`|#a%wTRVWxdTYx z+FbWMOst?Z9UD1lBmx>M>ttJPN`sdtXIf}-j7vaMfUK+e#`&l-F@E(z%TC}Z4K zkWn+M72lnm#{4l>i$2cFTYfq^J(mcBbP4eFyTcV#<~GK3fY!y3)3C#}h)%+9nv}d6 zjWx;ua67y$v)lH>^-|X`8TQ49sZqY%-FBa$KN@$^D{BCr`AaznotMZ=k%6;=XMZvI zUw!o}+Xg?eq+I@WKuwy~GfRz6I;K)g(i6DNkLT9EZ5Ul9rxH{!h%}xs0)ur>Xy?no z{Wj@6S8Yt6y&Tlfel8X!Y+{3^JxfO&=_185N*;Fbkq9|>O&_|wSf_557VAyu18CfC zuCuv5miUKe3lYU)(EIbn<8@KrG&N3P&dU*Ps&U=~)fTsVyR?q0G9T=5NHv6y9ECYf z7GI_u=I&3Oo4-EM|0b9=H-yG&mr8)49teXX)#9^X0FaUeY3O#V>__VI<{jWIBq;X8 zzcr$M45+}pnIpYATu?twjI%u`QnBZmN+uR0JqOFm$@Ahm;9rLL=NtGS-cfGyVW+9+ z4Valn^8E26dXXQSXxD;#bqjN1zCbLJHgIGQl7pRA$7G?}??R7V;=eL16<}ch!A&H# z>cCqnHf718r20W-0u{vMpe5LqNo|V89v^z`ZzKOD8-!G!F3wUAr{9`|Foj11GL>)S zb3Mv^#lfmL{{JF3pdDX74(i0N=XrnhUxoV*t?P46yY(jJy5Vz8J9|gSrXqBpji8Tc z7;-Nx%_Bb-2+EC6l5q`N%5*mJuh8IASwo>WmKdSXY&5GL#G>~$?NWRQg^JGar-QD4 zd{K>qYQ%+{ERlsYMuy0Quu(xjevx0@TycdIF92q@mqt-F9!QT_9iPf-z8M4?c zdIb%M8L9WA=m#NaV#_gm2xpJ%mQ2|)L}pG=T$E3bm?FalW|DVm-6)$S2Zggdk-Ub2 z3qn5}7>2}NN%8+m8SKB>do#O4I|2inu=`JMKz_1L)U-RFy{uB|gHVI~~!PK5m_moMW4@h(gR zWb(+SNQVBt_i!uRMpYYdrv{EYJ{u1_XYdwn8KL2@1(&`*}c_E##O}RhvCjy zMYFJLmpenj^~_u;X&RLIB&~AFNDgXZsWPegQ0(SSb5tjN7`~fbDYWGBo{HW2{3ZGk z^SDl zt%f12&4-qs3G+JM(X7TapQgUF^Z}S)>7FUkB-N9z^yq%*oCoePFFVkkXq9s2+tAVZ zdYDS!bGnSj@8#ulGZ%;4v7g?VfKODKbN`)!noc< zx5+Z#IUVDMEMOiV`%c8*Cu#4Kgjhs1V!*6(_O4IwmVHFnc3`rTqVRcHG6opRwfm&= z_J#IiHMTk|cTF1sXqZoQef-e# z5ow~dGPV1ntWbz~O>@*+o0bq2jax1mI#Kw9&(k;9A{-Kfb)R}kL1^Y+3l;I$pw$6i z`Q+01w0D1Kl1CR^98X3Cb;F4!?MBN1eafP@Q!dHQVmTF36v_}G4H$|No;hYzyMI1x zB&yNB-ISO$}KkCh*T(|M&%#4)}|H5!00#RYNWlv2#r`KAm{gjz! z@4OiM|9^AVTts_W|d@F?B?Io2MYK=vumu?Ke9qZo$H? zH>@ypc9`uE1th?Y#11<6&P$~mQ>;|JjZ*a2!%1Ix-gJ%lcI4O=Ebx+?9pMwzY8V>{yZYr3cIR3(4c!h%dU zOPFR{Mqy+HY_PrFUS+TbgX};MLE#`iPZBQwTmPwBnw&)HF?bV@tc5=+F{%;Es3PB? zeH0dw(F5smc|N#*9UwrkgSY@2-Xj_pft{HHg5$K{6)AD&N-OpQ@fq{t&s;tM(1xqJmx zbY<6e8_rP!+0P*+RD*yVlfg9%UGk2_(UpfK8INv895dqBA-bX8p_;pS``ahq^3D(7 z)>{c{YbCba=O*{hUM_O3OS+$dl-u#rmp-(1=U!a`dn}@rH+Nov+p0W zdB$EDRBZT{o`u-Ax@Ee=bgz&fjdo4+wv$JQ4SEc9Ot>^6eM&1);!<9T11H}@z6uVe zRUpy^mUIeNn){_Iqv~v2#;xf4sGrVM54ul(@oM#O65B~h5# zxa&Lt|KrnZYiEY7DN^Lzk^*9ie15`k)3s^6c3^GxZEyUxr#^1ag|Bb6FTJp1o_Oy! zedik@fcgl#U=njBsQWy~cK9Ny&cQ{#4w@Jn=nNr;09gt^N)bv5ZG0sqo@<^A00Dt$ z%U>6rqpc4KjYnd*Wr|Rh!bqTOVBblngq4Z?ir&nUOo?JGDE_N!m5YKLVk81-R`o2G zv2N#RV%bGE^?zQST=(016ppd_J-7wp&M$r0{~Mn%8umdTq?n) z0}|*;?V?mSP4xPZ-l7c1uCMwXFde1b06B!VmirX1iG2aU$31RMRXx}{B45`{bLPy( zZ++&YpMLf4{K)UzH9az|)(({2AB#Lfg_%IaxccfH=kMD7W3H}#pxvC`P2ip3yq%=f zq?}|y0K9$EDIb)W2O%8#Uzy$t#uO8=bCvVJ$Nk31=bbz``yc?6omd}o*-GOTd3Yr$ z@q-7qpAWx6P5m!@H$O$7CHO!=MXTJ?;QdoLBS5Hu1iedxcRHR#rcWvI3OQD>Uj3>P zdNMi7Gv+#GmF1XBvZ8<*Gi(|$j=6?R2WV_!kgOrf`fJph;B+qQM9t+a5_a{8fERe^AEyBImdikGUeBpAh`obgg~<&pm(X-LHH1hyU*X zeE1dX_n!F=HPM|_RV@*S!LIAWWFeA2{bxRsj`0!wbdPvRjgH(AHBro9G#MdYpKy$d z`ooBmFlN*a9%zO!l6GXNa~epzg^#s->LhgGW}&=KVj`6L}gPBRT?EgH*@ryR~MQ+2xcj z0dgsm;%AS(;=LEJJyvP+Qz3iK5)sF+I2n+2o;|hmdAEJX_7`6C#EX9H8-MB>cK|@~ zoGbE(m0($%zoU5?7~jfW*8-T3nS)91&*(VJYPLV5&-H}9${4AfeM+nz4+nA`xAR{+ z`IdKn007EPtPhHuML88Y2PGk-l@$V3+%E23K~4QSfbWIJga8L3f(!m$9QU7u_Ic0L zuaVx19)PT19RX81o|PIf)W-;z(+$ZPE4FGo6}$+!u^L|lN2wZR*vw!9Tdc*`>XLkj zN(Xx^qpuZGFLN+7?H${!EUgt0z=cS#4B%Eq$o1)LA6Yo5?)RWd7WJ93#)u>awqzPd zZ&PZ|V!*HC(CPd#ET?ZN>l0g;L8ImA?^|Q~EjOJo*QASBKO1sR`c6ONtp2uqS7>;e z6U{w#<*4NlZ3gn>Tt(nx%_r~rls#Ld$hj$nm@D%63&G#=eYeb3R;H7Suef;r)>pm# zNAJD!%s(TKzisNqfdCKRaad+YOq{&fI2*JBND)1ygP%eXXYqJXYNYdOJ9hz7<0E+} zU#V`=N>42~&D`ZRa?vDFUP#o@(k=}S1w3W?Si7yqxFICXR31?wj|4$smWWc)OlnC$ z3Mw0MUaVUV994^AU4pB`g)pa z>wacaA=?Ce8E}E3Goatq9?V@kVb-qy zJf?vdFFUb57@~*mtnV*>#C~yF=nuD+r|+&csBEaX?0`JvHDLfC)BVY zY??sk5nzRhQX0hI_~tzi`rCLZ=_Q9b#>|%VffmQ4RnlWY!L%AUCft`uxxR4O6hUKx z`}LrQzGTQ}0+jhO)ZBB*EKxPMuK#ZPPqGd93UF4~6M~UKI$5G=@!_!3`X;a%R#`2N z9FEtNjs2|qG2&o*T!6mh6YUVB+e7k9ak?MT$`~U6O?L*g8!=%7(%D}~O`*+5&%b^i zW2@84VId-=4KVD64bq>fpEwnmsw$t+0Mj@&AY9e=srFJ2byqZL8M6*~hUeY%UgD5F z0nEPhaPD{R{N3L@Io7j9ikxdwjIbh~zl61ntE+4C+wQu}t-NPn{r2nM{`OO!z4tY~ z?f%EoQbi6##55;Pb6zrf0#J&H(m`YNgae9%FoU^~oQxTxbk2^{2df};F_yPVUozb^ zmR_qO&QmWaLnDO@{@5g0!9xwCXSYUDYIwS^V|2RyPNp8EcyIc&34{&u%YTo##FPC! zgdi5&4Mbtq(FA!$^7sjmTac;7FrzZKR?pw3zRuedzw3JO@T)1G^9x35Oeu;9062D4 z=e_TI=eD1>{4Z|1?5VFje8b^6j>3V9UMTVilIfHH;G(}jaj_@*7Gf}b=g4!FtR|IS zjFhogN*lrVQLND*#$N~l;9T3y+yA`rmUn#wK=GC7hM1HCroPoF{-eK zeG39D0YvOb0=jJtxw7MiFsBgvWE6G|KuS0s6AUj}P%sUEh>D$dwjo7`k1#H=1;D24 z(TTzcOlfd*aq+`^7NLy6vewZHGtjJ{&}cV@zZk3ezVuU`#PreD;FyMo1_^UZVl3HM z4Ck{8kN#BadD5wD3GL^~GLgxD#1tE8gt6J6DKS+O3rq*sYrA3bElCfC&Ba(63-$zz zXxbA|<2(N`p3mM7BF4&!8?9K8BIl6Y@8P3Jk%gk%lU5F`)T^tjv*{bZ?Wub%dfboK zOZ7`;o3n}uEiu)eJUMm@@+t5`jIqTEM~Euy=V{VZSBMctK#c3d2;Oap3>qV*NU0-D zg2Z?UvOJRknHgnP`NmdiB_Tmrr7j}`qay8O!G(BT^u_kqSVEAJjVNu6#gCzxoM}?A z%hb+XRtZbHrHM*mawUZ5ON7Vbi3kq0b=DQLza#b=VF5Y2 z7dmDNtLrE^3$Gd2AWUSMGg$Qq+tifd*h#E@uGZbV}KYe0jLe zhq-s4<1j@}4#dp!%2ky^{i%~DPhbCzYj3{g${Vk2j^A*+9W=B^kuOXk0KDh=>%W;M z)vtH#D{g(|2n~b#C2T&kW|JFDlr(-Z*oW{6ic%*#0QD*3Y`+-N;ms#vxVj?nKg>5y zKmXJdF8csZoS+h0?m-tBLKoZNMa~6DihbX{ox7g-wAH$*e$aRRG=K>aIZ=Sj5=(77 zutf@|pW@2);poIqjt%ZHs|SouP9m6r40cIHlLB#hh!0&u3ws3NWV)XM{vnFwUK(Xc z;W|=GMZz2!>-AmMr<1Uo0@o6pQ!6m?<2ujy-C`dtoZ+4rvO|KWnW|=hM#|4{HX<(1 z`XXeRXO%g8rTnwJ)0tQjdT+e+SedwMFc+Co#lppOfsKim3MNImn)<{($O}p~iFx8j zya_mKYqX)D?s4|W$0w1~f&Ob+wIe2uLhM5u1!PPiggjzTkgW%+y2+am`yKr=9-q1_ zUu8ouGZUQKtea+AySe#}`lFwC?VY#WaxcVtzDSXCSc+#vkq1UB`rs2MPIS-zx#usf zz3$x~zU$+6zjkwd^Tws6$p$e`0B(t#2l62JI+8~^20)M1=6g?4=#zlBL35ry9UVwo z$&e8-oVb-m5_D10&6XQ6bxW{N0YqqbaZ2cd6e%ZpiO0pD27?0S0>tS?&Q3=i&Z&Iq zFOxQhGrf}UlW=q=@6U6rA0gdQYpx>JNAe((d#qMc{H7D~Hu71%8{Dgo^!=<>I6%OD z-pls>tA{G<&(v1cpQA|59x|Vda8HKm<8`tCPxnifK0afVo-wA_XbQW7Rc$-(*!hcg z?%4LzFMG?YzV!GF$LA|6D<$4skuQ7+*2|uQ2X}Mrz8#?5?7NPLCrKGBdeO3_T98*W zEh!SA;Q(WPTh9avd7z!oXTN#srnmnM0EfMM$CXo&B9A@^FV~9O_4KE{ysq30zUwxC z5LXUize<+1fx|6VhFCI8h7l1{+}0_UmX`QwN!W>V*a3Sih3g=dYyhUR;U?uDVOl;R zC+iSddFm6wGD@ALpe)KNt8k|*yHsS;pt*95o|s@Lk3Y$xuNT=SkB_E_GfDR)xWchC zKEF_wBg>N67)ONE#%>rw+X3*W^fv|{2X)rtI@xR%>I#f?dk9yO$`ZUg5bpm;=@s}L zDK~O`qm%PcH-zk|yfTDnXUH>=CsECd-(om#68kGXfvk#NWNpPd=J&%SOUs7i`-T)a zh}E%J|5SC)AolEiOYI-rdER-aRWB4Na!yMj=88NhvUX(6-|~I8%%&^T$&nvCa##C- z5B#3+yN8*e=vNWL;AsG((%NE|9A zVsftX@E_+EZwkw6JXTkW(eU6)rZ|e6gW|&SbKd^yzyBsz*Z&5f8i0eia}mHU#G8nB zQm=)8#X>5mTaJ&?BpgNEs0I;SDMv`a$nd3z!1$+FI9r;<9fL-b@Nk+Cq^$68Cm1n)oSx)OLDB;L3xuVFNb8BQ za33|eRV~$h6N#nRmlkK=XNe$9-+YF|zKb;(u^nP^8d@OcK1Q$^&XHMY6iUzTZU(4X zU7rA{dgj9y^*GtBlG=-_E%@FxOo1SSG?^f*VoVd^SW;Cs;OEcP}l3= zpk!IdiHu=`GHT#C28fL)N#cQ}Kheu(OL|tpR-=+hq((fami~zZ?;>cAJLBq~i2#Hl z@gqi@3Z5H52&ej_S`T2Pe58>6JODcq=vWkb4_x#z+acgI1;unpt(xYD4pci9D9jwH zBel~EAIEMmci0H*bR&jPRW`0je{B16B)bSa!bKdO%joqd z)n=7UcY`dSOBJo|xc40NlKt`DWstQ}0}Q!}RB; zJ8=lUYz9%}3s(9;*kj+my5;$EJ#eY_ZA*j(06;-xJXX43bX3>uwkks2#-m>6Eax*6n=7E55j{s@Icq7q{~{ z15|-P%uMW=;5~tr>v#sY5=BGNw$DA``&U2VFuks89}Xg^PG++N`K}sh;GOpL0!GjA2!2XXEYwl{pnwvmzDwu@gdaKJ^=x)D5;qYO>y;f$!}}L(2PVO>3A<@L7wrncU9kaUSTeO=_kkUH z2^Ly;oqq7XTLNY6e;md7$5|b&%C)Altl?1WvF%)V7MQqfY^k+-VxMFqG9TR~ZdsMUe+&4?rRZBaz|LdcNj=C!jTtQeVdZNe| zpd30x0C3(@FMX;b_wCeq&t1O<^XM>RE%6v%-N@+DA!U@&=uyP+))7}#RXfm~74UXi zSs^^{c^+H6=tUj~;q0@>xhaITwQk2tU;5a3x%vNBP5q_5ZD-_MAk!t8CPkD)ToBoi zgpKacoj=RdV&TKsK#^ypQb&$Tqp8KHhZbUBJ?;!CxvYkxXM3E<7R4;WMha)!42Fm9 z6NWQ0j_Q~kiUgfWU~(4^zSNiDI|rgD4#eX%%JH<$ee?>6bw)_>tk0V7*twyGFewzt zmOo#SkVWdk1OUg&&V$7ah_18NdU0E<{X+@ElkI;YU`HyK8grlCQHuE0xJytd(}BtL zZXS;C?z@0fSxH5ToYPVat|AYdq)-L+6TtBsj?dSyhUt-Mb@Quke&@-1Hol*jK3cn~ zp^iGoj(wOLH1<&pPMfBOn0%aT=@ThsE~J^64Mb4XVSYx(tB@>^&c$VkqH$Qt(D5R4 z{(8y2iXtkr6ve+N`de|igefeMuM)GC57Cyyx>;4x289f=L zUlX8W$Ze*0yICntTduBc0;loHo@q)urAwP-CXWPf3#he>p}I|o4wM*5+;U#LWGEez zjI5U&#Ywo_2+sj_yqEeJiWLlh@@$Z*;~2n=o_icQJhKsp@y;W1((3`JGG%u(Hj(0Q z3gFfEi)o$rp1QWB9Xpr5_X$sa!YdD~9q3lD;E#o z0S)(V5`8K)hU#}a_pAXByqf#>EBQBTF9!Knk9gr)Q3oC%BKE%foyqpq6Xe-2#nb>88YZT7Zm|zq!jh`2X zF}MJvz z!;qxOG*JFzIn0wDP~LBmOp=NBh(B!y**+uHJ#4TYhl1JkodUo)FjjSpt3cv4s!)UZ z<&d(ILJ_2HXoxlh?b(+W)g;@@{E@UrwbRr?uf<9CD}9FZVwNyE$5eaIA4a|OXJJDn z%5p7Ia3DZX-c`4dPqoqIy=}U*AUGa5K=l4of2o^yf;s=x<-;zJ_#&Nt zkOir9Suqs<9hOh(UpYeNY{L7x{2?K>820oKSAeFFMCxbAR5le;#ViKJGU2My3c
    W*t#9;OG-lAKW;W-E0^v=LUDGs<`46_8 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 - ``` The default **Applications** projection groups all concrete executions of one diff --git a/docs/src/index.md b/docs/src/index.md index 9e747f6d1..6446a1e64 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -2,7 +2,7 @@ CurrentModule = PlantSimEngine ``` -# PlantSimEngine +# From process models to plant simulations [![Build Status](https://github.com/VirtualPlantLab/PlantSimEngine.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/VirtualPlantLab/PlantSimEngine.jl/actions/workflows/CI.yml?query=branch%3Amain) [![Coverage](https://codecov.io/gh/VirtualPlantLab/PlantSimEngine.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/VirtualPlantLab/PlantSimEngine.jl) @@ -11,11 +11,6 @@ CurrentModule = PlantSimEngine [![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) -```@contents -Pages = ["index.md"] -Depth = 4 -``` - ## Overview `PlantSimEngine` is a Julia framework for building soil-plant-atmosphere @@ -23,6 +18,14 @@ simulations from small process models. A modeler writes reusable kernels with `inputs_`, `outputs_`, optional dependency traits, and `run!`. A simulation author then assembles those kernels on objects in a `CompositeModel`. +Choose a starting point for your work: + +| I want to… | Start here | +|:--|:--| +| Couple existing models and run a simulation | [Run models on one object](journeys/users/one_object.md), then [run them on several objects](journeys/users/several_objects.md) | +| Write or adapt a process model | [Implement a basic model](journeys/modelers/basic_model.md), then [couple it to other models](journeys/modelers/cross_object_values.md) | +| Understand how the pieces fit together | [Read the mental model](journeys/users/mental_model.md) | + The public scenario API has one application-construction form: ```julia From 2c8849fd48d21978efebc5983c7f3717a8fe5d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Vezy?= Date: Sun, 6 Sep 2026 09:43:43 +0200 Subject: [PATCH 2/6] Fix intro to model coupling --- README.md | 11 +++++++++-- docs/src/index.md | 34 +++++++++++++++++++++++++--------- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 31fbd5bcf..bb2ef8eea 100644 --- a/README.md +++ b/README.md @@ -62,12 +62,19 @@ This example runs three existing toy models on one model object: 2. `ToyLAIModel` consumes cumulative thermal time and computes LAI. 3. `Beer` consumes LAI and meteorology to compute absorbed PAR. +Run the full weather year to see canopy growth and senescence. The bundled +file contains daily radiation totals in MJ m⁻² d⁻¹ under historical `_f` +column names; convert them to mean fluxes in W m⁻² for `Beer` when reading. + ```julia using PlantSimEngine, PlantMeteo, Dates, DataFrames using PlantSimEngine.Examples meteo_day = read_weather( - joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"); + joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), + :Ri_SW_f => (x -> x .* 1e6 ./ 86_400) => :Ri_SW_f, + :Ri_PAR_f => (x -> x .* 1e6 ./ 86_400) => :Ri_PAR_f, + :Ri_NIR_f => (x -> x .* 1e6 ./ 86_400) => :Ri_NIR_f; duration=Dates.Day, ) @@ -78,7 +85,7 @@ model = CompositeModel( environment=meteo_day, ) -sim = run!(model; steps=30, outputs=:all) +sim = run!(model; steps=length(meteo_day), outputs=:all) out = collect_outputs(sim; sink=DataFrame) first(out, 6) ``` diff --git a/docs/src/index.md b/docs/src/index.md index 6446a1e64..458e5c6d6 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -110,16 +110,24 @@ This example runs three existing toy models on one object: 2. `ToyLAIModel` consumes cumulative thermal time and computes LAI. 3. `Beer` consumes LAI and meteorology to compute absorbed PAR. -The model kernels are unchanged; the model application layer says where they -run. Since no `every` is specified, these applications use the daily -cadence of `meteo_day`. +The model application layer says where the kernels run. Since no `every` is +specified, these applications use the daily cadence of `meteo_day`. We run +the full weather year to show canopy growth and senescence; the first 30 +winter days barely accumulate any thermal time. + +The bundled weather file stores daily radiation totals in MJ m⁻² d⁻¹, +despite its historical `_f` column names. Convert them to mean fluxes in +W m⁻² when reading the file, as required by `Beer`. ```@example readme using PlantSimEngine, PlantMeteo, Dates, DataFrames using PlantSimEngine.Examples meteo_day = read_weather( - joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"); + joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), + :Ri_SW_f => (x -> x .* 1e6 ./ 86_400) => :Ri_SW_f, + :Ri_PAR_f => (x -> x .* 1e6 ./ 86_400) => :Ri_PAR_f, + :Ri_NIR_f => (x -> x .* 1e6 ./ 86_400) => :Ri_NIR_f; duration=Dates.Day, ) @@ -130,7 +138,7 @@ model = CompositeModel( environment=meteo_day, ) -sim = run!(model; steps=30, outputs=:all) +sim = run!(model; steps=length(meteo_day), outputs=:all) out = collect_outputs(sim; sink=DataFrame) first(out, 6) ``` @@ -150,7 +158,10 @@ select( ) ``` -The outputs can be plotted like any other tabular result: +The outputs can be plotted like any other tabular result. `Beer` returns +`aPPFD` in μmol m⁻² s⁻¹ per unit ground area. Multiply by the number of +seconds in each day and divide by one million to plot daily absorbed PAR +in mol m⁻² d⁻¹. ```@example readme using CairoMakie @@ -158,16 +169,21 @@ using CairoMakie lai = out[out.variable .== :LAI, :value] appfd = out[out.variable .== :aPPFD, :value] tt_cu = out[out.variable .== :TT_cu, :value] +seconds_per_day = Dates.value.(Second.(meteo_day[:duration])) +absorbed_par_day = appfd .* seconds_per_day ./ 1e6 -fig = Figure(resolution=(800, 600)) +fig = Figure(size=(800, 600)) ax = Axis(fig[1, 1], ylabel="LAI (m² m⁻²)") lines!(ax, tt_cu, lai, color=:mediumseagreen) -ax2 = Axis(fig[2, 1], xlabel="Cumulated growing degree days since sowing (°C)", ylabel="aPPFD (mol m⁻² d⁻¹)") -lines!(ax2, tt_cu, appfd, color=:firebrick1) +ax2 = Axis(fig[2, 1], xlabel="Cumulated growing degree days since sowing (°C d)", ylabel="Absorbed PAR (mol m⁻² d⁻¹)") +lines!(ax2, tt_cu, absorbed_par_day, color=:firebrick1) fig ``` +The toy canopy reaches a LAI of about 8, then senesces around 1,500 degree +days. Absorbed PAR follows both canopy development and daily weather. + ## Multi-Object Inputs Use `ModelSpec(...; inputs=...)` when a model needs values from selected objects. Here the From 1571e9c76dedc0729b23e1951b633915ff167538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Vezy?= Date: Sun, 6 Sep 2026 11:38:33 +0200 Subject: [PATCH 3/6] docs: clarify scientific workflows and AI-assisted model development --- docs/make.jl | 32 +- docs/src/agent_skill.md | 13 +- docs/src/assets/brand.css | 53 +++ docs/src/index.md | 480 +++++++++----------- docs/src/introduction/why_julia.md | 141 ++---- docs/src/introduction/why_plantsimengine.md | 260 ++++++----- docs/src/journeys/modelers/basic_model.md | 19 +- docs/src/journeys/users/one_object.md | 14 +- 8 files changed, 514 insertions(+), 498 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 751e0f063..79b2557e0 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -30,29 +30,29 @@ DocMeta.setdocmeta!(PlantSimEngine, :DocTestSetup, :(using PlantSimEngine, Plant home = ( name="PlantSimEngine.jl", - text="Build plant simulations from connected models", - tagline="Write reusable process models, couple them across organs and plants, " * - "and run soil–plant–atmosphere simulations in Julia.", + text="Build plant simulations from connected process models", + tagline="Combine models of growth and plant–environment interactions in Julia, " * + "from crop canopies to individual plants and organs.", image="assets/logo.png", actions=[ - (text="Run a simulation", link="journeys/users/one_object.html", theme="brand"), - (text="Write a model", link="journeys/modelers/basic_model.html", theme="alt"), + (text="Couple existing models", link="journeys/users/one_object.html", theme="brand"), + (text="Write a process model", link="journeys/modelers/basic_model.html", theme="alt"), ], features=[ ( - title="Reusable process models", - details="Describe each process once, then couple models and compare scientific hypotheses.", - link="journeys/modelers/basic_model.html", + title="Crop and canopy models", + details="Connect phenology, canopy development and resource capture without representing every organ.", + link="journeys/users/one_object.html", ), ( - title="One leaf to many plants", - details="Use the same workflow for one object, a growing plant, or a scene of several plants.", - link="journeys/users/several_plants.html", + title="Functional–structural models", + details="Apply processes to individual organs and connect their exchanges as plants grow.", + link="journeys/users/one_plant.html", ), ( - title="Processes on their own clocks", - details="Connect processes that run at different rates and share their environmental inputs.", - link="journeys/users/cadences.html", + title="Plant–environment interactions", + details="Combine leaf, canopy and soil processes, with different time steps and shared resources.", + link="journeys/users/maespa_synthesis.html", ), ], ) @@ -69,11 +69,11 @@ makedocs(; version=get(ENV, "GITHUB_REF_TYPE", "") == "tag" ? get(ENV, "GITHUB_REF_NAME", "dev") : "dev", logo="assets/logo.png", home, - description="Compose reusable process models into multiscale plant simulations in Julia.", + description="Connect process models for crop canopies, plant architectures and plant–environment interactions in Julia, with tools for AI-assisted model development.", ), pages=[ "Home" => "index.md", "Start here" => [ - "Why PlantSimEngine ?" => "introduction/why_plantsimengine.md", + "Why PlantSimEngine?" => "introduction/why_plantsimengine.md", "Mental model" => "journeys/users/mental_model.md", "One object over time" => "journeys/users/one_object.md", "Several same-scale objects" => "journeys/users/several_objects.md", diff --git a/docs/src/agent_skill.md b/docs/src/agent_skill.md index ff50975b7..de4c905de 100644 --- a/docs/src/agent_skill.md +++ b/docs/src/agent_skill.md @@ -1,6 +1,17 @@ # AI agent skill -PlantSimEngine includes an optional Codex/OpenAI-style skill for users who want an AI agent to help write simulations or implement models. +An **AI coding agent** is software that can read and edit code and run tests +with the tools you make available. PlantSimEngine provides an optional skill +to guide agents that help you implement process models or assemble simulations. + +Models declare the values they need and produce, and their equations can be +tested independently. Structured diagnostics explain model connections, +execution order, and missing inputs. Together, these give an agent concrete +information to inspect when proposing and checking changes. + +The skill adds workflows and executable examples matched to your package +version. You guide the scientific assumptions and validate the equations, +parameters, and results against the evidence for your study. The skill uses the Julia tools available in the agent's environment. It does not require a particular editor or connector; repository and user execution diff --git a/docs/src/assets/brand.css b/docs/src/assets/brand.css index dcad12d68..5bebd65ef 100644 --- a/docs/src/assets/brand.css +++ b/docs/src/assets/brand.css @@ -29,3 +29,56 @@ html.dark { max-width: 200px; } } + +/* Keep the introduction readable on wide screens. */ +.VPHome .VPDoc .vp-doc { + max-width: 1040px; + margin-inline: auto; +} + +/* Scientific examples stay side by side when there is room for both. */ +.pse-home-examples { + display: grid; + grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); + gap: 24px; + margin: 24px 0; +} + +.pse-home-examples figure { + display: flex; + flex-direction: column; + margin: 0; + padding: 16px; + border: 1px solid var(--vp-c-divider); + border-radius: 12px; +} + +.pse-home-examples img { + width: 100%; + height: 310px; + object-fit: contain; + background: white; + border-radius: 6px; +} + +.pse-home-examples figcaption { + margin-top: 16px; + font-size: 15px; + line-height: 1.65; +} + +.pse-home-examples figcaption a { + display: block; + margin-top: 8px; +} + +@media (max-width: 760px) { + .pse-home-examples { + grid-template-columns: minmax(0, 1fr); + } + + .pse-home-examples img { + height: auto; + max-height: 340px; + } +} diff --git a/docs/src/index.md b/docs/src/index.md index 458e5c6d6..6b728f35e 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -2,124 +2,200 @@ CurrentModule = PlantSimEngine ``` -# From process models to plant simulations - -[![Build Status](https://github.com/VirtualPlantLab/PlantSimEngine.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/VirtualPlantLab/PlantSimEngine.jl/actions/workflows/CI.yml?query=branch%3Amain) -[![Coverage](https://codecov.io/gh/VirtualPlantLab/PlantSimEngine.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/VirtualPlantLab/PlantSimEngine.jl) -[![ColPrac: Contributor's Guide on Collaborative Practices for Community Packages](https://img.shields.io/badge/ColPrac-Contributor's%20Guide-blueviolet)](https://github.com/SciML/ColPrac) -[![Aqua QA](https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg)](https://github.com/JuliaTesting/Aqua.jl) -[![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 Julia framework for building soil-plant-atmosphere -simulations from small process models. A modeler writes reusable kernels with -`inputs_`, `outputs_`, optional dependency traits, and `run!`. A simulation -author then assembles those kernels on objects in a `CompositeModel`. - -Choose a starting point for your work: - -| I want to… | Start here | -|:--|:--| -| Couple existing models and run a simulation | [Run models on one object](journeys/users/one_object.md), then [run them on several objects](journeys/users/several_objects.md) | -| Write or adapt a process model | [Implement a basic model](journeys/modelers/basic_model.md), then [couple it to other models](journeys/modelers/cross_object_values.md) | -| Understand how the pieces fit together | [Read the mental model](journeys/users/mental_model.md) | +```@setup home +using PlantSimEngine, PlantMeteo, Dates, DataFrames, CairoMakie +using PlantSimEngine.Examples -The public scenario API has one application-construction form: +# The bundled file contains daily radiation totals; Beer expects mean fluxes. +meteo_day = read_weather( + joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), + :Ri_SW_f => (x -> x .* 1e6 ./ 86_400) => :Ri_SW_f, + :Ri_PAR_f => (x -> x .* 1e6 ./ 86_400) => :Ri_PAR_f, + :Ri_NIR_f => (x -> x .* 1e6 ./ 86_400) => :Ri_NIR_f; + duration=Dates.Day, +) +canopy = CompositeModel( + ToyDegreeDaysCumulModel(), ToyLAIModel(), Beer(0.6); + environment=meteo_day, +) +canopy_sim = run!(canopy; steps=length(meteo_day), outputs=:all) +canopy_out = collect_outputs(canopy_sim; sink=DataFrame) +lai = canopy_out[canopy_out.variable .== :LAI, :value] +appfd = canopy_out[canopy_out.variable .== :aPPFD, :value] +tt_cu = canopy_out[canopy_out.variable .== :TT_cu, :value] +seconds_per_day = Dates.value.(Second.(meteo_day[:duration])) +absorbed_par_day = appfd .* seconds_per_day ./ 1e6 -```julia -ModelSpec( - model; - name=:application, - on=Many(scale=:Leaf), - inputs=(...), - calls=(...), - every=Dates.Hour(1), - environment=Environment(...), - output_routing=(...), - updates=Updates(...), +canopy_figure = Figure(size=(720, 490), fontsize=16) +ax_lai = Axis(canopy_figure[1, 1], ylabel="LAI (m² m⁻²)") +lines!(ax_lai, tt_cu, lai, color="#156f64", linewidth=2.5) +ax_par = Axis(canopy_figure[2, 1], + xlabel="Growing degree days since sowing (°C d)", + ylabel="Absorbed PAR\n(mol m⁻² d⁻¹)", ) +lines!(ax_par, tt_cu, absorbed_par_day, color="#b46237", linewidth=2) +save("home-canopy.svg", canopy_figure) + +include(joinpath(pkgdir(PlantSimEngine), "skills", "plantsimengine", "assets", "alternative-model.jl")) +using .AlternativeModelExample +linear = LinearCarbonGain(0.2) +saturating = SaturatingCarbonGain(10.0, 5.0) +@assert Authoring.compare_models(linear, saturating).override_compatible + +function carbon_response(process_model, light) + model = CompositeModel(process_model; + id=:plant, scale=:Plant, kind=:plant, + status=(absorbed_par=light,), + ) + final_state(run!(model)).carbon_gain +end +light_values = collect(0.0:1.0:50.0) +linear_gain = [carbon_response(linear, light) for light in light_values] +saturating_gain = [carbon_response(saturating, light) for light in light_values] +@assert all(isfinite, linear_gain) && all(isfinite, saturating_gain) +@assert first(linear_gain) == first(saturating_gain) == 0.0 +comparison_figure = Figure(size=(760, 400), fontsize=16) +ax_response = Axis(comparison_figure[1, 1], + xlabel="Absorbed PAR (mol photons plant⁻¹ d⁻¹)", + ylabel="Carbon gain (g C plant⁻¹ d⁻¹)", +) +lines!(ax_response, light_values, linear_gain; + label="Linear response", color="#156f64", linewidth=3) +lines!(ax_response, light_values, saturating_gain; + label="Saturating response", color="#b46237", linewidth=3) +axislegend(ax_response; position=:lt, framevisible=false) +save("home-model-comparison.svg", comparison_figure) ``` -### Models And Applications - -A model is a reusable implementation of a process. A model application is one -configured use of that model in a simulation: it gives the use a name, selects its -target objects, and configures its inputs, calls, timestep, and environment. - -| Concept | Meaning | -|:--|:--| -| Process | The biological or physical operation, such as light interception | -| Model | An implementation of that process, such as `Beer` | -| Application | One configured use of a model in a `CompositeModel` | -| Target | An object on which that application executes | - -One application can target many objects. The same model can also be used in -several named applications with different parameters, selectors, or cadence. -During compilation, PlantSimEngine resolves each application into its concrete -`(application, object)` executions. - -This means the same model can be reused on one object, many leaves, several -plant species, a shared soil object, or a scene energy-balance solver -without changing the model implementation. - -## Why PlantSimEngine? - -- **Modular models**: each process model can be developed, tested, calibrated, - and replaced independently. -- **Explicit coupling**: `ModelSpec(...; inputs=...)` declares value - dependencies, while `calls=...` gives iterative parent solvers manual - control over hard model calls. -- **Object-based multiscale composite models**: scales are labels on objects, so a plant - can be described as plants, axes, internodes, leaves, roots, voxels, or any - topology the model requires. -- **Multirate execution**: use `every=Dates.Hour(1)` or - `every=Dates.Day(1)`, with temporal policies such as `Integrate()` or - `HoldLast()` in the same model. -- **Automatic environment binding**: global weather and spatial microclimate - backends are bound through `Environment(...)`, model `environment_inputs_`, and - explicit accepted-state commits with `commit_environment!`. -- **Performance-oriented internals**: selectors and bindings are compiled - before the timestep loop, same-rate inputs use references when possible, and - homogeneous object batches are specialized. -- **Generic values**: status, model parameters, meteorology, and outputs can - carry units, automatic-differentiation values, uncertainty wrappers, or other - compatible Julia types. - -## Installation - -To install the package, enter Julia package mode by pressing `]` in the REPL, -then run: +# From process models to plant simulations -```julia -add PlantSimEngine +PlantSimEngine connects and runs models of plant processes such as light +interception, photosynthesis and growth. You choose the equations, using +[existing model packages](#Scientific-models-and-further-reading) or writing +your own. PlantSimEngine manages their +connections, execution order, time steps and simulation outputs in Julia. + +You can represent a whole crop canopy, individual plants, or their organs. +**You choose the level of detail; 3D geometry is optional.** Small, testable +models and clear reports on their connections also support +[AI-assisted model development](agent_skill.md). + +For scientific equations, explore packages such as +[PlantBiophysics.jl](https://github.com/VEZY/PlantBiophysics.jl), for plant +biophysical processes, and [XPalm](https://github.com/PalmStudio/XPalm.jl), for +oil-palm growth. Their documentation describes the models, assumptions and +supported package versions. + +## See what you can build + +These examples show two ways to represent a plant system. The canopy curves +are calculated when this documentation is built, using teaching models and a +full year of weather. The 3D image illustrates an explicit organ structure. + +```@raw html +

    +
    + Teaching simulation: leaf area index rises and falls over the season, while absorbed light responds to both canopy development and weather. +
    A crop canopy over a season. Connect thermal time, leaf area index (LAI) and light interception without describing individual leaves. Couple models for a canopy →
    +
    +
    + Static illustration of a plant with a stem, branches and individual leaves in three dimensions. +
    A plant described by its organs. Apply models to leaves and combine their outputs at plant level. This is an illustrative 3D structure, not the output of the canopy example. Connect organ and plant models →
    +
    +
    ``` -Use it from Julia with: +For plant–environment interactions, the +[MAESPA-style example](journeys/users/maespa_synthesis.md) combines two species, +leaf exchanges, soil water and daily growth. It is an advanced teaching +example of coupling these processes, rather than a validated reproduction of +MAESPA. + +## Compare a different hypothesis + +What happens if carbon gain stops increasing in proportion to absorbed light? +Here, the same small simulation is run with a linear response and then a +saturating response. Only the carbon-gain model changes. Both models declare +the same inputs, outputs and units, so PlantSimEngine can check that their +interfaces are compatible. + +![Two teaching models predict different carbon gains from the same absorbed light: a linear response and a saturating response.](home-model-comparison.svg) + +The coefficients are illustrative. These curves demonstrate how to compare +model formulations; they are not calibrated predictions for a crop or species. +The models come from the [examples supplied for model authors and AI agents](agent_skill.md). +Follow the [model replacement guide](step_by_step/model_switching.md) to try +alternatives and check whether their inputs require different connections. + +## Build and change simulations with confidence + +- **Keep equations separate from the scenario.** Write a process model once, + then choose which plants or organs use it, its parameters and its connections. + [Reuse models across several plants](journeys/users/several_plants.md). +- **Combine different time steps.** Connect hourly exchanges and daily growth, + stating when values should be accumulated, averaged or held between updates. + [Connect hourly and daily models](journeys/users/cadences.md). +- **Follow changes in plant structure.** Add or remove organs during a + simulation and let PlantSimEngine update the affected model connections. + [Model a changing structure](journeys/users/structure_changes.md). +- **Inspect what is connected.** Find the source of an input, inspect the + execution order and diagnose missing or ambiguous connections. + [Explore a model graph](guides/graph_visualizer_editor.md). +- **Control interactions that need iteration.** For example, a canopy solver + can repeatedly call leaf models until an energy balance converges, then + record the accepted result. [Control iterative calculations](guides/multiscale/manual_calls.md). +- **Run repeated simulations efficiently.** PlantSimEngine prepares model + connections before the time loop and reuses them during execution. This + avoids resolving those connections again at every time step, including when + models run on many organs. + [Read about the design and performance evidence](introduction/why_plantsimengine.md). + +## Work with an AI coding agent + +An **AI coding agent** is software that can read and edit code and run tests +with your development tools. PlantSimEngine is designed to support this way +of working: models expose their inputs and outputs, and can declare units and +other scientific conventions. Tools report model compatibility, missing inputs +and simulation connections. A versioned +**agent skill** provides instructions and tested examples for the installed +package. These give an agent concrete information to implement, inspect and +check a proposed model or coupling. You guide the scientific assumptions and +validate the equations, parameters and results. + +[Set up an agent for PlantSimEngine](agent_skill.md), or use the same +[model descriptions and checks](API/model_catalog.md) directly from Julia. + +## Choose your starting point + +| Your next step | What you will do | +|:--|:--| +| [Couple existing models](journeys/users/one_object.md) | Start with three teaching models, supply weather, run a simulation and read its results. | +| [Write a process model](journeys/modelers/basic_model.md) | Implement an equation, declare its inputs and outputs, test it and use it in a simulation. | +| [Understand how the pieces fit together](journeys/users/mental_model.md) | Learn the few ideas shared by both workflows, before reading detailed configuration. | -```julia -using PlantSimEngine -``` +The examples introduce Julia as it is needed. If you are new to the language, +start with [installation](prerequisites/installing_plantsimengine.md) and +[Julia basics](prerequisites/julia_basics.md). -## Quickstart: One CompositeModel Object +## A small simulation in Julia -This example runs three existing toy models on one object: +The canopy example above connects three existing models: temperature drives +thermal time, thermal time drives LAI, and LAI and radiation determine absorbed +photosynthetically active radiation (PAR). The phenology and LAI models are +teaching examples, not a calibrated crop model. -1. `ToyDegreeDaysCumulModel` computes daily thermal time. -2. `ToyLAIModel` consumes cumulative thermal time and computes LAI. -3. `Beer` consumes LAI and meteorology to compute absorbed PAR. +First install PlantSimEngine and the packages used to read weather and results: -The model application layer says where the kernels run. Since no `every` is -specified, these applications use the daily cadence of `meteo_day`. We run -the full weather year to show canopy growth and senescence; the first 30 -winter days barely accumulate any thermal time. +```julia +using Pkg +Pkg.add(["PlantSimEngine", "PlantMeteo", "DataFrames"]) +``` -The bundled weather file stores daily radiation totals in MJ m⁻² d⁻¹, -despite its historical `_f` column names. Convert them to mean fluxes in -W m⁻² when reading the file, as required by `Beer`. +Read the weather supplied with PlantSimEngine. This file stores daily radiation +totals in MJ m⁻² d⁻¹; the three conversions below give the mean fluxes in +W m⁻² required by the light-interception model. -```@example readme +```@example home using PlantSimEngine, PlantMeteo, Dates, DataFrames using PlantSimEngine.Examples @@ -130,7 +206,12 @@ meteo_day = read_weather( :Ri_NIR_f => (x -> x .* 1e6 ./ 86_400) => :Ri_NIR_f; duration=Dates.Day, ) +nothing # hide +``` + +Choose the models and run the full weather year: +```@example home model = CompositeModel( ToyDegreeDaysCumulModel(), ToyLAIModel(), @@ -138,177 +219,38 @@ model = CompositeModel( environment=meteo_day, ) -sim = run!(model; steps=length(meteo_day), outputs=:all) -out = collect_outputs(sim; sink=DataFrame) -first(out, 6) +simulation = run!(model; steps=length(meteo_day), outputs=:all) +results = collect_outputs(simulation; sink=DataFrame) +nothing # hide ``` -`ToyLAIModel` does not know where `TT_cu` comes from, and `Beer` does not know -where `LAI` comes from. The compiler infers the unambiguous same-object -bindings from each model's declared inputs and outputs: - -```@example readme -select( - DataFrame(Diagnostics.explain_bindings(model)), - :application_id, - :input, - :source_application_ids, - :carrier_kind, - :copy_semantics, -) -``` +PlantSimEngine connects these models because each required variable has one +matching source. The result table contains their outputs over time. The +[step-by-step tutorial](journeys/users/one_object.md) shows how to inspect the +connections and continue a simulation; the +[plotting guide](guides/data/outputs_plotting.md) explains how to display results. -The outputs can be plotted like any other tabular result. `Beer` returns -`aPPFD` in μmol m⁻² s⁻¹ per unit ground area. Multiply by the number of -seconds in each day and divide by one million to plot daily absorbed PAR -in mol m⁻² d⁻¹. +## Scientific models and further reading -```@example readme -using CairoMakie +PlantSimEngine provides the simulation tools. Model packages provide equations, +parameters and their scientific validation. Examples of packages using +PlantSimEngine include: -lai = out[out.variable .== :LAI, :value] -appfd = out[out.variable .== :aPPFD, :value] -tt_cu = out[out.variable .== :TT_cu, :value] -seconds_per_day = Dates.value.(Second.(meteo_day[:duration])) -absorbed_par_day = appfd .* seconds_per_day ./ 1e6 +- [PlantBiophysics.jl](https://github.com/VEZY/PlantBiophysics.jl), for plant + biophysical processes such as photosynthesis, stomatal conductance and energy balance. +- [XPalm](https://github.com/PalmStudio/XPalm.jl), for oil-palm growth and development. -fig = Figure(size=(800, 600)) -ax = Axis(fig[1, 1], ylabel="LAI (m² m⁻²)") -lines!(ax, tt_cu, lai, color=:mediumseagreen) +Their documentation describes the models available, their assumptions and +supported package versions. To choose an approach, read +[Why PlantSimEngine?](introduction/why_plantsimengine.md). For a published +application and its validation, see the +[PlantBiophysics paper](https://doi.org/10.1093/insilicoplants/diaf021). -ax2 = Axis(fig[2, 1], xlabel="Cumulated growing degree days since sowing (°C d)", ylabel="Absorbed PAR (mol m⁻² d⁻¹)") -lines!(ax2, tt_cu, absorbed_par_day, color=:firebrick1) -fig -``` +PlantSimEngine is open source under the MIT license. For questions or feedback, +[open an issue](https://github.com/VirtualPlantLab/PlantSimEngine.jl/issues) +or join the [Virtual Plant Lab discussion](https://fspm.discourse.group/c/software/virtual-plant-lab). -The toy canopy reaches a LAI of about 8, then senesces around 1,500 degree -days. Absorbed PAR follows both canopy development and daily weather. - -## Multi-Object Inputs - -Use `ModelSpec(...; inputs=...)` when a model needs values from selected objects. Here the -scene LAI model reads live references to all plant surfaces in the scene: - -```@example readme -plant_scene = CompositeModel( - Object(:scene; scale=:Scene, kind=:scene), - Object(:plant_1; scale=:Plant, kind=:plant, parent=:scene, - status=Status(surface=12.0)), - Object(:plant_2; scale=:Plant, kind=:plant, parent=:scene, - status=Status(surface=8.0)); - applications=( - ModelSpec(ToyLAIfromLeafAreaModel(100.0); name=:scene_lai, on=One(scale=:Scene), inputs=(:plant_surfaces => Many( - scale=:Plant, - within=SceneScope(), - var=:surface, - ),)), - ), -) - -plant_sim = run!(plant_scene) -scene_status = final_state(plant_sim, One(scale=:Scene)) -scene_status -``` - -The same `Many(...)` selector would be plant-local if the consumer ran on a -plant and used `within=Subtree()`. This is the same mechanism used for plant -allocation models that sum their own leaves, scene models that aggregate all -plants, and microclimate solvers that select objects inside one environment -cell. - -When a selector reads a value produced by another model application, prefer -`application=...` to identify the concrete producer. A process name describes -the reusable scientific contract; an application name identifies the mounted -producer in this scenario. This matters as soon as several applications -implement the same process or publish the same variable on different objects. - -## Manual Calls For Iterative Solvers - -Use `ModelSpec(...; calls=...)` when a parent model must directly run another model, for -example a scene energy-balance solver that iterates leaf temperatures until -convergence: - -```julia -ModelSpec(SceneEnergyBalance(); name=:scene_energy, on=One(scale=:Scene), calls=(:leaf_energy => Many( - kind=:plant, - scale=:Leaf, - within=SceneScope(), - application=:energy_balance, - ), - :soil => One( - kind=:soil, - scale=:Soil, - within=SceneScope(), - application=:soil_water, - ),), every=Hour(1)) -``` - -The same rule applies to manual calls: scenario wiring should select the -concrete callee application with `application=...`. Model authors use process -requirements in `dep(model)` when they declare generic dependencies, because -they cannot know the application names that a user will choose later. - -Inside `run!`, use `run_call!(context, :leaf_energy)` to execute every target and -receive a vector-like collection. Pass `sampled_environment=value` to the same -bulk call when the parent has already sampled one environment for all targets. -Use `call_model(context, :leaf_energy)` when a singular call's concrete model -must guide an iterative algorithm. Reserve `call_targets` and -`run_call!(target; publish=false)` for selection, custom ordering, target status -inspection, or different per-target environments. Publish the accepted state -once, so temporal outputs and mutable environment writes are recorded exactly -once. - -## Where To Go Next - -- [A mental model](journeys/users/mental_model.md) introduces the seven ideas - used throughout the framework without configuration details. -- [Couple models on one object](journeys/users/one_object.md) is the first - executable journey and runs a coupled simulation over many timesteps. -- [Run the coupling on several objects](journeys/users/several_objects.md) - introduces stable object identity and `Many`. -- [Implement a basic model](journeys/modelers/basic_model.md) is the canonical - executable path for parameters, ports, contracts, a direct kernel test, and - runtime composition. -- [Public API](API/API_public.md) lists the composite-model/object constructors, - selectors, lifecycle hooks, and explanation helpers. -- [Model traits](model_traits.md) explains `inputs_`, `outputs_`, `dep`, - `timespec`, `output_policy`, and `environment_inputs_`. -- [Migration guide](migration_composite_model.md) covers upgrades from earlier - PlantSimEngine releases. - -## Performance - -PlantSimEngine keeps model kernels close to regular Julia functions while the -runtime handles dependency scheduling, object selection, temporal aggregation, -and environment sampling. On an M1 MacBook Pro, toy daily simulations run in -hundreds of microseconds, and PlantBiophysics.jl models using PlantSimEngine -have been measured much faster than equivalent implementations in typical -scientific scripting languages. - -For performance-sensitive composite models, inspect the supported structured -explanations: - -```julia -Diagnostics.explain_bindings(model) -Diagnostics.explain_schedule(model) -Diagnostics.explain_execution_plan(model) -``` - -These helpers expose resolved objects, carriers, copy/reference semantics, -application clocks, and homogeneous execution batches. - -## Ask Questions - -If you have 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). - -## Projects That Use PlantSimEngine - -- [PlantBiophysics.jl](https://github.com/VEZY/PlantBiophysics.jl) -- [XPalm](https://github.com/PalmStudio/XPalm.jl) - -## Make It Yours - -PlantSimEngine is distributed under the MIT license. If you develop a package -or model suite that uses it and want it listed here, please open a pull request -or contact the maintainers. +[![Build Status](https://github.com/VirtualPlantLab/PlantSimEngine.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/VirtualPlantLab/PlantSimEngine.jl/actions/workflows/CI.yml?query=branch%3Amain) +[![Coverage](https://codecov.io/gh/VirtualPlantLab/PlantSimEngine.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/VirtualPlantLab/PlantSimEngine.jl) +[![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) diff --git a/docs/src/introduction/why_julia.md b/docs/src/introduction/why_julia.md index 892421fc7..9ddc7dda3 100644 --- a/docs/src/introduction/why_julia.md +++ b/docs/src/introduction/why_julia.md @@ -1,94 +1,51 @@ # The choice of using Julia -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. 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 - -PlantSimEngine was originally a goal-oriented framework. Its features arose -and continue to evolve- out of necessity for more and more complex simulation setups. - -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. - -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 - -### 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 [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 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 - -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. - -### 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 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 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. - -### 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. 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. - -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. - -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. - -## Conclusion - -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 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. +PlantSimEngine uses Julia so that researchers can write process equations, +assemble simulations, inspect results, and improve computational performance +in the same language. This supports the model-development workflow described +in [Why PlantSimEngine?](why_plantsimengine.md). + +## Keep equations and their implementation close + +A process model contains a Julia function whose inputs, state, and parameters are +explicit. You can read its calculations, call it directly in a small test, +and reuse it in a larger simulation. Optional +[Unicode names](https://docs.julialang.org/en/v1/manual/unicode-input/) can keep +symbols close to the notation in a paper, while descriptive names make their +meaning clear. The [model-author tutorial](../journeys/modelers/basic_model.md) +shows a complete example. + +Julia also lets numerical code work with different compatible value types. +For example, a model can preserve `Float32` values or carry uncertainty through +an optional numerical package when its operations support those types. See +[numerical reliability](../guides/data/numerical_reliability.md) for the +requirements and examples. + +## Prototype, measure, and improve in one language + +You can begin with a readable implementation and use Julia's timing, +profiling, and type-inspection tools to find where optimization is useful. +Keeping the model in Julia makes it possible to maintain the same tests while +improving its implementation. + +Performance still depends on the algorithm, data representation, allocations, +and work requested by the simulation. Package loading and first execution can +include compilation time, so measure them separately from repeated runs. +Julia's [performance guide](https://docs.julialang.org/en/v1/manual/performance-tips/) +explains these distinctions. PlantSimEngine's +[benchmarking guidance](../developers.md) adds the costs of scenario +initialization, structural updates, and output collection. + +## Share the software environment with the experiment + +Julia's package manager supports a separate environment for each project. +Its `Project.toml` records dependencies and its `Manifest.toml` records resolved +package versions. Sharing these files, the Julia version, model code, and +input data helps others recreate an experiment. See the official +[environment guide](https://pkgdocs.julialang.org/v1/environments/) and +[PlantSimEngine installation](../prerequisites/installing_plantsimengine.md). + +Learning Julia still takes time, especially its type system and package +workflow. Start with [Julia basics](../prerequisites/julia_basics.md), then +[run one coupled simulation](../journeys/users/one_object.md). You can learn +the model-author tools when your research requires a new equation or process. diff --git a/docs/src/introduction/why_plantsimengine.md b/docs/src/introduction/why_plantsimengine.md index 6813f1051..154cede4d 100644 --- a/docs/src/introduction/why_plantsimengine.md +++ b/docs/src/introduction/why_plantsimengine.md @@ -1,110 +1,154 @@ # Why PlantSimEngine? -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 Current Landscape of Plant Modeling - -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] offer comprehensive functionality but present certain tradeoffs: - -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. - -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. - -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] 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. - -Their modular nature facilitates component reuse and integration, while sometimes requiring proficiency in multiple programming languages for extending computational backends. - -They support diverse modeling paradigms but may involve a longer iteration cycle between design, implementation, and performance tuning compared to more specialized tools. - -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] make specific design choices that benefit certain applications: - -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. - -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. - -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 address these various tradeoffs, offering a unique combination of 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. - -**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. - -**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. - -### Outstanding Performance - -**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. - -**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: - -- **Uniform API:** Standardized interfaces make it easy to define new processes and component models, reducing the cognitive load on researchers. - -- **Automatic Dependency Resolution:** The system automatically determines the relationships between different models and processes, eliminating the need for manual coupling. - -- **Concrete Batched Execution:** The model compiler groups compatible object - targets into concrete execution batches. A public parallel or distributed - executor is not currently provided; parallel execution remains planned work - that requires explicit correctness and independence guarantees. - -- **Flexible Model Integration:** The ability to easily combine models from different sources and at different scales facilitates more comprehensive and realistic simulations. - -- **User-Centric Design:** Emphasizing usability ensures that researchers with varied programming backgrounds can effectively engage with the system. - -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. . - -[^4]: Bailey, R. Spatial Modeling Environment for Enhancing Conifer Crown Management. Front. For. Glob. Change 3, 106 (2020). - -[^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). - -[^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). . - -[^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). +PlantSimEngine helps you build plant simulations from process models that you +can inspect, test, reuse, and replace. You choose the scientific equations, +the plants, organs or other entities they describe, and the exchanges between +them. The engine connects their inputs and outputs and runs the resulting +simulation in Julia. + +This is useful when your research question requires a particular combination +of processes or assumptions: + +- **Crop modelling:** assemble development, light interception, biomass, or + water-balance processes at the resolution your question needs. Start with + [a coupled simulation for a canopy](../journeys/users/one_object.md). +- **Canopy ecophysiology:** connect weather, local conditions and organ processes, + including calculations that must iterate together. The + [MAESPA-style synthesis](../journeys/users/maespa_synthesis.md) demonstrates + these coupling mechanisms with pedagogical models. +- **Functional–structural plant modelling:** apply processes to organs, + connect their results to the plant, and update the structure during growth. + Start with [one multiscale plant](../journeys/users/one_plant.md). + +The scientific models, parameter sets, input data, and validation for your +species or experiment come from your own work or model packages such as +[PlantBiophysics.jl](https://github.com/VEZY/PlantBiophysics.jl). PlantSimEngine +provides the tools for connecting and running them. + +## Compare hypotheses without rewriting the whole simulation + +A process can have several model implementations. For example, you may want +to compare two biomass-production equations or introduce water limitation +into a previously radiation-driven model. Each implementation keeps its own +parameters and equations; the scenario specifies where it runs and how it +receives inputs. + +Replacement depends on the model's interface: the values it needs and produces, +and what those values mean. The new model must provide the outputs other +models need, and its own inputs must be available. A variable contract records +information such as units, whether a quantity is per plant or per unit area, +and whether it is a rate or an accumulated amount. The same variable name +alone is insufficient. Moving from a quantity per unit ground area to a total +per plant, for example, needs an explicit conversion. + +Replacing a model for selected plants within one application requires a +compatible complete interface. Broader changes may require reconnecting +affected inputs. See +[model compatibility and replacement](../step_by_step/model_switching.md). + +For model authors, this separation keeps a process implementation readable +from inputs to outputs. For researchers assembling models, it makes the +choice of hypotheses visible in the scenario. Both can test a component +before investigating its behaviour in the full system. + +## Choose the representation that answers your question + +An object can represent a leaf, a plant, a canopy layer, a soil compartment, +or another entity you define. You can work with one object, several objects, +or a hierarchy imported from a multiscale tree graph. Geometry is optional. +Different models can be applied to different selections of these objects. + +The same process equation can be reused over compatible objects while the +scenario handles their selection and connections. A plant-level model can, +for example, read values from its own leaves. The modeller supplies the +aggregation equation and any required area, mass, or temporal conversion. +Changing resolution also requires checking the assumptions and validity of +the chosen models. See [value coupling across objects](../guides/multiscale/value_coupling.md). + +When organs appear, disappear, or move within the plant structure, +PlantSimEngine updates the affected model selections and connections. This lets +growth change which organs contribute to a plant-level calculation while +retaining their identities and historical outputs. Follow +[Modify Plant Structure](../journeys/users/structure_changes.md). + +## Connect different time steps and control scientific iteration + +Canopy exchange may run hourly while development runs daily. Each model +can run at its own time step, with explicit rules for reading values produced +at another time step. The appropriate averaging, accumulation, +or rate-to-amount conversion is a scientific choice. The +[cadence tutorial](../journeys/users/cadences.md) makes those choices explicit. + +Some calculations also need a controller: an energy-balance algorithm may +call gas-exchange models repeatedly while finding an accepted leaf temperature. +Explicit model calls let the controller manage that iteration and record the +accepted result once. See [advanced execution](../journeys/users/advanced_execution.md). + +## Understand what the simulation will do + +The `Authoring` and `Diagnostics` interfaces expose model declarations, +missing inputs, the sources of values, execution order, local environmental +conditions, and which results are saved. You can inspect why a particular leaf +receives a value, which model supplies it, and when it is updated. The +[graph viewer](../guides/graph_visualizer_editor.md) provides another view of +the same composition. + +These reports help separate a coupling problem from a problem in an equation +or its assumptions. Declared contracts expose mismatches at model boundaries; +scientific validation still needs suitable observations, reference results, +and tests. See the [model authoring API](../API/API_public.md) and +[model testing guide](../guides/modelers/repository_and_tests.md). + +## Keep repeated simulations practical + +PlantSimEngine prepares model selections and connections before repeated +execution, avoids copying input values where possible, and processes groups +of similar objects together. Model equations remain ordinary Julia +calculations whose performance can be measured and improved. + +There is application-level performance evidence: the +[2025 PlantBiophysics.jl paper](https://doi.org/10.1093/insilicoplants/diaf021) +reports a median of 5.3 microseconds for one leaf and one time step of its +coupled energy-balance, photosynthesis, and stomatal-conductance benchmark. +That result concerns the implementations, versions, inputs, and hardware used +in the study. For your scenario, measure initialization, repeated execution, +structural changes, and retained outputs separately using the +[benchmarking guidance](../developers.md). A public parallel or distributed +executor remains [planned work](../planned_features.md). + +## AI-assisted model development + +An AI coding agent is software that can read and edit code and run tests with +your development tools. Small process models, explicit inputs and outputs, +declared units, and reports on model connections give it concrete information +to read and check. An agent can help draft a model, compare declared +interfaces, assemble a scenario, inspect its connections, and run tests with +the tools available in your development environment. + +PlantSimEngine provides a versioned [AI agent skill](../agent_skill.md), a +[loaded model catalog](../API/model_catalog.md), and the same authoring tools +used by people. You supply the coding agent and its execution environment. +Model assumptions, physical conversions, supporting references, and scientific +validation remain the responsibility of the researcher. Begin with +[Implement a basic model](../journeys/modelers/basic_model.md) to see the +complete model, an independent equation test, and its use in a simulation. + +## How this fits among plant-modelling tools + +Modularity and multiscale modelling have a substantial history. +[APSIM](https://docs.apsim.info/docs/development/software/interfaces) supports +replaceable model interfaces; +[DSSAT](https://dssat.net/frontpage/) combines crop models with data and +experimental workflows; +[OpenAlea](https://openalea.readthedocs.io/en/latest/packages/modelling.html) +provides components, multiscale structures, and plant-geometry tools; and +[GroIMP](https://grogra.de/) integrates FSPM modelling and visualisation. +[Cropbox](https://doi.org/10.1093/insilicoplants/diac021) also uses Julia and +dependency analysis for declarative crop modelling. + +PlantSimEngine's contribution is the combination described here: readable +process equations, explicit inputs and outputs, a choice of how to represent +plants, different time steps, controlled iteration, changing structures, and +calculations you can inspect. It is a useful fit when you want to develop or assemble +that scientific model combination yourself, while keeping its choices visible +and its components independently testable. diff --git a/docs/src/journeys/modelers/basic_model.md b/docs/src/journeys/modelers/basic_model.md index 8b9be91a1..27919ac95 100644 --- a/docs/src/journeys/modelers/basic_model.md +++ b/docs/src/journeys/modelers/basic_model.md @@ -1,19 +1,20 @@ # Implement a basic model -**New concept:** the complete one-step model boundary: process identity, -parameters, ports, scientific contracts, and a readable five-argument kernel. -This page uses the tested canonical fixture shipped with the PlantSimEngine -skill, first directly and then through the ordinary runtime. +This teaching example turns a simple biomass-growth equation into a reusable +model. A **process** is a scientific calculation; a model implements one choice +of equations for that process. You will declare the equation's inputs, outputs, +and units, test it on its own, then run it in a simulation. -Before creating a type, follow [New process or new model?](@ref). The example -below is one hypothesis for the `biomass_production` process. Its coefficients -are pedagogical, not a calibrated crop model. +The example is one hypothesis for the `biomass_production` process. Its +coefficients are pedagogical, not a calibrated crop model. Before adding your +own model, follow [New process or new model?](@ref) to decide whether it answers +an existing scientific question or introduces a new process. ## Read the complete model This is the complete, copyable source shipped with the package. The model -declaration comes first; `direct_example` tests its kernel and -`single_object_scenario` shows the smallest runtime composition. +declaration comes first; `direct_example` tests the equation on its own and +`single_object_scenario` runs it on one simulated entity, called an **object**. ```@eval using Markdown, PlantSimEngine diff --git a/docs/src/journeys/users/one_object.md b/docs/src/journeys/users/one_object.md index 350cee5e5..e94b20761 100644 --- a/docs/src/journeys/users/one_object.md +++ b/docs/src/journeys/users/one_object.md @@ -2,21 +2,29 @@ ## New concept: automatic same-object coupling over time -This first executable simulation couples three existing models on one object: +This teaching example couples three existing models on one simulated entity, +called an **object**. Here, that object represents a canopy without describing +individual organs. A **process** is a scientific calculation, such as thermal +time or light interception; a model implements its equations. The toy models +below demonstrate coupling and are not a calibrated crop model: 1. `ToyDegreeDaysCumulModel` reads temperature and accumulates thermal time. 2. `ToyLAIModel` reads cumulative thermal time and computes LAI. 3. `Beer` reads LAI and radiation and computes absorbed PAR. The weather file is supplied forcing data for now. Environments get their own -journey later. +journey later. Its radiation columns contain daily totals in MJ m⁻² d⁻¹; +we convert them to mean fluxes in W m⁻², as required by `Beer`. ```@example journey_one_object using PlantSimEngine, PlantMeteo, Dates, DataFrames using PlantSimEngine.Examples weather = read_weather( - joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"); + joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), + :Ri_SW_f => (x -> x .* 1e6 ./ 86_400) => :Ri_SW_f, + :Ri_PAR_f => (x -> x .* 1e6 ./ 86_400) => :Ri_PAR_f, + :Ri_NIR_f => (x -> x .* 1e6 ./ 86_400) => :Ri_NIR_f; duration=Day, ) From fd5039c9af0ed6c1be33d6879ea99804be0e3b0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Vezy?= Date: Sun, 6 Sep 2026 13:44:58 +0200 Subject: [PATCH 4/6] docs: simplify modeling guides and remove obsolete work notes Organize the Bonito manual around coupling existing models and writing new ones, with runnable examples, explicit scientific assumptions, and practical guidance for AI coding agents. Consolidate duplicate introductions and downstream testing guidance, and remove completed design plans, audits, and handoffs rather than preserving a separate archive. Correct the teaching examples' radiation units and cumulative carbon accounting, preserve generated code/table rendering, and improve narrow-screen navigation. Keep only source files needed to build and validate the manual. --- README.md | 30 +- docs/bonito_rendering.jl | 21 + docs/make.jl | 90 +- docs/model_source.jl | 23 + docs/src/API/API_public.md | 6 +- docs/src/API/model_catalog.md | 195 ++-- docs/src/agent_skill.md | 150 +-- docs/src/assets/brand.css | 15 +- docs/src/composite_model/quickstart.md | 261 ----- docs/src/dev/code_cleanup_audit.md | 97 -- .../dev/composite_model_completion_audit.md | 96 -- docs/src/dev/composite_model_design.md | 758 ------------ .../composite_model_implementation_plan.md | 1036 ----------------- docs/src/dev/distributed_output_ownership.md | 330 ------ docs/src/dev/maespa_model_handoff.md | 161 --- .../public_api_refinement_completion_audit.md | 43 - .../dev/public_api_refinement_decisions.md | 105 -- docs/src/dev/release_notes_handoff.md | 521 --------- docs/src/developers.md | 21 + docs/src/documentation_improvement.md | 11 - docs/src/guides/data/environment_inputs.md | 10 - docs/src/guides/data/forcing_observations.md | 86 +- docs/src/guides/data/observed_lai.jl | 11 + docs/src/guides/data/outputs_plotting.md | 184 ++- .../guides/modelers/port_existing_model.md | 187 ++- .../guides/modelers/repository_and_tests.md | 166 ++- docs/src/guides/modelers/stateful_models.md | 154 ++- docs/src/guides/multiscale/from_one_object.md | 10 - docs/src/guides/multiscale/import_mtg.md | 82 +- .../multiscale/visualizing_structure.md | 66 +- .../guides/time/advanced_time_environment.md | 89 +- docs/src/guides/time/hourly_daily_weekly.md | 150 ++- docs/src/guides/time/multirate_concepts.md | 13 - docs/src/guides/time/teaching_models.jl | 48 + docs/src/index.md | 8 +- docs/src/journeys/modelers/basic_model.md | 245 ++-- .../journeys/modelers/cross_object_values.md | 187 ++- .../modelers/environment_and_cadence.md | 171 ++- .../journeys/modelers/hard_dependencies.md | 175 ++- .../journeys/modelers/mutable_environment.md | 146 ++- docs/src/journeys/users/cadences.md | 102 +- docs/src/journeys/users/environments.md | 89 +- docs/src/journeys/users/maespa_synthesis.md | 68 +- docs/src/journeys/users/one_object.md | 38 +- docs/src/journeys/users/one_plant.md | 29 +- docs/src/journeys/users/several_objects.md | 79 +- docs/src/journeys/users/several_plants.md | 23 +- .../installing_plantsimengine.md | 41 +- docs/src/prerequisites/key_concepts.md | 108 -- .../step_by_step/detailed_first_example.md | 225 ---- docs/src/step_by_step/implement_a_model.md | 31 - .../implement_a_model_additional.md | 113 -- docs/src/step_by_step/implement_a_process.md | 128 +- docs/src/step_by_step/model_switching.md | 253 ++-- .../step_by_step/quick_and_dirty_examples.md | 121 -- .../src/step_by_step/simple_model_coupling.md | 97 -- docs/src/troubleshooting/common_errors.md | 64 +- docs/src/troubleshooting/runtime_contracts.md | 69 +- .../downstream_tests.md | 9 - .../tutorials/growing_plant/part1_growth.md | 79 +- .../growing_plant/part2_roots_water.md | 110 +- .../growing_plant/part3_debugging.md | 75 +- docs/test/runtests.jl | 52 +- examples/ToyLightPartitioningModel.jl | 18 +- examples/maespa_model_example.jl | 54 +- src/composite_model/registry_topology.jl | 19 + test/test-maespa-model-example.jl | 69 +- 67 files changed, 2503 insertions(+), 5818 deletions(-) create mode 100644 docs/bonito_rendering.jl create mode 100644 docs/model_source.jl delete mode 100644 docs/src/composite_model/quickstart.md delete mode 100644 docs/src/dev/code_cleanup_audit.md delete mode 100644 docs/src/dev/composite_model_completion_audit.md delete mode 100644 docs/src/dev/composite_model_design.md delete mode 100644 docs/src/dev/composite_model_implementation_plan.md delete mode 100644 docs/src/dev/distributed_output_ownership.md delete mode 100644 docs/src/dev/maespa_model_handoff.md delete mode 100644 docs/src/dev/public_api_refinement_completion_audit.md delete mode 100644 docs/src/dev/public_api_refinement_decisions.md delete mode 100644 docs/src/dev/release_notes_handoff.md delete mode 100644 docs/src/documentation_improvement.md delete mode 100644 docs/src/guides/data/environment_inputs.md create mode 100644 docs/src/guides/data/observed_lai.jl delete mode 100644 docs/src/guides/multiscale/from_one_object.md delete mode 100644 docs/src/guides/time/multirate_concepts.md create mode 100644 docs/src/guides/time/teaching_models.jl delete mode 100644 docs/src/prerequisites/key_concepts.md delete mode 100644 docs/src/step_by_step/detailed_first_example.md delete mode 100644 docs/src/step_by_step/implement_a_model.md delete mode 100644 docs/src/step_by_step/implement_a_model_additional.md delete mode 100644 docs/src/step_by_step/quick_and_dirty_examples.md delete mode 100644 docs/src/step_by_step/simple_model_coupling.md delete mode 100644 docs/src/troubleshooting_and_testing/downstream_tests.md diff --git a/README.md b/README.md index bb2ef8eea..0a2d5cc39 100644 --- a/README.md +++ b/README.md @@ -42,18 +42,24 @@ model-scale simulations. ## Installation -In Julia package mode: - -```julia -add PlantSimEngine -``` - -Then: +The examples on this development branch use `CompositeModel`. Registered +releases through 0.14.1 use the previous mapping API; use their matching +[stable documentation](https://VirtualPlantLab.github.io/PlantSimEngine.jl/stable). +To run the examples below, install the development version in a project: ```julia +using Pkg +Pkg.activate("my_simulation") +Pkg.add(["PlantMeteo", "DataFrames"]) +Pkg.add(url="https://github.com/VirtualPlantLab/PlantSimEngine.jl", rev="main") using PlantSimEngine ``` +When following a pull-request preview, use its branch or commit instead of +`"main"`. Keep `Project.toml` and `Manifest.toml` with your experiment to record +the package revisions. The [installation guide](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/prerequisites/installing_plantsimengine.html) +also installs the plotting tools used by the tutorials. + ## Quickstart This example runs three existing toy models on one model object: @@ -215,11 +221,11 @@ Diagnostics.explain_execution_plan(model) - [Stable documentation](https://VirtualPlantLab.github.io/PlantSimEngine.jl/stable) - [Development documentation](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev) -- [CompositeModel/object quickstart](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/composite_model/quickstart/) -- [Implement a model](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/journeys/modelers/basic_model/) -- [AI agent skill](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/agent_skill/) -- [CompositeModel/object migration guide](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/migration_composite_model/) -- [Public API reference](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/API/API_public/) +- [Run and couple models](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/journeys/users/one_object.html) +- [Implement a model](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/journeys/modelers/basic_model.html) +- [AI agent skill](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/agent_skill.html) +- [CompositeModel/object migration guide](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/migration_composite_model.html) +- [Public API reference](https://VirtualPlantLab.github.io/PlantSimEngine.jl/dev/API/API_public.html) ## Projects That Use PlantSimEngine diff --git a/docs/bonito_rendering.jl b/docs/bonito_rendering.jl new file mode 100644 index 000000000..8175167c5 --- /dev/null +++ b/docs/bonito_rendering.jl @@ -0,0 +1,21 @@ +module BonitoRendering + +using Bonito, Documenter + +const Writer = Base.get_extension(Bonito, :BonitoDocumenterExt) +isnothing(Writer) && error("Bonito's Documenter extension must be loaded") + +# Documenter wraps @eval Markdown results in a MarkdownAST.Document node. +# Bonito 5.2 falls back to plain text for that root, dropping code blocks, +# tables and their formatting. Dispatch its children through the normal writer. +# Leave an upstream implementation in place when Bonito adds this method. +const document_method = which( + Writer.domify, (Writer.DCtx, Writer.MA.Node, Writer.MA.Document), +) +if last(Base.unwrap_unionall(document_method.sig).parameters) === Any + function Writer.domify(ctx::Writer.DCtx, node::Writer.MA.Node, ::Writer.MA.Document) + return Writer.domify_children(ctx, node) + end +end + +end diff --git a/docs/make.jl b/docs/make.jl index 79b2557e0..9b8a40de5 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -6,6 +6,9 @@ using Bonito using CairoMakie using PlantSimEngine.Examples +include(joinpath(@__DIR__, "model_source.jl")) +include(joinpath(@__DIR__, "bonito_rendering.jl")) + function build_model_graph_example() output_dir = joinpath(@__DIR__, "src", "assets") mkpath(output_dir) @@ -74,84 +77,75 @@ makedocs(; "Home" => "index.md", "Start here" => [ "Why PlantSimEngine?" => "introduction/why_plantsimengine.md", - "Mental model" => "journeys/users/mental_model.md", - "One object over time" => "journeys/users/one_object.md", - "Several same-scale objects" => "journeys/users/several_objects.md", + "Installation" => "prerequisites/installing_plantsimengine.md", + "Your first simulation" => "journeys/users/one_object.md", + "How the pieces fit" => "journeys/users/mental_model.md", + "Help from an AI coding agent" => "agent_skill.md", ], - "Structure and composition" => [ + "Couple models" => [ + "Several independent objects" => "journeys/users/several_objects.md", "One multiscale plant" => "journeys/users/one_plant.md", "Several plants" => "journeys/users/several_plants.md", - "Value coupling" => "guides/multiscale/value_coupling.md", + "Connect values between objects" => "guides/multiscale/value_coupling.md", + "Collect and plot results" => "guides/data/outputs_plotting.md", + "Use observed values" => "guides/data/forcing_observations.md", "Importing an MTG" => "guides/multiscale/import_mtg.md", - "How composite models execute" => "guides/multiscale/concepts.md", "Visualizing structure" => "guides/multiscale/visualizing_structure.md", ], + "Write models" => [ + "New process or new hypothesis?" => "step_by_step/implement_a_process.md", + "Write and test a first model" => "journeys/modelers/basic_model.md", + "Keep scientific kernels readable" => "guides/modelers/port_existing_model.md", + "Repository layout and test pyramid" => "guides/modelers/repository_and_tests.md", + "Cross-object values" => "journeys/modelers/cross_object_values.md", + "Choose a coupling mechanism" => "guides/coupling.md", + "Model compatibility and replacement" => "step_by_step/model_switching.md", + "Environment and cadence traits" => "journeys/modelers/environment_and_cadence.md", + "Hard dependencies" => "journeys/modelers/hard_dependencies.md", + "Mutable environment controllers" => "journeys/modelers/mutable_environment.md", + "Stateful models" => "guides/modelers/stateful_models.md", + ], "Environment and time" => [ "Read an environment" => "journeys/users/environments.md", "Different model cadences" => "journeys/users/cadences.md", "Hourly, daily, and weekly" => "guides/time/hourly_daily_weekly.md", - "Advanced configuration" => "guides/time/advanced_time_environment.md", + "Choose compatible time steps" => "guides/time/advanced_time_environment.md", ], - "Dynamic and advanced simulations" => [ + "Growth and advanced simulations" => [ "Modify plant structure" => "journeys/users/structure_changes.md", + "Growth within a time step" => "tutorials/growing_plant/part1_growth.md", + "Roots and water" => "tutorials/growing_plant/part2_roots_water.md", + "Check a growing simulation" => "tutorials/growing_plant/part3_debugging.md", "Modify the environment" => "journeys/users/mutable_environments.md", "Control advanced execution" => "journeys/users/advanced_execution.md", "MAESPA-style synthesis" => "journeys/users/maespa_synthesis.md", - "Part 2: roots and water" => "tutorials/growing_plant/part2_roots_water.md", - "Part 3: debugging" => "tutorials/growing_plant/part3_debugging.md", "Manual calls" => "guides/multiscale/manual_calls.md", "Advanced coupling and hard dependencies" => "step_by_step/advanced_coupling.md", ], - "Implement models" => [ - "New process or new hypothesis?" => "step_by_step/implement_a_process.md", - "Basic contract and reuse" => "journeys/modelers/basic_model.md", - "Keep scientific kernels readable" => "guides/modelers/port_existing_model.md", - "Repository layout and test pyramid" => "guides/modelers/repository_and_tests.md", - "Cross-object values" => "journeys/modelers/cross_object_values.md", - "Choose a coupling mechanism" => "guides/coupling.md", - "Model compatibility and replacement" => "step_by_step/model_switching.md", - "Environment and cadence traits" => "journeys/modelers/environment_and_cadence.md", - "Hard dependencies" => "journeys/modelers/hard_dependencies.md", - "Mutable environment controllers" => "journeys/modelers/mutable_environment.md", - "Stateful models" => "guides/modelers/stateful_models.md", + "Check and troubleshoot" => [ + "Common errors" => "troubleshooting/common_errors.md", + "Inspect a simulation" => "troubleshooting/runtime_contracts.md", + "Dependency cycles" => "troubleshooting/dependency_cycles.md", + "Numerical reliability" => "guides/data/numerical_reliability.md", + "Parameter fitting" => "working_with_data/fitting.md", ], - "Reference" => [ - "Installing PlantSimEngine" => "prerequisites/installing_plantsimengine.md", + "Reference and tools" => [ "Julia language basics" => "prerequisites/julia_basics.md", - "Why Julia ?" => "introduction/why_julia.md", + "Why Julia?" => "introduction/why_julia.md", + "How multiscale models execute" => "guides/multiscale/concepts.md", "Model execution" => "model_execution.md", "Model traits" => "model_traits.md", - "Collecting and plotting outputs" => "guides/data/outputs_plotting.md", - "Forcing observations" => "guides/data/forcing_observations.md", - "Numerical reliability" => "guides/data/numerical_reliability.md", - "Parameter fitting" => "working_with_data/fitting.md", "Graph editor" => "guides/graph_visualizer_editor.md", - "Common errors" => "troubleshooting/common_errors.md", - "Runtime contracts" => "troubleshooting/runtime_contracts.md", - "Dependency cycles" => "troubleshooting/dependency_cycles.md", - "Downstream testing" => "troubleshooting_and_testing/downstream_tests.md", "Environment backend extensions" => "guides/extensions/environment_backends.md", - "AI agent skill" => "agent_skill.md", - "Loaded model catalog" => "API/model_catalog.md", + "Find available models" => "API/model_catalog.md", "Public API" => "API/API_public.md", "Public symbol inventory" => "API/public_symbols.md", "Example models" => "API/API_examples.md", ], - "Migration" => [ - "From the mapping runtime" => "migration_composite_model.md", - ], - "Maintainers" => [ + "Contribute and migrate" => [ "Developer guidelines" => "developers.md", "Internal API" => "API/API_private.md", - "Public API refinement decisions" => "dev/public_api_refinement_decisions.md", - "Public API refinement completion audit" => "dev/public_api_refinement_completion_audit.md", - "Composite model/object design" => "dev/composite_model_design.md", - "Distributed output ownership" => "dev/distributed_output_ownership.md", - "Composite model/object implementation plan" => "dev/composite_model_implementation_plan.md", - "Composite model/object completion audit" => "dev/composite_model_completion_audit.md", - "MAESPA-style composite-model example handoff" => "dev/maespa_model_handoff.md", - "Code cleanup audit" => "dev/code_cleanup_audit.md", - "Release notes handoff" => "dev/release_notes_handoff.md", + "From the mapping runtime" => "migration_composite_model.md", "Roadmap" => "planned_features.md", ], ] diff --git a/docs/model_source.jl b/docs/model_source.jl new file mode 100644 index 000000000..c9835736c --- /dev/null +++ b/docs/model_source.jl @@ -0,0 +1,23 @@ +module DocsSources + +using Markdown +using PlantSimEngine + +"""Render an exact source section, failing if its start or end marker moved.""" +function section(relative_path, from, to=nothing) + path = joinpath(pkgdir(PlantSimEngine), relative_path) + source = read(path, String) + starts = findall(from, source) + length(starts) == 1 || error("Expected one start marker in $relative_path: $from") + first_index = first(only(starts)) + last_index = lastindex(source) + if !isnothing(to) + stop = findnext(to, source, last(only(starts)) + 1) + isnothing(stop) && error("Missing end marker in $relative_path: $to") + last_index = prevind(source, first(stop)) + end + code = strip(source[first_index:last_index]) + return Markdown.MD([Markdown.Code("julia", code)]) +end + +end diff --git a/docs/src/API/API_public.md b/docs/src/API/API_public.md index 69afc094b..87980685f 100644 --- a/docs/src/API/API_public.md +++ b/docs/src/API/API_public.md @@ -167,8 +167,10 @@ targets. - `register_object!`, `remove_object!`, and `reparent_object!` change topology. - `move_object!` and `update_geometry!` change spatial state. -- Supported lifecycle operations automatically invalidate and refresh the - affected structural or spatial bindings before the next timestep. +- Supported lifecycle operations invalidate affected bindings. Structural + connections refresh after the application that changed topology, so new + objects can run applications still remaining in the same time step. + Changes made between steps are processed before the next step. - A creator that must run an application which already completed on existing objects declares an `Initializer` call. The compiler orders the scheduled target before the creator and the creator before direct non-temporal diff --git a/docs/src/API/model_catalog.md b/docs/src/API/model_catalog.md index 254aad5c7..a578fba81 100644 --- a/docs/src/API/model_catalog.md +++ b/docs/src/API/model_catalog.md @@ -1,127 +1,106 @@ # Loaded model catalog -This catalog is generated from the Julia modules loaded by the documentation -build. It is not a manually maintained registry: downstream packages appear -after they are loaded, and a type without a zero-argument constructor remains -visible as an incomplete, best-effort description. +Start with the scientific process you need, then choose a model and inspect +its requirements. PlantSimEngine can list models from packages already loaded +in your Julia session. It does not search all installed or available packages. -```@example loaded-model-catalog -using DataFrames -using PlantSimEngine +## Choose where to start -rows = NamedTuple[] -descriptions = Authoring.ModelDescription[] -for process_type in Authoring.available_processes() - model_types = Authoring.available_models(process_type) - if isempty(model_types) - push!(rows, ( - process=missing, - process_type=string(process_type), - model="", - package=missing, - provenance=:unavailable, - complete=false, - parameter_provenance=:unavailable, - diagnostics="no concrete model loaded", - )) - continue - end +| You want to… | Start here | +|---|---| +| Learn the interface with small examples | `PlantSimEngine.Examples` and [one-object simulation](../journeys/users/one_object.md) | +| Work on leaf gas exchange or energy balance | [PlantBiophysics.jl](https://github.com/VEZY/PlantBiophysics.jl) and its model documentation | +| Write a new equation | [New process or new model?](@ref) | +| Compare two implementations | [Model compatibility and replacement](@ref) | - for model_type in model_types - description = Authoring.describe_model(model_type) - push!(descriptions, description) - parameter_field = description.field_provenance.parameters - push!(rows, ( - process=description.process, - process_type=something(description.process_type, ""), - model=description.model_type, - package=something(description.package, ""), - provenance=description.provenance, - complete=description.complete, - parameter_provenance=parameter_field isa NamedTuple ? - parameter_field.values : parameter_field, - diagnostics=join( - string.(getfield.(description.diagnostics, :code)), - ", ", - ), - )) - end -end +The `Toy...` models in these tutorials are teaching examples. Their presence +in a catalog does not establish scientific validation. -catalog = DataFrame(rows) -sort!(catalog, [:process_type, :model]) -catalog +## List a process's models + +After loading a package, list its process types with +`Authoring.available_processes()`. For a known process, request its models: + +```@example loaded-model-catalog +using PlantSimEngine, DataFrames +using PlantSimEngine.Examples + +Authoring.available_models(AbstractGrowthModel) ``` -`provenance=:best_effort` means the row was requested from a type. When -`complete=false`, inspect `diagnostics` and pass the concrete instance used by -the scenario to `Authoring.describe_model(instance)` before making decisions -about its parameters or interface. The nested `parameter_provenance` column -also distinguishes exact values from declared metadata and unavailable data. +In your own session, load the relevant scientific package first. The table on +this website reflects the documentation build's session only. -## Port and scientific-contract catalog +## Inspect a model you might use -The long table below is generated from each `ModelDescription.ports` entry. -Missing values remain missing: an incomplete type description produces an -explicit `:unavailable` row rather than guessed ports or contracts. +Construct a real model with explicit parameters. This avoids guessing the +defaults of a type that requires arguments. ```@example loaded-model-catalog -port_rows = NamedTuple[] -for description in descriptions - port_field = description.field_provenance.ports - declaration_provenance = port_field isa NamedTuple ? - port_field.declarations : port_field - contract_provenance = port_field isa NamedTuple ? - port_field.contracts : port_field - - if isempty(description.ports) - availability = description.complete ? :none : :unavailable - push!(port_rows, ( - process=description.process, - model=description.model_type, - complete=description.complete, - role=availability, - port=availability, - declaration=availability, - expected_type=string(availability), - unit=missing, - basis=missing, - temporal=missing, - aggregation=missing, - extent=missing, - declaration_provenance=declaration_provenance, - contract_provenance=contract_provenance, - )) - continue - end +candidate = ToyRUEGrowthModel(0.2) +description = Authoring.describe_model(candidate) + +( + model=description.model_type, + process=description.process, + parameters=description.parameters, + inputs=inputs(candidate), + outputs=outputs(candidate), +) +``` + +Read the model's equations, assumptions, units, parameter meanings, domain of +validity, and validation evidence in its package documentation. The report can +expose declared metadata, but cannot supply missing scientific evidence. + +`variable_contracts(candidate)` returns the declared physical meanings of its +variables. When a candidate has no contracts, that information remains +missing. Before using a model in a contracted connection, its author must +provide the matching declarations or an explicit physical adapter. + +## Check whether a candidate fits your simulation + +1. Confirm that its inputs can come from your data, environment, or other models. +2. Compare units, basis, timing, and aggregation at every connection. +3. Use `Authoring.compare_models(current, candidate)` when replacing a model. +4. Validate the assembled scenario with `Authoring.validate_scenario`, then + check a small run against an expected result. + +See [Coupling models](@ref) for connection choices and +[Model compatibility and replacement](@ref) for a complete comparison. + +## Reference: models visible during this build - for port in description.ports - contract = port.variable_contract - push!(port_rows, ( - process=description.process, - model=description.model_type, - complete=description.complete, - role=port.role, - port=port.name, - declaration=port.declaration, - expected_type=port.expected_type, - unit=isnothing(contract) ? missing : contract.unit, - basis=isnothing(contract) ? missing : contract.basis, - temporal=isnothing(contract) ? missing : contract.temporal, - aggregation=isnothing(contract) ? missing : contract.aggregation, - extent=isnothing(contract) ? missing : contract.extent, - declaration_provenance=declaration_provenance, - contract_provenance=contract_provenance, +The following table is generated from the loaded modules. `complete=false` +means a type could not provide a complete description, for example because +it needs constructor arguments. Inspect a concrete instance before making +a choice. `provenance` distinguishes declared information from best-effort +inspection; detailed reports also record provenance field by field. + +```@setup loaded-model-catalog +rows = NamedTuple[] +for process_type in Authoring.available_processes() + for model_type in Authoring.available_models(process_type) + model_description = Authoring.describe_model(model_type) + push!(rows, ( + process=model_description.process, + model=model_description.model_type, + package=something(model_description.package, ""), + complete=model_description.complete, + provenance=model_description.provenance, )) end end +catalog = DataFrame(rows) +sort!(catalog, [:package, :model]) +``` -port_catalog = DataFrame(port_rows) -sort!(port_catalog, [:process, :model, :role, :port]) -port_catalog +```@example loaded-model-catalog +catalog ``` -The same pattern can generate a package-specific process page after loading -that package. Filter the table by `package`, then enrich the human-facing page -with assumptions, equations, references, domain of validity, and scientific -validation evidence; Authoring deliberately does not infer those claims. +To reproduce this discovery yourself, loop over +`Authoring.available_processes()` and call +`Authoring.available_models(process_type)` for each process. Pass a concrete +instance to `Authoring.describe_model` for its parameters, variables, +contracts, metadata, and diagnostic messages. diff --git a/docs/src/agent_skill.md b/docs/src/agent_skill.md index de4c905de..dfa028350 100644 --- a/docs/src/agent_skill.md +++ b/docs/src/agent_skill.md @@ -1,29 +1,17 @@ # AI agent skill -An **AI coding agent** is software that can read and edit code and run tests -with the tools you make available. PlantSimEngine provides an optional skill -to guide agents that help you implement process models or assemble simulations. +An **AI coding agent** can read and edit code and run tests with the tools you +make available. PlantSimEngine provides an optional skill with instructions +and executable examples for implementing process models and assembling +simulations. -Models declare the values they need and produce, and their equations can be -tested independently. Structured diagnostics explain model connections, -execution order, and missing inputs. Together, these give an agent concrete -information to inspect when proposing and checking changes. +You choose the scientific question, assumptions, references, and validation +evidence. The agent can help turn those choices into code and check that the +declared models connect as intended. -The skill adds workflows and executable examples matched to your package -version. You guide the scientific assumptions and validate the equations, -parameters, and results against the evidence for your study. +## Set up the skill for your package version -The skill uses the Julia tools available in the agent's environment. It does -not require a particular editor or connector; repository and user execution -requirements still apply. - -The skill file is stored in the repository at: - -```text -skills/plantsimengine/SKILL.md -``` - -Use the copy shipped with the package version that Julia actually loaded: +Find the skill shipped with the PlantSimEngine version you actually use: ```julia using PlantSimEngine @@ -33,60 +21,78 @@ skill_root = joinpath(package_root, "skills", "plantsimengine") (Base.pathof(PlantSimEngine), Base.pkgversion(PlantSimEngine), skill_root) ``` -Copy or link that complete `skills/plantsimengine` directory into the agent's -skill directory. Do not install a skill from a floating `main` branch for a -tagged or otherwise older package: its instructions may describe another API -generation. Updating the installed copy is an explicit user action. - -The skill gives agents the package-specific conventions they need for: - -- building object graphs with `CompositeModel`, `Object`, `CompositeModelTemplate`, and - `ObjectInstance`; -- applying models with `ModelSpec` and `on`; -- coupling values and manual model calls with `inputs` and `calls`; -- using cached hard-call plans through bulk `run_call!`, singular `call_model`, - and lifecycle-maintained object target buffers; -- configuring multirate simulations with `every`, `Dates.Period` values, - and temporal policies; -- binding global or spatial microclimate through `Environment`; -- reasoning about model-clock weather aggregation, `environment_hint` reducers, and - scenario source overrides; -- inspecting compiled scenarios with structured explanation helpers; -- reporting supplied, generated, bound, and unresolved variables with - `Diagnostics.explain_initialization`; -- accessing the live model from lifecycle-capable kernels with - `runtime_model(context)`; -- inspecting homogeneous runtime batches with `Diagnostics.explain_execution_plan`; -- collecting raw or requested model outputs with `outputs`, - `OutputRequest`, `collect_outputs`, and `Diagnostics.explain_output_retention`; -- implementing or wrapping models with `@process`, `inputs_`, `outputs_`, - `variable_contracts_`, `run!`, hard dependencies, and model traits; -- discovering, describing, comparing, and validating concrete models with - `Authoring` before editing a scenario; -- validating incomplete scenarios through versioned, serializable authoring - reports instead of inspecting compiler fields; -- checking `ModelDescription.field_provenance` so exact declarations are not - confused with inferred, best-effort, or unavailable information. - -The canonical source for a given package version is the local -`skills/plantsimengine/` directory below `pkgdir(PlantSimEngine)`. Its main -entry point is `SKILL.md`; its relative `references`, `assets`, and `scripts` -directories are part of the same versioned resource. - -The main file is a short router. It loads focused references for model -authoring, scenario coupling, repository organization, dynamic models, or -diagnostics. Copyable fixtures live under `assets/`, including a complete -minimal model, compatible and incompatible alternatives, an explicit physical -adapter, and their tests. Run the canonical local check with: +Copy or link that complete directory into your coding agent's skill directory, +following its installation instructions. Keep its `SKILL.md`, `references`, +`assets`, and `scripts` together. Use the copy from your installed package +rather than a floating `main` branch: different versions can describe +different APIs. + +The skill uses the Julia tools available in your agent's environment. It does +not require a particular editor or connector. + +## Ask it to assemble existing models + +Adapt this request to the packages, data, and scientific question you have: + +> Use the PlantSimEngine skill shipped with my loaded package. I want to +> compare two existing models for the same process. First identify available +> alternatives and explain their inputs, outputs, units, and assumptions. +> Show what data each needs and flag anything that is missing. Build the +> smallest simulation that can compare them with the same forcing. Explain +> their connections, run the tests, and show the results. Keep any required +> physical conversion explicit. + +Useful inputs to provide are the package names, the process of interest, +your forcing data and its units, and the outputs you want to compare. The +[Loaded model catalog](@ref) and [Model compatibility and replacement](@ref) +show the corresponding manual workflow. + +## Ask it to implement an equation + +Provide the equation or source paper, definitions of the variables, +parameter values and units, and at least one expected result: + +> Use the PlantSimEngine skill shipped with my loaded package to implement +> the equation and variable definitions I provide. Check whether an existing +> process already represents this question. Keep the equation readable and +> declare its inputs, outputs, and complete scientific contracts. Test one +> calculation against my reference result, then compose it on one object. +> If any assumption, physical conversion, parameter, or validation criterion +> is unspecified, identify it before choosing one. Show the code, test +> results, and remaining scientific questions. + +The [basic model tutorial](journeys/modelers/basic_model.md) follows this +sequence without an agent. For an existing implementation, also use +[Port an existing model](@ref). + +## Review what the agent returns + +Expect a concrete result you can inspect: + +- the loaded package path and version; +- the selected process and hypotheses, with the evidence supplied for them; +- readable model code and scenario configuration; +- checks of required inputs, physical contracts, and resolved connections; +- tests actually run, their results, and any remaining limitations. + +PlantSimEngine's `Authoring` reports describe models and compare interfaces. +`Diagnostics` explains inputs, connections, timing, and execution. Reports +mark missing or inferred information so it can be distinguished from declared +facts. These checks help find implementation and coupling errors; scientific +validation still needs suitable observations or reference results. + +## Check the packaged examples + +The skill includes a minimal model, alternative hypotheses, a physical +adapter, coupling examples, and their tests. You or your agent can check +the copy supplied by the installed package with: ```julia include(joinpath( - pkgdir(PlantSimEngine), - "skills", - "plantsimengine", - "scripts", - "check-examples.jl", + pkgdir(PlantSimEngine), "skills", "plantsimengine", + "scripts", "check-examples.jl", )) ``` -Agents should still inspect the local package code before making changes. The skill is a usage and modeling guide, not a replacement for the current API definitions in `src/`. +Use [Model repository layout and tests](@ref) when you are ready to organize +your own reusable model package. diff --git a/docs/src/assets/brand.css b/docs/src/assets/brand.css index 5bebd65ef..57425a534 100644 --- a/docs/src/assets/brand.css +++ b/docs/src/assets/brand.css @@ -1,4 +1,4 @@ -/* PlantSimEngine's botanical palette; the Bonito layout stays unchanged. */ +/* PlantSimEngine's botanical palette and documentation layout adjustments. */ :root { --vp-c-brand-1: #156f64; --vp-c-brand-2: #197b6f; @@ -13,6 +13,19 @@ html.dark { --vp-c-brand-soft: rgba(131, 207, 167, 0.14); } +/* Long API names must wrap in prose without widening the page. */ +.vp-doc :not(pre) > code { + overflow-wrap: anywhere; + white-space: normal; +} + +/* Keep search, version, theme and the navigation menu reachable on phones. */ +@media (max-width: 480px) { + .VPNav .title > span { + display: none; + } +} + .VPHero .name { color: var(--vp-c-brand-1); background: none; diff --git a/docs/src/composite_model/quickstart.md b/docs/src/composite_model/quickstart.md deleted file mode 100644 index 988ae41cc..000000000 --- a/docs/src/composite_model/quickstart.md +++ /dev/null @@ -1,261 +0,0 @@ -# CompositeModel/Object Quickstart - -This page is the shortest path to a native composite-model/object simulation. - -Use this API for new multiscale, multi-plant, soil, microclimate, and -model-scale simulations. Applications use one direct constructor: - -```julia -ModelSpec( - model; - name=:application, - on=Many(scale=:Leaf), - inputs=(...), - calls=(...), - every=Dates.Hour(1), - environment=Environment(...), - output_routing=(...), - updates=Updates(...), -) -``` - -Scenarios are defined with `CompositeModel` and model applications. A model is a -reusable process implementation; an application is one configured use of that -model, including its name, target objects, inputs, cadence, and environment. -One application may run on many objects, and the same model may appear in -several applications with different parameters or targets. - -```@setup model_object_quickstart -using PlantSimEngine, PlantMeteo, Dates, DataFrames -using PlantSimEngine.Examples -``` - -## One Object, Several Models - -For models that all run on one object, the concise constructor lowers directly -to the ordinary CompositeModel/Object representation: - -```julia -model = CompositeModel(ModelA(), ModelB(); status=(initial_value=1.0,)) -``` - -Use the explicit form later in this guide when applications need names, -selectors, or other scenario policies. - -The first model has one object, `:scene`, and three model applications: - -- `ToyDegreeDaysCumulModel` computes daily thermal time; -- `ToyLAIModel` consumes cumulative thermal time and computes LAI; -- `Beer` consumes LAI and meteorology to compute absorbed PAR. - -The model implementations are ordinary PlantSimEngine kernels. The model -application layer decides where they run. With no explicit `every`, these -applications use the environment cadence. - -```@example model_object_quickstart -meteo_day = read_weather( - joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"); - duration=Dates.Day, -) - -model = CompositeModel( - ToyDegreeDaysCumulModel(), - ToyLAIModel(), - Beer(0.6); - environment=meteo_day, -) - -sim = run!(model; steps=30, outputs=:all) -out = collect_outputs(sim; sink=DataFrame) -first(out, 6) -``` - -The `Simulation` provides a snapshot of the latest status values: - -```@example model_object_quickstart -scene_status = final_state(sim) -(TT_cu=scene_status.TT_cu, LAI=scene_status.LAI, aPPFD=scene_status.aPPFD) -``` - -## Inspect The Compiled Bindings - -Before running, `Diagnostics.explain_initialization(model)` classifies each variable as -`:required`, `:defaulted`, `:supplied`, `:producer_bound`, `:generated`, or -`:environment_bound`. The report remains available when required values are -missing, so it can be used to finish configuring a model. - -The compiler infers unambiguous same-object dependencies from declared model -inputs and outputs: - -- `:LAI_Dynamic` reads `TT_cu` from `:Degreedays`; -- `:light_interception` reads `LAI` from `:LAI_Dynamic`. - -```@example model_object_quickstart -select( - DataFrame(Diagnostics.explain_bindings(model)), - :application_id, - :input, - :source_application_ids, - :carrier_kind, - :copy_semantics, -) -``` - -`carrier_kind = :ref` and `copy_semantics = :live_references` mean the -consumer sees a shared reference rather than a copied value. - -For runtime performance diagnostics, inspect the execution plan: - -```@example model_object_quickstart -select( - DataFrame(Diagnostics.explain_execution_plan(model)), - :application_id, - :object_ids, - :batch_size, - :inner_loop_dispatch, -) -``` - -## Request Outputs - -By default, model runs retain no user output streams. Pass `outputs=:all` to -retain every publisher, or pass `OutputRequest` values to retain and -materialize selected streams plus those required by temporal inputs. - -```@example model_object_quickstart -request = OutputRequest( - Many(scale=:Scene), - :LAI; - name=:lai_every_two_days, - application=:LAI_Dynamic, - policy=HoldLast(), - clock=Day(2), -) - -requested_sim = run!( - model; - steps=30, - outputs=request, -) - -collect_outputs(requested_sim, :lai_every_two_days; sink=nothing)[1:4] -``` - -The retention explanation reports why a stream was kept: - -```@example model_object_quickstart -Diagnostics.explain_output_retention(requested_sim) -``` - -## Many Objects As Inputs - -Use `ModelSpec(...; inputs=...)` when a model needs values from selected objects. This -model-scale LAI model reads live references to the surface of every plant: - -```@example model_object_quickstart -plant_scene = CompositeModel( - Object(:scene; scale=:Scene, kind=:scene), - Object( - :plant_1; - scale=:Plant, - kind=:plant, - parent=:scene, - status=Status(surface=12.0), - ), - Object( - :plant_2; - scale=:Plant, - kind=:plant, - parent=:scene, - status=Status(surface=8.0), - ); - applications=( - ModelSpec(ToyLAIfromLeafAreaModel(100.0); name=:scene_lai, on=One(scale=:Scene), inputs=(:plant_surfaces => Many( - scale=:Plant, - within=SceneScope(), - var=:surface, - ),)), - ), -) - -plant_sim = run!(plant_scene) -plant_model_status = final_state(plant_sim, One(scale=:Scene)) -(total_surface=plant_model_status.total_surface, LAI=plant_model_status.LAI) -``` - -The compiled binding shows a `RefVector` carrier: - -```@example model_object_quickstart -select( - DataFrame(Diagnostics.explain_bindings(plant_scene)), - :application_id, - :input, - :source_ids, - :carrier_kind, - :copy_semantics, -) -``` - -If the consumer model runs on each plant, use `within=Subtree()` to read only -objects inside the current plant. Use `within=SceneScope()` when a model model -must aggregate all matching objects. - -## Manual Calls - -Use `ModelSpec(...; calls=...)` when a parent model must directly run selected child models. -This is the mechanism for iterative solvers such as model energy balance: - -```julia -ModelSpec(SceneEnergyBalance(); name=:scene_energy, on=One(scale=:Scene), calls=(:leaf_energy => Many( - kind=:plant, - scale=:Leaf, - within=SceneScope(), - application=:energy_balance, - ), - :soil => One( - kind=:soil, - scale=:Soil, - within=SceneScope(), - application=:soil_water, - ),), every=Hour(1)) -``` - -For a one-shot call, execute all targets directly: - -```julia -targets = run_call!(context, :leaf_energy; publish=true) -``` - -`targets` is always vector-like regardless of whether the declaration uses -`One`, `OptionalOne`, or `Many`. For iterative control, retrieve the same -compiled targets without executing them: - -```julia -function PlantSimEngine.run!(model::SceneEnergyBalance, status, environment, - constants, context) - trial = trial_environment(model, status) - run_call!(context, :leaf_energy; environment=trial, publish=false) - - accepted = accepted_environment(model, status) - commit_environment!(context, accepted) - run_call!(context, :leaf_energy; publish=true) - - return nothing -end -``` - -`run_call!` defaults to `publish=false`, so trial calls mutate target statuses -without publishing temporal streams or committing mutable environment updates. -Pass non-committing trial state with `environment=trial_state`, then call -`commit_environment!` before publishing the accepted state. - -## Next Steps - -- [Migrating To The CompositeModel/Object API](../migration_composite_model.md) translates - scenarios written with removed APIs. -- [Public API](../API/API_public.md) lists constructors, selectors, lifecycle - helpers, environment helpers, and explanation helpers. -- [Model traits](../model_traits.md) documents `inputs_`, `outputs_`, `dep`, - `timespec`, `output_policy`, and `environment_inputs_`. -- [MAESPA-style model example handoff](../dev/maespa_model_handoff.md) - records the current multi-plant model energy-balance acceptance example. diff --git a/docs/src/dev/code_cleanup_audit.md b/docs/src/dev/code_cleanup_audit.md deleted file mode 100644 index 1e75bafb0..000000000 --- a/docs/src/dev/code_cleanup_audit.md +++ /dev/null @@ -1,97 +0,0 @@ -# Code Cleanup Audit - -## Status - -The compatibility cleanup is implemented on the `multi-plants` branch. - -The package now has one scenario compiler and runtime: the composite-model/object API. -The following superseded implementations were removed rather than deprecated: - -- `ModelList` and `SingleScaleModelSet`; -- `ModelMapping` and `MultiScaleModel`; -- `DependencyGraph`, `HardDependencyNode`, and `SoftDependencyNode`; -- `GraphSimulation` and the MTG mapping runner; -- mapping-specific multirate input resolution and output export; -- the unreleased domain prototype that preceded the composite-model/object API, - including `Domain`, `SimulationMapping`, `Route`, `AllDomains`, - `HardDomains`, and its separate scheduler, runtime, environment bridge, and - output publisher; -- mapping-only initialization, dataframe, dimension, and topology helpers; -- unused parallel-executor traits after removal of the executor runtime; -- dead `UninitializedVar`, `RefVariable`, `TreeAlike`, and `StatusView` types; -- the unreleased `CompositeModelTemplate(...; mapping=...)` alias and legacy - selector-to-mapping conversion helpers; -- compatibility tests, tutorials, and executable examples. - -Migration details are retained in -[`migration_composite_model.md`](../migration_composite_model.md) and -[`release_notes_handoff.md`](release_notes_handoff.md). - -## Current Ownership - -| Concern | Owner | -| --- | --- | -| Object registry, selectors, compilation, execution, lifecycle | `src/composite_model_api.jl` | -| Model application configuration | `src/ModelSpec.jl` | -| Status and reference vectors | `src/component_models/Status.jl`, `src/component_models/RefVector.jl` | -| Dates-based clocks and policies | `src/time/multirate.jl`, `src/time/runtime/clocks.jl` | -| Environment sampling | `src/time/runtime/environment_sampling.jl` | -| Environment backends | `src/time/runtime/environment_backends.jl` | -| Output request definition | `src/time/runtime/output_export.jl` | - -## Remaining Review Rules - -Future cleanup should reject: - -- a second scenario/runtime abstraction parallel to `CompositeModel`; -- compatibility wrappers for unreleased APIs; -- package-specific behavior in PlantSimEngine; -- model kernels that know their scenario object, timestep, or coupling unless - the scientific algorithm requires a hard call; -- dynamic per-object dispatch or copying in hot loops when compiled typed - batches and reference carriers are available; -- undocumented public names or agent instructions that describe removed APIs. - -## Verification - -Current cleanup evidence: - -- the `src` tree contains only the composite-model/object runtime and supporting status, - time, environment, fitting, trait, and example files; -- empty directories left by removed subsystems were deleted; -- `git diff --check` passes; -- PlantSimEngine precompiles and loads from a clean Kaimon session; -- `test/test-unified-model-object-api.jl` passes 576 tests; -- the complete package environment passes 885 tests, including Aqua and - doctests; -- `test/test-fitting.jl` passes; -- the documentation build passes, including executable examples and - cross-reference checks; -- repository search finds no superseded scenario-runtime definitions or - references in source, tests, examples, README, public docs, or the packaged - agent skill; -- remaining `ModelMapping` and `MultiScaleModel` references outside - development notes are migration text that explicitly points historical code - to the composite-model/object API. -- repository search finds no `Domain`, `SimulationMapping`, `Route`, - `AllDomains`, or `HardDomains` implementations, exports, tests, examples, or - public documentation. The only remaining references are development/release - notes that record their removal from this unreleased branch; -- ignored `.DS_Store` files were removed from the working tree outside `.git`. -- `ModelSpec` pipe-helper boilerplate was consolidated behind shared internal - helpers while keeping the public `on`, `inputs`, `calls`, `every`, - `Environment`, `Updates`, and `output_routing` grammar unchanged. -- the duplicate `Advanced.TimeStepTable` export was removed from the PlantMeteo - re-export block; `Advanced.TimeStepTable` remains exported once from the core status - export list. -- stale `PlantSimEngine.Examples` exports for the deleted - `ToyInternodeEmergence` example were removed. - -Downstream verification: - -- PlantBiophysics passes 117/117 tests against this working tree. -- XPalm's uncommitted CompositeModel/Object migration executes 74/75 assertions. The - remaining assertion retains the removed runtime's first-step LAI value, - while the explicit current dependency order produces zero from the initial - zero-biomass leaf before plant/model aggregation. PlantSimEngine intentionally - contains no package-specific compatibility workaround for that stale fixture. diff --git a/docs/src/dev/composite_model_completion_audit.md b/docs/src/dev/composite_model_completion_audit.md deleted file mode 100644 index 828ea4573..000000000 --- a/docs/src/dev/composite_model_completion_audit.md +++ /dev/null @@ -1,96 +0,0 @@ -# Unified CompositeModel/Object Completion Audit - -This audit records the evidence used to assess the breaking composite-model/object -redesign against `composite_model_design.md` and -`composite_model_implementation_plan.md`. - -Audit date: June 12, 2026. - -## Result - -The unified composite-model/object redesign is implemented as the primary public -configuration API. - -The superseded mapping implementation and the unreleased intermediate -prototype were removed after the composite-model/object runtime replaced them. - -## Public Contract - -The exported scenario vocabulary centers on: - -```julia -CompositeModel -Object -ModelSpec -Updates -Environment -``` - -Legacy scenario constructors such as `ModelMapping` and `MultiScaleModel` were -removed. - -Evidence: - -- `src/PlantSimEngine.jl` -- `docs/src/API/API_public.md` -- `docs/src/migration_composite_model.md` -- `README.md` - -## Requirement Evidence - -| Requirement | Evidence | -| --- | --- | -| One model object registry without prescribing plant topology | `CompositeModel`, `Object`, selectors, relations, scopes, and `objects_from_mtg` in `src/composite_model_api.jl`; selector and MTG adapter tests in `test/test-unified-model-object-api.jl` | -| Reusable species models and repeated plant instances | `CompositeModelTemplate`, `ObjectInstance`, `Override`, shared model ownership, and homogeneous/heterogeneous batches; four-instance and override tests | -| Soft/value dependencies | Compiled `ModelSpec(...; inputs=...)` bindings, same-object inference, scalar references, `RefVector`, `Advanced.ObjectRefVector`, temporal carriers, renaming, and optional inputs | -| Manual hard dependencies | Compiled `ModelSpec(...; calls=...)`, vector-like `CallTargets`, `run_call!(context, name)`, and fine-grained `run_call!(target)`; trial calls default to `publish=false` and accepted calls publish explicitly | -| Model-author dependency defaults | `Input(...)` and `Call(...)` entries from `dep(model)`, with `ModelSpec` overrides and binding provenance in explanations | -| Multirate execution | `every=Dates.Period`, model `timespec`, input policies, explicit windows, `PreviousTimeStep`, and stable compiled scheduling | -| Generic value types | Reference and temporal tests using `ModelObjectDualLike{BigFloat}` and `BigFloat` interpolation/integration without `Float64` conversion | -| Reference semantics and low-copy execution | Shared scalar references, typed many-object carriers, preinstalled status bindings, zero-allocation materialization tests, and a zero-allocation warmed 128-object execution batch | -| Duplicate writers | Canonical writer validation and `Updates(:variable; after=...)`, including pruning-after-allocation tests | -| Growth, pruning, reparenting, and movement | Central lifecycle APIs, structural/environment cache invalidation, dynamic target/carrier rebuilding, removed-object output history, and object-scoped geometry refresh tests | -| Automatic meteorology and microclimate | `environment_inputs_`, global and spatial backends, ancestor geometry fallback, source remapping, model hints, tabular aggregation, cached opaque handles, `run_call!(...; environment=trial_state)` trial sampling, and `commit_environment!` accepted mutable commits | -| Structured agent explanations | Object, instance, scope, application, binding, call, model-bundle, environment, schedule, writer, execution-plan, output, and retention explanations returning structured rows | -| Initialization workflow | `Required(T)` and `Default(value)` make the input contract explicit; `Diagnostics.explain_initialization` classifies required, defaulted, supplied, generated, producer-bound, and environment-bound variables before strict compilation | -| Runtime model access | `runtime_model` is the public accessor used by lifecycle-capable kernels and accepts `CompositeModel`, `RunContext`, and `Simulation` | -| One-object ergonomics | `CompositeModel(model, models...; status=...)` lowers to the same object/application compiler and runtime as explicit construction | -| Output ownership and retention | Application-qualified streams, `output_routing`, `OutputRequest(application=...)`, dynamic-object exports, and bounded policy-specific dependency histories | -| MAESPA acceptance case | `build_maespa_scene` and `run_maespa_example` use `CompositeModelTemplate`, `ObjectInstance`, `on`, `inputs`, `calls`, and `every`; verified by `test/test-maespa-model-example.jl` | -| Documentation and migration | CompositeModel/object-first README, home page, quickstart, execution guide, public API, migration guide, and explicitly labeled legacy reference sections | - -## Verification - -The following gates passed from a clean, controllable Kaimon Julia session: - -```text -test/test-unified-model-object-api.jl 576 passed -test/runtests.jl 885 passed -docs/make.jl passed -PlantBiophysics/test/runtests.jl 117 passed -git diff --check passed -``` - -The complete package suite includes Aqua, all focused restoration files, the -broad unified runtime regression, examples, fitting, and doctests. The -documentation build completed its executable examples, doctests, -cross-references, document checks, and HTML rendering successfully. - -The current uncommitted XPalm CompositeModel/Object migration loads this PlantSimEngine -worktree and executes 74 of 75 downstream assertions. Its remaining assertion -expects the removed runtime's first-step LAI (`0.000272`); the current compiled -dependency order correctly runs leaf area before plant and model aggregation, -so the initial zero-biomass leaf produces `0.0`. This is a downstream fixture -expectation in a dirty migration worktree, not a package-specific behavior to -restore in PlantSimEngine. No XPalm workaround was added here. - -## Compatibility Boundary - -Historical mapping source and tests were removed. Migration guidance remains -in the documentation for downstream packages moving to the composite-model/object API. -The branch-only intermediate prototype was also removed because it was never -released and has no compatibility boundary. - -Requested output histories are still materialized after a run. Dependency-only -temporal histories are bounded, but a fully online output sink would be an -additional optimization rather than a missing requirement of this redesign. diff --git a/docs/src/dev/composite_model_design.md b/docs/src/dev/composite_model_design.md deleted file mode 100644 index 8c983c99f..000000000 --- a/docs/src/dev/composite_model_design.md +++ /dev/null @@ -1,758 +0,0 @@ -# Unified CompositeModel/Object Design - -This page records the target breaking design for one composite-model/object -configuration and runtime API. - -The central idea is: - -> Structural groupings and scales are selections over objects in one model. - -The engine should expose one way to say "this model input comes from these -objects" and one way to say "this model must manually call these models". The -compiler can then choose whether the runtime carrier is a `Ref`, `RefVector`, -temporal stream, materialized value, or callable model handle. - -The public API should be simple enough to remember as: - -```julia -ModelSpec( - model; - name=:application, - on=Many(scale=:Leaf), - inputs=(...), - calls=(...), - every=Dates.Hour(1), - environment=Environment(...), - output_routing=(...), - updates=Updates(...), -) -``` - -Everything else should either be a selector, a trait declared by the model -author, or an internal compiled carrier. - -## Core Concepts - -### CompositeModel - -A `CompositeModel` is the whole simulation universe. It contains: - -- simulated objects; -- model applications; -- environment providers; -- time/runtime state; -- caches for object selections and environment bindings. - -Plants, soil, atmosphere, microclimate grids, organs, sensors, and artificial -objects all live in the same model-level object graph. - -### Object - -An object is any simulated entity with identity. It may have: - -- a unique object id; -- one or more labels, such as `scale=:Leaf`, `kind=:plant`, - `species=:oil_palm`; -- parent/child links; -- geometry or position; -- status variables; -- model applications. - -The engine must not prescribe a plant architecture. A plant can be described as -`Plant -> Internode -> Leaf`, `Plant -> Axis -> Segment -> Leaf`, -`Plant -> Metamer -> Organ`, or another topology. The engine only needs object -identity, labels, and relations. - -Existing `MultiScaleTreeGraph.Node` topologies enter the same registry through -`objects_from_mtg(root; ...)` or `CompositeModel(root; ...)`. The adapter traverses once -and accepts accessors for ids, labels, status, and geometry; the timestep -runtime does not query the MTG topology. - -### Scale - -A scale is a label on objects, not a separate runtime layer. Examples: - -```julia -:Scene -:Plant -:Axis -:Internode -:Leaf -:Soil -:SoilLayer -:Voxel -``` - -### Scope - -A scope is a named or inferred subset of objects. Examples: - -```julia -SceneScope() -Self() -SelfPlant() -Ancestor(scale=:Plant) -Scope(:oil_palm) -Many(kind=:plant) -Many(species=:oil_palm) -``` - -`Self()` means only the current object: the object on which the consuming -application runs. It never means the model, species, or plant unless that -object is itself the plant. `Subtree()` means that object and its descendants. -Neither spelling changes meaning with scale. - -`SelfPlant()` is the nearest containing plant scope. The more generic form is -`Ancestor(scale=:Plant)`. Use these when a model running below the plant scale -must access siblings or state inside the containing plant. - -Reusable plant models should use scope-relative queries. If an allocation -model is applied to each `:Plant`, `Many(scale=:Leaf, within=Subtree())` means -"the leaves inside this plant", not all leaves in the model. The same query -applied to an axis-scale model means "the leaves inside this axis". - -CompositeModel-level models widen the scope explicitly with `within=SceneScope()`. - -Topology-relative selections use `Relation(...)`: - -```julia -One(Relation(:parent)) -Many(Relation(:children)) -Many(Relation(:ancestors); scale=:Plant) -Many(Relation(:descendants); scale=:Leaf) -Many(Relation(:siblings)) -``` - -Supported relations are `:self`, `:parent`, `:children`, `:ancestors`, -`:descendants`, and `:siblings`. They resolve relative to the current model -application object. An explicit `within=...` scope intersects the relation -result; inferred default scopes do not hide parents or siblings. Relation -results are normalized to stable object-id order before bindings are compiled. - -### Object Template And Instance - -An object template is a reusable model/parameter bundle, for example one oil -palm species model. An object instance is one concrete object in the model. - -The same template can be mounted several times: - -```julia -oil_palm = CompositeModelTemplate( - kind=:plant, - species=:oil_palm, - applications=oil_palm_applications, - parameters=oil_palm_parameters, -) - -model = CompositeModel( - ObjectInstance(:palm_1, oil_palm; root=node1), - ObjectInstance(:palm_2, oil_palm; root=node2), - ObjectInstance(:palm_3, oil_palm; root=node3), - ObjectInstance(:palm_4, oil_palm; root=node4), -) -``` - -Models and parameters can be overridden at instance or object level: - -```julia -ObjectInstance(:palm_2, oil_palm; overrides=( - stomatal_conductance = Tuzet(; g1=3.2), -)) - -Override( - object=:leaf_12, - application=:photosynthesis, - model=Fvcb(; VcMaxRef=90.0), -) -``` - -Ownership is reference-based and explicit: - -- a template retains the supplied model and parameter objects without copying; -- unchanged instances share those exact objects; -- an instance override replaces one complete model application with another - user-owned model object; -- an object override replaces that application only for the selected object; -- PlantSimEngine does not mutate model fields or implicitly merge parameter - dictionaries. - -Overrides must preserve the model contract: process identity and declared -status/environment variable names cannot change. Parameter-only overrides of -the same concrete model type retain concrete runtime dispatch. Heterogeneous -alternative implementations are supported but may require dynamic dispatch for -the exceptional application. - -### Model Kernel And Model Application - -A model kernel is the reusable model implementation written by a modeler. It -defines a process, parameters, `inputs_`, `outputs_`, optional `dep` defaults, -optional environment traits, and `run!`. - -A model application is the scenario-specific use of that kernel on selected -objects, at a selected rate, with selected value inputs, model calls, update -rules, output routing, and environment binding behavior. - -The model kernel should not need to know: - -- the species it will be used with; -- the model it will be embedded in; -- the timestep chosen by the user; -- whether its inputs come from local state, another scale, another object, a - temporal stream, units, automatic differentiation values, or uncertainty - wrappers. - -The scenario owns those decisions through `ModelSpec`. - -Target shape: - -```julia -ModelSpec(LeafEnergyBalance(); name=:leaf_energy, on=Many(kind=:plant, scale=:Leaf), inputs=(...), calls=(...), every=Hour(1)) -``` - -Application names are optional when a process occurs only once. Scenario -declarations identify singular producers and call targets with -`application=:sunlit_photosynthesis`; `process=...` is reserved for explicit -discovery queries such as `Many(process=:photosynthesis)`. - -## Unified Model Configuration - -`ModelSpec` is the single scenario wrapper. Released mapping-era configuration -is replaced by explicit value inputs and callable model calls. - -### Applies To - -Use `ModelSpec(...; on=...)` to declare the object set where a model application runs. -This should be first-class, not inferred from a container or mapping key. - -```julia -ModelSpec(LeafState(); on=Many(kind=:plant, scale=:Leaf)) - -ModelSpec(AllocationModel(); on=Many(kind=:plant, scale=:Plant)) - -ModelSpec(SceneEB(); on=One(scale=:Scene)) -``` - -The same model kernel can be applied several times with different selectors, -parameters, timesteps, or input bindings. The compiler should normalize each -application to a stable application id. - -### Dependency Defaults From Traits - -Model authors should still declare `inputs_`, `outputs_`, and `dep`. In the -final design, `dep(model)` is the model-level place for default dependency -intent. Historical `ModelMapping` declarations are migration inputs to the -CompositeModel/Object runtime, not a second supported path. - -The rule is: - -- `inputs_(model)` declares the variables the model needs; -- `outputs_(model)` declares the variables the model computes; -- `dep(model)` declares default value sources or manual model calls when the - model author knows a sensible coupling pattern; -- `ModelSpec(...; inputs=(...))` and `ModelSpec(...; calls=(...))` override - or specialize those defaults for a specific simulation. - -For example, a plant allocation model can provide a plant-local default: - -```julia -dep(::PlantAllocationModel) = ( - leaf_carbon = Input(Many(scale=:Leaf, within=Subtree(), var=:leaf_carbon)), -) -``` - -An energy-balance model can declare that it usually calls a stomatal -conductance model manually: - -```julia -dep(::LeafEnergyBalanceModel) = ( - stomatal_conductance = Call(One(process=:stomatal_conductance)), -) -``` - -These trait defaults are not absolute wiring. They are model-author defaults -that make common cases work without repeating configuration, while scenario -authors keep final authority through `ModelSpec`. - -Compiler order: - -1. read `inputs_`, `outputs_`, and `dep`; -2. infer simple same-object value dependencies when unambiguous; -3. apply `dep(model)` defaults for value inputs and model calls; -4. apply `ModelSpec` overrides last. - -This order is part of the public contract. It keeps modeler defaults useful -without making them final wiring. Missing or ambiguous inputs after this pass -are errors, not incidental fallback behavior. - -### Value Inputs - -Use `ModelSpec(...; inputs=...)` when a model needs values before its `run!` method executes: - -```julia -ModelSpec(LAIModel(ground_area); inputs=(:leaf_areas => Many(scale=:Leaf, within=SceneScope(), var=:leaf_area))) -``` - -Reusable plant allocation: - -```julia -ModelSpec(AllocationModel(); on=Many(scale=:Plant), inputs=(:leaf_carbon => Many(scale=:Leaf, within=Subtree(), var=:leaf_carbon))) -``` - -The same declaration must compile to: - -- direct `Ref`/`RefVector` wiring when producer and consumer live in the same - object graph and rate; -- temporal stream reads when producer and consumer run at different rates; -- materialization when target status must be assigned before a - model runs; -- source-status lookup for graph-backed object selections. - -The important user rule is: - -- `ModelSpec(...; inputs=...)` means "give this model values"; the runtime schedules or - samples producers; -- the receiving model never manually calls the producer because of an - `ModelSpec(...; inputs=...)` declaration. - -### Carrier And Copy Semantics - -The compiler chooses the carrier, but the semantics must be documented and -explainable: - -| Situation | Preferred carrier | Copy behavior | -| --- | --- | --- | -| same-rate scalar input | shared `Ref` or local alias | no copy when possible | -| same-rate `Many(...)` input | `RefVector` or equivalent typed reference collection | no copy for live values | -| cross-rate input | temporal stream sample | value materialized for the consumer timestep | -| `Integrate` or `Aggregate` input | temporal window reduction | reduced value materialized | -| materialized target status input | compiler-generated assignment | assigned before consumer run | -| environment input | cached `EnvironmentBinding` sample | backend-defined value sample | - -This table is a required part of the design because performance, units, -automatic differentiation, and error propagation depend on preserving user -value types and avoiding hidden copies. - -PlantSimEngine should not force `Float64` internally. Status values, -parameters, environment values, and outputs must be allowed to use units, dual -numbers, uncertainty wrappers, tracked arrays, or other numeric-like types. -Compiled carriers should be parametric and type stable whenever the object set -and value type are known at initialization. - -### Multirate Inputs - -Multirate must be supported by the same `ModelSpec(...; inputs=...)` declaration, not a -separate mapping language. The public time language should remain `Dates` -periods. - -Example: - -```julia -ModelSpec(PlantAllocation(); on=Many(kind=:plant, scale=:Plant), inputs=(:leaf_assimilation => Many( - scale=:Leaf, - within=Subtree(), - var=:assimilation, - policy=Integrate(), - window=Day(1), - )), every=Day(1)) -``` - -Policy precedence should stay explicit: - -1. input-level policy in `ModelSpec(...; inputs=...)`; -2. producer `output_policy(model)`; -3. default `HoldLast()`. - -Cross-rate links must go through temporal state even when they point to objects -that could otherwise be reference-wired. - -Same-timestep feedback cycles are broken explicitly on the receiving input: - -```julia -ModelSpec(CarbonState(); inputs=(PreviousTimeStep(:carbon_biomass) => One( - scale=:Plant, - application=:carbon_allocation, - var=:carbon_biomass, - ),)) -``` - -`PreviousTimeStep(:x)` removes the producer-to-consumer edge from the current -timestep graph and reads the latest source sample at or before the previous -model timestep. Before a source sample exists, the initialized consumer status -value for `x` is used. This makes initialization part of the scenario contract -instead of silently inventing a zero value. - -### Model Calls - -Use `ModelSpec(...; calls=...)` when a model must manually run selected models, typically -inside an iterative solver. This is the required public API name and must be -implemented as part of the unified composite-model/object redesign, not left as a later -rename. - -```julia -ModelSpec(SceneEB(); calls=(:leaf_energy => Many( - kind=:plant, - scale=:Leaf, - process=:energy_balance, - )), calls=(:soil => One(kind=:soil, application=:soil_water))) -``` - -Inside `run!`, the model model receives call handles and calls -`run_call!(call)` during trial iterations, then -`run_call!(call; publish=true)` for the accepted final solution. The default is -deliberately `publish=false`: trial calls mutate target status for convergence -checks but do not append temporal samples or write environment outputs. - -The important user rule is: - -- `ModelSpec(...; calls=...)` means "give this model callable model handles"; -- the parent model owns the call stack and can iterate, reject, or accept trial - calls; -- call outputs are published only according to the call publication contract. - -`Diagnostics.explain_calls(compiled)` exposes this as -`publication_policy=:explicit_accept`, with `default_publish=false` and -`accepted_publish=true`. - -Binding and call explanations also report where each dependency declaration -came from: - -- `origin=:inferred_same_object` for compiler-inferred value dependencies; -- `origin=:model_default` for `Input(...)` or `Call(...)` declarations coming - from `dep(model)`; -- `origin=:model_spec` for scenario-level `ModelSpec(...; inputs=...)` or `ModelSpec(...; calls=...)`, - including declarations that override a model default. - -### Multiplicity - -Selection multiplicity is explicit: - -```julia -One(...) -Many(...) -OptionalOne(...) -``` - -`OptionalOne(...)` resolves to zero or one dependency. With zero matches, an -input keeps its `inputs_` default and a call returns an empty -`call_targets(...)` collection. Explanations retain these unresolved -optional bindings instead of hiding them. - -The compiler validates that `One(...)` resolves to exactly one producer per -consumer scope. `Many(...)` returns a vector-like value or target collection. - -### Address Normalization - -All source and target declarations normalize to an internal address: - -```julia -Diagnostics.ObjectAddress( - scope, - kind, - species, - scale, - name, - process, - application, - var, - relation, - policy, - window, - from_status, - after, - multiplicity, -) -``` - -Only the compiler works with this normalized address. Users should not need to -construct it manually. [`Diagnostics.object_address`](@ref) is the structured diagnostic -view and preserves every normalized selector field, including temporal and -status-routing fields. - -## Object Lifecycle And Spatial Contracts - -Growth, pruning, organ creation, reparenting, and moving organs must all update -the same compiled caches: - -- object selections used by `on`, `inputs`, and `calls`; -- `RefVector` or equivalent many-object carriers; -- temporal stream ownership; -- writer validation; -- environment bindings. - -The public mutation API should make cache invalidation explicit and centralized: - -```julia -register_object!(model, object; parent) -remove_object!(model, object) -reparent_object!(model, object, new_parent) -move_object!(model, object, geometry_or_position) -Advanced.refresh_bindings!(model) -``` - -Spatial environment backends should depend on a small geometry contract, not on -a particular plant representation: - -```julia -position(object_or_status) -geometry(object_or_status) -bounds(object_or_status) -``` - -Packages can provide richer geometry, octrees, voxel grids, or layers, but -PlantSimEngine should only require enough information to bind an object to an -environment provider. - -## Duplicate Writers And Updates - -Most variables should have one canonical writer per object and timestep. When a -variable is intentionally updated by several models, the scenario should say so -where the model applications are assembled: - -```julia -ModelSpec(PruningModel(); on=Many(scale=:Leaf), updates=Updates(:leaf_biomass; after=:carbon_allocation)) -``` - -`Updates(...)` should be rare and explicit. It is a scenario-level ordering -rule, because a model author cannot predict every model that will later update -the same variable. - -## Environment And Microclimate - -Meteorology should remain automatic unless a model or scenario needs special -behavior. Models declare environment variables: - -```julia -environment_inputs_(::LeafEnergyModel) = ( - T=0.0, - Rh=0.0, - Wind=0.0, - Ri_PAR_f=0.0, - CO2=0.0, -) -``` - -The runtime resolves those variables through the model environment service. - -Default resolution: - -1. A global/table environment backend gives every object the current sampled row. -2. A voxel, octree, layered, or grid backend samples the cell bound to the - object. -3. If the object has no position, use the parent position. -4. If no spatial binding can be made, fall back to the global environment or error when - the environment variable is required. - -Users can override the binding contract: - -```julia -EnvironmentResolver( - bind=(model, object) -> containing_cell(model.microclimate, position(object)), -) -``` - -PlantSimEngine should define the protocol and caching hooks, not the voxel or -octree implementation. Specialized packages should provide concrete spatial -backends. - -The environment backend protocol should be small and backend-oriented: - -```julia -handle = EnvironmentAPI.bind_environment(backend, object, context, config) -EnvironmentAPI.sample(backend, handle, variable, time) -EnvironmentAPI.sample(backend, handle, trial_state, variable, time) -commit_environment!(backend, handle, accepted_state, time) -EnvironmentAPI.update_index!(backend, changed_entities, removed_object_ids) -``` - -`environment_inputs_(model)` declares what a model reads from the active environment -provider, while `environment_outputs_(model)` declares what it may commit. Controllers -commit accepted mutable microclimate state with -`commit_environment!(context, accepted_state)` and run trial descendants with -`run_call!(context, name; environment=trial_state)`. Simple global meteorology -remains the default provider. - -Scenario-level environment source remapping belongs on `Environment(...)`, for -example: - -```julia -ModelSpec(LeafGasExchange(); name=:gas_exchange, on=Many(scale=:Leaf), environment=Environment(provider=:global, sources=(CO2=:Ca,))) -``` - -Here the model reads `environment.CO2` because that is its declared generic -contract, while the active environment backend samples the source variable -`:Ca`. Environment binding refresh validates source availability when the -backend can enumerate variables, and explanations report both -`required_inputs` and `source_inputs`. - -Global tabular meteorology follows the model application's compiled -`ModelSpec(...; every=...)`. PlantMeteo samples the table with the reducer and window from -`environment_hint(...)` when the model runs more slowly than the weather base step. -An `Environment(; sources=...)` override replaces only the source variable; it -does not discard the model-author reducer. The prepared weather sampler is -compiled once, and one sampled row is reused by every object targeted by the -same application at that timestep. - -Spatial or mutable backends retain control of their own temporal semantics. -PlantSimEngine supplies the compiled object/cell binding and current simulation -time; a specialized microclimate backend decides whether its local state is -instantaneous, interpolated, or internally integrated. - -### Cached Environment Bindings - -Spatial lookup must not happen for every model call. At initialization and when -objects are created, the runtime builds an environment binding cache: - -```julia -EnvironmentBinding( - object_id, - provider=:microclimate_grid, - cell_id, - variables=(:T, :Rh, :Wind, :Ri_PAR_f), -) -``` - -Runtime sampling is: - -```text -object -> cached binding -> environment cell -> current values -``` - -Invalidation events: - -- object created; -- object removed; -- object moved; -- geometry changed; -- environment grid rebuilt or refined; -- model environment requirements changed. - -Geometry APIs should provide ergonomic invalidation: - -```julia -mark_environment_binding_dirty!(model, object) -update_geometry!(object, geometry; invalidate_environment=true) -``` - -Before each timestep, dirty bindings are refreshed in batch. - -## Compilation Strategy - -The compiler builds one global dependency graph over object addresses. -The graph includes: - -- value dependencies from `ModelSpec(...; inputs=...)`; -- callable dependencies from `ModelSpec(...; calls=...)`; -- model update edges from `Updates(...)`; -- temporal policy edges; -- environment reads and writes; -- object-scope selection caches. - -The runtime representation is an implementation detail: - -- same-rate local links can stay as aliases; -- cross-rate links use temporal state; -- many-object links use `RefVector` or node-value streams; -- call links use immutable call plans with lifecycle-maintained target buffers; -- environment links use application-level plans and object-specific handles. - -### Immutable plans and mutable object state - -The application graph is immutable during a simulation. Growth changes which -objects participate in that graph, not which applications, dependency -declarations, cadence rules, or selector programs exist. The compiler keeps -these two ownership layers separate: - -| Immutable for the scenario | Mutable at lifecycle barriers | -| --- | --- | -| application identity, model specification, and dense application slot | application target object ids | -| dependency edges and topological order | value carriers and temporal source buffers | -| compiled input and hard-call selector programs | hard-call target buffers | -| cadence definitions and schedule entries | schedule cursor and current execution groups | -| output-retention requirements and publication variables | stream instances and request membership intervals | -| environment backend/configuration, sampling rules, sampler, and prepared source | object handles, geometry provenance, and spatial index entries | - -`CompiledScenarioPlan` owns the immutable application, input, call, ordering, -and cadence definitions. Object-level bindings delegate to these plans rather -than copying their selector, policy, contract, or environment metadata. -`CompiledExecutionPlan` uses dense application slots for root scheduling and -keeps homogeneous target batches concrete. - -A `LifecycleDelta` journals additions, removals, reparenting, and movement once. -At the safe barrier after the mutating application, the runtime consumes that -shared delta to update affected application targets, input carriers, hard-call -targets, temporal storage, output membership, environment handles, and -execution groups. Applications still due later in the same timestep may run on -new objects. The following unchanged timestep returns to the same precompiled -schedule without selector resolution or graph reconstruction. - -The final execution plan should group contiguous targets with the same concrete -model, status, model-bundle, input-binding, and environment-binding types. -Dynamic dispatch may occur once at the application/batch boundary, but not for -every leaf in a homogeneous target set. Exceptional model overrides form -separate concrete batches while preserving stable object order. Lifecycle or -environment refreshes rebuild these batches before the next timestep. - -The public explanation API must describe the normalized graph, not the internal -carrier choice. - -Use `run!(model; performance=true)` only for coarse phase instrumentation. The -structured `Diagnostics.explain_runtime_performance(simulation)` view groups -the resulting counters into immutable-plan compilation, object-target -instantiation, lifecycle-buffer updates, steady-state execution, output -collection, and initial totals. Benchmark the normal runtime with -`performance=false`, because enabled instrumentation deliberately calls -`time_ns()` around compiler/runtime boundaries. - -## Agent-Facing Requirements - -The final design must be understandable by agents through structured -explanation helpers: - -```julia -Diagnostics.explain_objects(model) -Diagnostics.explain_instances(model) -Diagnostics.explain_scopes(model) -Diagnostics.explain_bindings(sim) -Diagnostics.explain_calls(sim) -Diagnostics.explain_environment_bindings(sim) -Diagnostics.explain_schedule(sim) -Diagnostics.explain_writers(sim) -Diagnostics.explain_execution_plan(sim) -Diagnostics.explain_output_retention(sim) -Diagnostics.explain_runtime_performance(sim) -``` - -These helpers should return stable structured data, not only pretty text. A -binding row should include at least: - -- consumer application id; -- consumer object id; -- consumer variable; -- source selector; -- resolved producer application id or environment provider id; -- resolved producer object ids; -- process/name filters; -- temporal policy and window; -- carrier kind; -- copy/reference semantics; -- reason the binding was chosen; -- whether it came from inference, `dep(model)`, or `ModelSpec`. - -Execution-plan rows should additionally expose the selected object ids, -concrete model/status/carrier types, batch size, and whether the inner loop is -homogeneous and specialized. - -Output-retention rows should expose the retained application id, variable, -retention reasons, compiled retention horizon, and current target count so -agents can distinguish default retain-all behavior, requested output streams, -and bounded temporal-dependency streams. - -Errors should report concrete object labels, scope selectors, process names, -variables, and suggested fixes. - -## API Position - -This is a breaking design. Model kernels use -`run!(model, status, environment, constants, context)` while the scenario -configuration surface uses `CompositeModel`, `Object`, `ModelSpec`, selectors, -`ModelSpec(...; inputs=...)`, `ModelSpec(...; calls=...)`, `ModelSpec(...; every=...)`, and `Environment(...)`. diff --git a/docs/src/dev/composite_model_implementation_plan.md b/docs/src/dev/composite_model_implementation_plan.md deleted file mode 100644 index cd575289b..000000000 --- a/docs/src/dev/composite_model_implementation_plan.md +++ /dev/null @@ -1,1036 +0,0 @@ -# Unified CompositeModel/Object Implementation Plan - -This plan is the persistent handoff for replacing the historical -multiscale-mapping system with one composite-model/object address system. - -The implementation can be incremental internally, but the target API is -breaking. Do not preserve experimental intermediate APIs as user-facing -concepts in the final design. - -The target public surface should be centered on a small set of concepts: - -```julia -ModelSpec( - model; - name=:application, - on=Many(scale=:Leaf), - inputs=(...), - calls=(...), - every=Dates.Hour(1), - environment=Environment(...), - output_routing=(...), - updates=Updates(...), -) -``` - -This is the API memory target for users, modelers, and agents. Additional -types should be selectors, model traits, or internal compiled carriers. - -## Implementation Progress - -- Started Phase 0 by adding typed application metadata, now constructed - directly with `ModelSpec` keywords. -- Added `ModelSpec(model; name=...)` application names and getters: - `application_name`, `applies_to`, `value_inputs`, `model_calls`, and - `environment_config`. -- Added selector and address types: `SceneScope`, `Self`, `Subtree`, - `SelfPlant`, `Ancestor`, `Scope`, `Relation`, `One`, `OptionalOne`, `Many`, - and `Diagnostics.ObjectAddress`. -- Added initial `CompositeModel`/`Object` registry types and lifecycle hooks: - `register_object!`, `remove_object!`, `reparent_object!`, `move_object!`, - and `Advanced.refresh_bindings!`. -- Added registry-backed selector resolution with `resolve_object_ids` and - `resolve_objects` for `SceneScope()`, `Self()`, `SelfPlant()`, - `Ancestor(...)`, `Scope(...)`, label keywords such as `kind=:plant` and - `scale=:Leaf`, and `One`/`OptionalOne`/`Many` cardinality checks. -- Added `Diagnostics.explain_scopes(model)` for agent-readable scope diagnostics. It - reports the global model scope, each object subtree, each named - `Scope(...)`, and label groups by scale, kind, and species with concrete - resolved object ids. -- Selector cardinality and named-scope failures now report the consumer - context, matched object ids, requested criteria, available scales, kinds, - species, and names inside the resolved scope, plus bounded edit-distance - suggestions. -- `Relation(...)` selectors now resolve `:self`, `:parent`, `:children`, - `:ancestors`, `:descendants`, and `:siblings` relative to the consuming - object. Explicit scopes constrain relation results, default dependency scopes - do not erase parent/sibling queries, and compiled `ModelSpec(...; inputs=...)` can use these - relations without runtime selector resolution. -- `Diagnostics.ObjectAddress(selector)` normalizes scope, relation, label, routing, - temporal-policy, and status-ordering fields into one structured diagnostic - record. -- Started the object-address compiler with `Advanced.compile_composite_model(model, specs)` and - compiled model application/binding carriers. The compiler now resolves - `ModelSpec(...; on=...)` target object ids, object-relative `ModelSpec(...; inputs=...)` source - object ids, and object-relative `ModelSpec(...; calls=...)` callee object/application ids - before runtime. -- Added `Diagnostics.explain_applications`, `Diagnostics.explain_bindings`, and `Diagnostics.explain_calls` - for the compiled model view. These explanations expose application ids, - processes, target ids, input source ids, call callee ids, temporal policy, - window, and carrier hints. -- Added status-backed compiled input carriers. When source objects already - hold `Status` values, `ModelSpec(...; inputs=...)` bindings now precompile a scalar shared - `Ref`, a homogeneous `RefVector`, or an `Advanced.ObjectRefVector` fallback for - heterogeneous reference-preserving vectors. `Diagnostics.input_carrier`, `Diagnostics.input_value`, - and `Diagnostics.has_reference_carrier` expose these carriers, and `Diagnostics.explain_bindings` - reports carrier kind, copy/reference semantics, carrier type, and reference - availability. -- Added conservative same-object input inference in the model compiler. When a - model declares an `inputs_` variable that is not covered by explicit/default - `ModelSpec(...; inputs=...)`, and exactly one other application on the same object outputs - the same variable, `Advanced.compile_composite_model` creates an inferred reference binding. - `Diagnostics.explain_bindings` now reports binding `origin` values such as - `:model_default`, `:model_spec`, and `:inferred_same_object`. -- Compiled input bindings now carry producer metadata. When an `ModelSpec(...; inputs=...)` - selector uses `process=` or `application=`, `Advanced.compile_composite_model` validates that a - matching source application exists for the selected source objects. - `Diagnostics.explain_bindings` reports `source_application_ids`, `process`, and - `application` for agent-readable dependency diagnostics. -- Dependency selectors in `ModelSpec(...; inputs=...)` and `ModelSpec(...; calls=...)` now infer a default - scope from the consumer object when no explicit `within=...` is provided: - model objects default to `SceneScope()`, while non-model objects default to - `Self()`. Shared model/soil dependencies from organs should therefore use - `within=SceneScope()` explicitly. -- `Advanced.compile_composite_model` now validates `Required(T)` status inputs - from `inputs_(model)`. Each required input must either have a compiled - binding or already exist on the target object `Status`; otherwise - compilation errors with the concrete application id, object id, and input - variable. -- CompositeModel compilation creates an empty `Status` for model-targeted - objects when status is omitted, inserts missing `outputs_` fields and - `Default(value)` inputs from their initial values, and installs explicitly or - implicitly bound input carriers. Unbound `Required(T)` inputs remain - compilation errors. -- `Advanced.compile_composite_model` now rejects `ModelSpec(...; inputs=...)` declarations whose left-hand - variable is not declared by the target model's `inputs_`. This catches - misspelled or stale scenario bindings before they create silent unused - metadata. -- `Advanced.compile_composite_model` now validates source availability for status-backed - non-temporal `ModelSpec(...; inputs=...)` bindings. When selected source objects already - have `Status` values, the requested source variable must resolve to - references instead of silently compiling to an unused/no-op binding. -- Carrier compilation preserves source `Status` references and arbitrary value - types; tests cover scalar refs, heterogeneous many-object vectors, and a - homogeneous dual-like `BigFloat` value through `RefVector`, model arithmetic, - source mutation, and typed output publication. -- Same-object renaming is supported directly by `ModelSpec(...; inputs=...)`, for example - `inputs=(:renamed_signal => One(within=Self(), var=:signal),)`. The compiler - aliases the source `Ref`, records the renamed source variable in - `Diagnostics.explain_bindings`, and schedules the producer before the consumer. -- Same-rate input carriers are installed directly into consumer `Status` - reference cells during model compilation. Scalar bindings share the source - `Ref`; many-object bindings store the compiled `RefVector` or - `Advanced.ObjectRefVector` once. The timestep runtime performs no assignment for - these bindings, and a focused `Many(...)` materialization gate verifies zero - allocations after compilation. -- Reference wiring adds a missing bound input field to the consumer `Status` - schema when needed, instead of requiring users to duplicate compiler-owned - input placeholders. -- Added call ambiguity validation in the compiled model view: a call can select - by process when unique, and must use `application=:name` when several model - applications with the same process match the same object. -- Added a model binding cache with `Advanced.refresh_bindings!`, `Advanced.bindings_dirty`, - `Advanced.compiled_bindings`, and `Advanced.model_revision`. Object creation, removal, - and reparenting now invalidate the compiled binding cache and bump a model - revision before the next refresh. -- Added an environment binding cache with `Advanced.refresh_environment_bindings!`, - `Advanced.compile_environment_bindings`, `Advanced.CompiledEnvironmentBinding`, - `Advanced.CompiledEnvironmentBindings`, `Advanced.environment_bindings_dirty`, - `Advanced.compiled_environment_bindings`, `Advanced.environment_revision`, and - `Diagnostics.explain_environment_bindings`. The compiler resolves each - application/object environment provider, backend, required - `environment_inputs_`, support descriptor, and backend cell before runtime. -- Added the minimal model geometry contract: `geometry(object_or_status)`, - `position(object_or_status)`, and `bounds(object_or_status)`. Environment - binding refreshes now call `EnvironmentAPI.update_index!(backend, changed_entities, removed_object_ids)` once per - distinct backend before `Advanced.EnvironmentAPI.bind_environment`, giving spatial backends a current - model-wide object/entity list for precomputed microclimate lookup. -- Automatic spatial binding now uses the nearest ancestor geometry when a - target object has no geometry of its own. Existing backends still receive an - `Object` carrying the target id/status, while its binding-time geometry comes - from the ancestor. Explanations report `geometry_source=:self`, `:ancestor`, - or `:global` and the source object id. -- Moving an object invalidates environment bindings for descendants that - inherit its geometry, stopping at descendants with their own geometry. This - preserves unaffected cached bindings. -- Environment refresh now reconciles model environment contracts against - cached spatial bindings. If only `environment_inputs_` changes while application - id, object, process, provider, backend, status, and geometry provenance remain - unchanged, required metadata is updated while the cached cell is reused - without `EnvironmentAPI.update_index!` or `Advanced.EnvironmentAPI.bind_environment`. -- `validate_environment_inputs(model)` and - `validate_environment_inputs(compiled_scene, environment_or_backend)` now validate - composite-model/object model application `environment_inputs_` against the active - environment or an explicit replacement. Errors report model application ids, - so duplicate process applications remain diagnosable. -- `Environment(; sources=(target=:source,))` now remaps model-facing - environment variables to backend source variables. Environment binding - refresh validates missing source variables when the backend can enumerate its - variables, and `Diagnostics.explain_environment_bindings` reports both `required_inputs` - and `source_inputs`. -- CompositeModel applications now infer model-author default environment source remaps - from `environment_hint(...).bindings` when the scenario does not provide explicit - environment bindings. Scenario `Environment(; sources=...)` keeps precedence over - the trait. -- Global tabular meteorology is now sampled at each model application's - compiled clock. `environment_hint(...).bindings` reducers and windows are applied - through PlantMeteo, while `Environment(; sources=...)` replaces only the - source variable and preserves the selected reducer. Prepared weather - samplers are shared by applications using the same weather table, and each - application/timestep sample is cached once per run so all target objects - reuse it. -- Object creation, removal, and reparenting invalidate both structural and - environment bindings. Object movement invalidates only environment bindings, - so moving a leaf or changing its geometry can refresh microclimate lookup - without rebuilding object/model binding carriers. -- Added public geometry invalidation helpers: - `update_geometry!(model, object, geometry; invalidate_environment=true)` - and object-scoped `mark_environment_binding_dirty!(model, object)`. - Geometry-only changes record the affected object ids and refresh only their - compiled environment bindings; descendants inheriting moved ancestor - geometry are invalidated as part of the same object-scoped refresh. -- Started composite-model/object execution with `run!(model; steps=...)`. - The runtime refreshes compiled object bindings and environment bindings, - materializes precompiled `ModelSpec(...; inputs=...)` carriers into consumer `Status` - fields, samples the bound environment backend, and calls generic model - kernels through the existing `run!` contract. -- CompositeModel/object execution now publishes model outputs to model-local temporal - streams. Compiled `ModelSpec(...; inputs=...)` bindings marked as `:temporal_stream` can - materialize `HoldLast`, `Interpolate`, `Integrate`, and `Aggregate` values - before the consumer runs, using selector source ids, source variables, - windows, and the model base timestep. -- CompositeModel temporal `ModelSpec(...; inputs=...)` now honor producer `output_policy(...)` traits - when the selector omits `policy=...` and resolves to a unique source - application. Explicit selector policies remain scenario-level overrides. -- CompositeModel `Interpolate(...)` matches the established multirate runtime: - bracketed samples use linear interpolation, online consumers use linear - extrapolation from the last two samples when requested, and insufficient or - non-interpolable values fall back to hold-last. `mode=:hold` and - `extrapolation=:hold` are supported, invalid modes fail during model - compilation, and interpolation arithmetic preserves generic numeric value - types without converting model values to `Float64`. -- Unified `ModelSpec(...; inputs=...)` now supports explicit lagged dependencies with - `PreviousTimeStep(:input) => selector`. Lagged bindings use temporal streams, - read source samples at or before `t - 1`, preserve the initialized consumer - status value until history exists, and do not add a same-timestep scheduling - edge. This allows feedback cycles to compile without changing generic model - kernels. -- CompositeModel/object execution now exposes explicit mutable environment - commits through `commit_environment!(context, accepted_state)` and - non-committing trial sampling through - `run_call!(context, name; environment=trial_state)`. - Meteorological state stays in the environment backend instead of being staged - through same-named status values. -- Added root application scheduling from `ModelSpec(...; every=...)` using `Dates.Period` - values and the model environment base step. `Diagnostics.explain_schedule` on a - `Advanced.CompiledCompositeModel` now reports each application clock, phase, timestep in base - steps, timestep duration in seconds, and whether the application is scheduled - as a root application or is manual-call-only. -- CompositeModel application scheduling now also honors a model's `timespec(...)` trait - when `ModelSpec(...; every=...)` is omitted. Scenario-level `ModelSpec(...; every=...)` keeps - precedence over the model trait, matching the established multirate runtime. -- CompositeModel application scheduling now validates `timestep_hint(...)` required - bounds for base-step-derived clocks. Hints remain compatibility constraints; - they do not override explicit `ModelSpec(...; every=...)` or non-default `timespec(...)`. -- `Advanced.compile_composite_model` now computes a stable topological application order from - resolved `ModelSpec(...; inputs=...)` producer edges and `Updates(...)` writer-order edges. - Inputs produced by manual-call-only applications are redirected to the parent - application that owns the `ModelSpec(...; calls=...)` call stack. `run!(model)` uses this - precompiled order instead of user declaration order, cycles fail at compile - time, and `Diagnostics.explain_schedule` reports `execution_index`. -- `Advanced.CompiledCompositeModel` now pre-indexes input and call bindings by - `(application_id, object_id)`. Per-object input materialization and - `call_targets` lookup uses these indexes instead of scanning every - binding in the model at each model call. -- `Advanced.CompiledCompositeModel` now also pre-indexes applications by application id. - Hard-call target resolution and stable ordered-application materialization - use this index instead of scanning or rebuilding lookup dictionaries. -- `Advanced.CompiledEnvironmentBindings` now pre-indexes environment bindings by - `(application_id, object_id)`. Environment sampling and mutable environment - output scattering use direct lookup instead of scanning all environment - bindings for every model invocation. -- Added `RunContext` and `CallTarget`. Models can retrieve manual - `ModelSpec(...; calls=...)` targets with `call_targets(context, :name)` and execute - them with `run_call!`, preserving explicit call-stack control in the - composite-model/object runtime. Manual calls execute immediately under the parent call - stack; applications selected by `ModelSpec(...; calls=...)` are skipped by the root - `run!(model)` loop and only execute through `run_call!`. -- Added composite-model/object duplicate-writer validation. During `Advanced.compile_composite_model`, each - `(object, output variable)` now has one canonical writer unless later - writers declare `Updates(:var; after=...)`. The `after` token can match a - previous application id/name or process, so scenario authors can express - cases such as pruning after carbon allocation without changing either model - implementation. -- Added `Diagnostics.explain_writers(compiled)`. It reports each object/variable writer - group, duplicate-writer status, writer application ids/processes, and the - `Updates(...)` declarations used to validate ordered updates. -- Extended `explain_model_specs` rows with application name, target selector, - value inputs, manual calls, and environment metadata. -- Started Phase 3 by compiling simple `ModelSpec(...; inputs=...)` declarations to typed - scale/variable carriers, for example - `inputs=(:x => Many(scale=:Leaf, var=:y),)`. -- Added model-level `Input(...)` defaults from `dep(model)` into - `ModelSpec` value inputs. Scenario-level `ModelSpec(...; inputs=(...))` - overrides those defaults before the native binding is compiled. -- Removed the intermediate scenario bridge after the composite-model/object compiler - gained native `ModelSpec(...; inputs=...)` support. Manual value-transfer carriers are not - retained as user-authored API. -- Removed the intermediate dependency resolver after `ModelSpec(...; calls=...)` became - native composite-model/object metadata. Manual model execution now goes through - `CallTargets`, `call_targets`, and `run_call!`. -- Added model-level `Call(...)` defaults from `dep(model)` into - `ModelSpec` manual-call metadata. Scenario-level - `ModelSpec(...; calls=(...))` overrides those defaults, and - `dep(::ModelSpec)` excludes raw `Call(...)` trait entries so default calls - are normalized through the same bridge as explicit calls. -- Migrated the MAESPA example's model energy-balance hard calls to - scenario-level `ModelSpec(scene_model; calls=(...))`. -- Migrated the MAESPA example's model LAI leaf-area transfer to consumer-side - `ModelSpec(LAIModel(...); inputs=(...))`. -- Started Phase 5 with `CompositeModelTemplate` and `ObjectInstance`. A template stores - reusable composite-model/object `ModelSpec`s plus default object labels, and an - instance mounts those specs inside one named object subtree. -- `CompositeModel(...)` accepts `ObjectInstance` values directly or through its - `instances` keyword. An instance root can be an owned `Object` or the id of - an object supplied separately to the model. -- Mounted template applications receive stable instance-prefixed application - names and an implicit `Scope(instance_name)` on unqualified - `ModelSpec(...; on=...)` selectors. Their `ModelSpec(...; inputs=...)`, `ModelSpec(...; calls=...)`, scheduling, - writer validation, and execution use the normal compiled composite-model/object path. -- Instance overrides can replace one template application by application name - or process. Overrides must be unambiguous and preserve process identity. - Instances without overrides retain the exact shared model object from the - template. -- Template labels fill missing `kind` and `species` metadata throughout the - mounted subtree, while the root receives the instance name used by - `Scope(...)`. Tests cover four instances, plant-local aggregation, shared - model storage, and one process-level model override. -- Added explicit exceptional-organ overrides with - `Override(object=..., application=..., model=...)` through - `ObjectInstance(...; object_overrides=...)`. The override must resolve to one - template application, belong to the instance subtree, and preserve process, - input, output, and environment-variable declarations. -- Object overrides remain one logical model application: the compiler stores - the selected replacement model by target object id. Dependency bindings, - writer ownership, application names, and manual calls therefore remain - unchanged, and no selector resolution occurs in the runtime loop. -- Parameter/model ownership is explicit. Templates retain user-supplied model - and `parameters` objects by reference; unchanged instances share them. - Instance and object overrides retain their user-supplied replacement model - by reference. PlantSimEngine does not copy models or mutate model fields to - merge parameter overrides. -- Same-concrete-type object overrides use a concretely typed object-to-model - table. Structured application explanations report shared/per-object storage, - concrete versus heterogeneous dispatch, overridden object ids, and model - types. -- `CompositeModel` retains mounted instance metadata and `Diagnostics.explain_instances(model)` - reports each instance root, current subtree object ids, mounted application - ids, instance/object overrides, template labels, and parameter ownership. - `Diagnostics.explain_objects(model)` also reports instance membership. -- New objects registered below an instance automatically inherit missing - template `kind` and `species` labels. Instance explanations derive membership - from the current topology, so growth, pruning, and reparenting do not leave a - separate stale membership list. -- CompositeModel hard calls can run under temporary local meteorology with - `run_call!(context, name; environment=local_state)`. Descendants sample the - temporary state through normal environment bindings, while `publish=false` - suppresses output publication and environment commits. This supports - iterative microclimate solvers such as the MAESPA model energy-balance loop. -- Model kernels read their own parameters from the `model` argument. Generic - hard-dependency kernels such as `Monteith` and `Fvcb` discover declared call - targets through `call_targets` and execute them through `run_call!`. -- CompositeModel duplicate-writer validation now ignores manual-call-only applications - when validating canonical root writers. This keeps hard-dependency children - from being treated as independent root writers when they intentionally update - the same object status inside their parent call stack. -- Added a unified composite-model/object MAESPA example path: - `build_maespa_scene(...)` and `run_maespa_example(...)`. - It uses `CompositeModelTemplate`, `ObjectInstance`, `on`, `inputs`, `calls`, - and `every=Dates.Period` with two plant species, one shared soil object, - model LAI, and model energy balance. -- `test/test-maespa-model-example.jl` verifies the unified composite-model/object - MAESPA path. -- `run!(model)` now returns a `Simulation` wrapper with the mutated - `CompositeModel`, compiled model bindings, compiled environment bindings, and the - model-local temporal output streams. This keeps existing status mutation - behavior but makes model outputs inspectable after a run. -- Added `outputs(sim::Simulation)`, `collect_outputs(sim)`, and - `Diagnostics.explain_outputs(sim)` for composite-model/object runs. The explanation reports object - ids, variables, publishing application ids, sample counts, time bounds, and - value types. -- `run!(model; tracked_outputs=...)` now accepts `OutputRequest` and returns - requested model outputs through `collect_outputs(sim)` or - `collect_outputs(sim, :request_name)`. CompositeModel requests are materialized from - retained typed temporal streams after the run. They support `HoldLast`, - `Interpolate`, `Integrate`, and `Aggregate`, `Dates.Period` export clocks, - canonical-publisher inference when unique, explicit `process=...` - selection, and dynamic objects over each object's own published sample - interval. -- CompositeModel output requests now compile a publisher-level retention plan. With - `tracked_outputs=nothing`, the model runtime retains all output streams for - historical inspection. With explicit `tracked_outputs`, including an empty - request vector, it retains only requested publisher streams plus streams - required by temporal `ModelSpec(...; inputs=...)`. Dependency-only streams are pruned after - publication to the compiled policy horizon: latest-only for `HoldLast`, the - input window for `Integrate`/`Aggregate`, and enough source history for - `Interpolate`/`PreviousTimeStep`. Explicitly requested streams retain their - complete histories for post-run export. Export is therefore not yet fully - online, but unrequested temporal dependencies no longer grow for the full - simulation. -- Added `Diagnostics.explain_output_retention(sim)` to report which application/variable - streams are retained and whether the reason is default retention, an output - request, or a temporal dependency. Dependency-only rows also report their - compiled `retention_steps`; unbounded requested/default rows report - `nothing`. -- CompositeModel temporal streams are now keyed by application id, object id, and - variable. Multiple applications can publish the same variable on the same - object without overwriting each other's stream samples. -- CompositeModel output-export tests now cover requested-output `DataFrame` - materialization, canonical publisher inference when `process=` is omitted, - rejection when only stream-only publishers exist, and ambiguity when an - explicit process matches both a stream-only and a canonical publisher. -- CompositeModel `OutputRequest(...)` now accepts `application=...` to select an - explicit application id/name when the same process is mounted more than - once. Explicit application selection can retain and export a - `:stream_only` publisher. -- Each model temporal stream owns a concrete `Vector{Tuple{Float64,T}}` - selected from its first published value rather than boxing all values as - `Any`. Output type changes fail explicitly, and `Interpolate`/`Integrate` - tests verify `BigFloat` histories and reduced values remain `BigFloat`. -- CompositeModel `output_routing=(var=:stream_only,)` now matches the unified graph - semantics: stream-only outputs are excluded from canonical writer validation - and same-object input inference, while remaining available in output streams - and explicit `inputs=(... One(application=:name), ...)` selections. -- `run!(model; steps=...)` now refreshes dirty structural bindings at timestep - boundaries. Objects created, removed, or reparented by a model during one - timestep update `ModelSpec(...; on=...)` target sets, input carriers, call targets, - writer validation, and scheduling before the next timestep. -- Geometry-only mutations refresh environment bindings at the next timestep - without recompiling structural bindings. The returned `Simulation` - always contains final compiled structural and environment bindings, including - mutations performed on the last step. -- Environment dirty tracking is now object-scoped for geometry-only changes. - `move_object!`, `update_geometry!`, and - `mark_environment_binding_dirty!(model, object)` retain unaffected compiled - bindings and re-run `Advanced.EnvironmentAPI.bind_environment` only for applications targeting the - changed object. Structural changes and provider-wide invalidation still - rebuild the complete environment cache. -- Runtime lifecycle tests cover a model-created leaf joining a leaf - application and plant-local `RefVector`, a pruned leaf leaving both before - the next step, and a moved leaf switching mock microclimate cells. -- Root model execution now compiles contiguous homogeneous target batches. - Each target prebinds its concrete model, `Status`, input-binding tuple, and - environment binding. Runtime dispatch occurs once - at the batch function barrier; the inner object loop is specialized on a - concrete target type. -- Exceptional object overrides with another concrete model implementation - become separate batches without changing stable object execution order. - Structural or environment binding refresh recompiles the execution plan - before the next timestep. -- Added `Diagnostics.explain_execution_plan(scene_or_simulation)`. It reports batch object - ids, concrete model/status/carrier types, batch sizes, and inner-loop dispatch - semantics. A focused 128-leaf gate verifies zero allocations inside a warmed - homogeneous no-output batch. -- Added `objects_from_mtg(root; ...)` and `CompositeModel(root::MultiScaleTreeGraph.Node; - ...)`. Existing MTG topology is traversed once into the unified registry, - preserving stable node-derived ids, parent relations, labels, geometry, and - explicitly imported runtime status through configurable accessors; the - canonical adapter does not read or write runtime objects in MTG attributes. - -The composite-model/object compiler is executable: selectors normalize to object -addresses, resolve before runtime, and compile into reference, temporal, call, -writer, and environment carriers. The historical mapping compiler has been -removed. - -## Phase 0: Public Contract Freeze - -Goal: decide the small public vocabulary before implementing internals. - -Define: - -- `ModelSpec(model; name=nothing)` as the model-application wrapper. -- `ModelSpec(...; on=selector)` as the target object-set declaration. -- `ModelSpec(...; inputs=...)` for value dependencies. -- `ModelSpec(...; calls=...)` for manual call-stack dependencies. -- `Updates(...)` for rare ordered duplicate writers. -- `every=period::Dates.Period` and related multirate policies. -- `Environment(...)` for optional environment resolver/backend overrides. - -Rules: - -- a model kernel remains generic and declares `inputs_`, `outputs_`, optional - `dep`, optional `environment_inputs_`, and `run!`; -- a model application decides where the kernel runs, at what rate, and how its - inputs, calls, updates, outputs, and environment are bound; -- application ids are stable and can be generated from explicit `name`, - process, object selector, and occurrence index; -- if several applications provide the same process on the same object set, - selectors must disambiguate by application name or another explicit filter. - -Acceptance tests: - -- a model can be applied twice to the same leaf objects with different names; -- a dependency selector can choose by process when unique and by name when not; -- structured explanations expose model kernel type, process, application name, - and target object ids. - -## Phase 1: CompositeModel Object Registry - -Goal: introduce the internal object model without changing public behavior yet. - -Implement: - -- `ObjectId` as the stable identity key for every runtime object. -- `ModelObject` metadata with labels: - `scale`, `kind`, `species`, optional `name`, parent id, child ids, and - optional geometry/position handle. -- `Advanced.ObjectRegistry` storing objects, parent/child relations, and indexes by - label. -- adapters from existing MTG state into the registry: - each selected root and each MTG node gets an object id; - single-status simulations get one object with `scale=:Default`. -- object lifecycle hooks for add/remove/reparent that mirror the existing MTG - runtime reindexing. - -Acceptance tests: - -- the MAESPA example registers five leaf objects, two plant objects, one soil - object, and one model object; -- `status(sim, :plant_A, :Leaf)` and `status(sim, :Leaf)` can be expressed as - registry queries; -- add/remove/reparent updates object registry relations and status views. - -## Phase 2: Selector And Scope Language - -Goal: make "which objects?" explicit and reusable. - -Implement selector types: - -```julia -SceneScope() -Self() -Subtree() -SelfPlant() -Ancestor(scale=:Plant) -Scope(name) -Relation(...) -``` - -Object labels use keyword criteria such as `kind=:plant`, -`species=:oil_palm`, `scale=:Leaf`, and `name=:leaf_1`. - -Implement multiplicity wrappers: - -```julia -One(selector...) -OptionalOne(selector...) -Many(selector...) -``` - -Selectors must normalize to `Diagnostics.ObjectAddress` objects with enough context to be -resolved relative to a consuming object. - -Implement `ModelSpec(...; on=...)` using the same selector system. The target object -set of a model application must never be hidden inside a mapping key or -implicit scale table. - -Definitions: - -- `Self()` means only the current object: the object on which the consuming - model application runs. It is a plant only when that object is the plant. -- `Subtree()` means the current object and its descendants. -- `SelfPlant()` means the nearest containing plant scope. -- `Ancestor(scale=:Plant)` is the generic selector form for `SelfPlant()`. -- `SceneScope()` means the whole model. -- `Scope(name)` means a named scope or object collection. - -Rules: - -- unqualified selectors inside a reusable plant application bundle default to - `within=Self()`; -- model-level selectors default to `within=SceneScope()`; -- `One(...)` errors unless exactly one object resolves per consumer; -- `Many(...)` preserves stable object-id order; -- object-id order replaces incidental traversal order as the semantic default. -- selectors are resolved during compilation or binding refresh, not inside the - inner model loop. - -Acceptance tests: - -- plant allocation on four oil palms reads only leaves under each plant; -- model LAI reads leaves across all plant objects; -- a species-specific model model can read only `species=:oil_palm` leaves; -- a model application target set declared with `ModelSpec(...; on=...)` produces stable - application/object pairs; -- selector errors report available labels and near matches. - -## Phase 3: Unified Value Inputs - -Goal: use `ModelSpec(...; inputs=...)` as the only user-facing value-dependency declaration. -Historical `MultiScaleModel(...)` mappings are migration sources only. - -Target API: - -```julia -ModelSpec(AllocationModel(); on=Many(kind=:plant, scale=:Plant), inputs=(:leaf_carbon => Many(scale=:Leaf, within=Subtree(), var=:leaf_carbon))) - -ModelSpec(LAIModel(area); on=One(scale=:Scene), inputs=(:leaf_areas => Many(kind=:plant, scale=:Leaf, within=SceneScope(), var=:leaf_area))) -``` - -Implement: - -- `ModelSpec(...; inputs=...)` as `ModelSpec` configuration. -- `Input(...)` or an equivalent internal wrapper that lets `dep(model)` - provide default value-input bindings. -- normalized input bindings from target variable to `Diagnostics.ObjectAddress`. -- compiler pass that decides carrier: - direct reference, `RefVector`, temporal stream, or materialization. -- status-default insertion for materialized target variables using the - consumer model's `inputs_` default. -- temporal policies on value inputs: - `HoldLast`, `Interpolate`, `Integrate`, `Aggregate`. -- `Dates.Period` windows on value inputs, for example `window=Day(1)`. -- copy/reference semantics reporting for every compiled input binding. - -Rules: - -- model authors still declare `inputs_`; scenario authors decide where those - inputs come from; -- `dep(model)` may provide defaults for common value-input bindings in - composite-model/object composition; -- scenario-level `ModelSpec(...; inputs=(...))` always wins over `dep(model)` - defaults; -- same-rate local links should keep reference semantics where possible; -- cross-rate links always go through temporal state; -- duplicate source candidates are errors unless the selector disambiguates; -- materialization carriers, when needed, are internal compiler details - and are not user-authored structs; -- same-rate scalar and many-object links should avoid copies when they can use - aliases, shared refs, `RefVector`, or an equivalent typed carrier; -- PlantSimEngine must preserve arbitrary value types, including units, - automatic differentiation numbers, uncertainty wrappers, and other - numeric-like values. - -Carrier expectations: - -| Binding kind | Runtime carrier | -| --- | --- | -| same-rate scalar | shared `Ref` or local alias | -| same-rate many-object | `RefVector` or equivalent typed reference collection | -| cross-rate | temporal stream sample | -| integrate/aggregate | temporal window reduction | -| materialized cross-object input | generated pre-run status assignment | -| environment | cached environment binding sample | - -Acceptance tests: - -- the MAESPA model LAI cross-object input is declared with `ModelSpec(...; inputs=...)` and - produces the same `lai` and `leaf_area`; -- historical plant allocation `MultiScaleModel([:leaf_carbon => [:Leaf => :leaf_carbon]])` - becomes `ModelSpec(...; inputs=...)` and remains plant-local; -- a same-scale rename currently expressed with `SameScale()` works through - `ModelSpec(...; inputs=...)`; -- multi-rate value inputs integrate object streams by object id. -- same-rate many-object bindings do not allocate per timestep in a benchmarked - hot loop beyond unavoidable model work. -- unitful or dual-number status values survive `ModelSpec(...; inputs=...)` without forced - conversion to `Float64`. - -## Phase 4: Unified Model Calls - -Goal: use `ModelSpec(...; calls=...)` as the only user-facing manual model-call declaration. -The same mechanism must also be usable from `dep(model)` so hard-dependency -traits become default call declarations. - -Target API: - -```julia -ModelSpec( - SceneEB(); - on=One(scale=:Scene), - calls=( - :leaf_energy => - Many(kind=:plant, scale=:Leaf, process=:energy_balance), - :soil => One(kind=:soil, application=:soil_water), - ), -) -``` - -Implement: - -- `ModelSpec(...; calls=...)` as `ModelSpec` configuration. -- `Call(...)` or an equivalent internal wrapper that lets `dep(model)` provide - default manual-call dependencies. -- call resolution from `Diagnostics.ObjectAddress` to concrete `ModelCall` handles, or an - equivalent callable runtime object if the final internal type name differs. -- same-status hard dependency calls using the same public API. -- publication semantics: - trial `run_call!(call)` mutates status only; - final `run_call!(call; publish=true)` appends outputs and temporal - streams. -- structured call explanations with parent application id, selected callee - application ids, selected object ids, selector, and publication behavior. - -Rules: - -- calls are manual call-stack dependencies and are not independently - scheduled under the parent; -- `dep(model)` call defaults are model-author defaults, not final wiring; -- scenario-level `ModelSpec(...; calls=(...))` overrides `dep(model)` defaults; -- hard target outputs still participate in dependency graph compilation through - the owning parent when needed; -- call selection must be visible through explanation helpers. - -Acceptance tests: - -- MAESPA model energy balance uses `ModelSpec(...; calls=...)` and still controls iterative - leaf energy calls; -- missing call selectors report `kind`, `scale`, `process`, and available - matches; -- final accepted calls publish exactly once per timestep. -- an iterative model model can run selected leaf and soil calls several times - with `publish=false` and publish only the accepted state. - -Implemented: - -- `run_call!(::CallTarget)` defaults to `publish=false`, matching the - iterative manual-call contract. -- One-shot accepted calls use `publish=true` explicitly. -- An iterative hard-call regression executes two default non-publishing trials - followed by one accepted call and verifies exactly one environment write and - one temporal output sample for the accepted state. -- `Diagnostics.explain_calls(compiled)` reports - `publication_policy=:explicit_accept`, `default_publish=false`, and - `accepted_publish=true` for every compiled call edge. -- `ModelSpec` now retains per-binding provenance for value inputs and manual - calls. Bindings from `dep(model)` are reported as `:model_default`, - scenario-level `ModelSpec(...; inputs=...)` and `ModelSpec(...; calls=...)` are reported as `:model_spec`, - and compiler-created same-object value links are reported as - `:inferred_same_object`. `Diagnostics.explain_bindings`, `Diagnostics.explain_calls`, and - `explain_model_specs` expose these origins for agent-readable diagnostics. -- Zero-match `OptionalOne(...)` dependencies remain compiled and visible. - Optional inputs retain the consumer `inputs_` default with - `carrier_kind=:optional_default`; optional calls expose an empty target set - and `resolved=false` instead of failing compilation. - -## Phase 5: Object Templates, Instances, And Overrides - -Goal: support several plants of the same species with shared default models and -selective per-instance differences. - -Target API: - -```julia -oil_palm = CompositeModelTemplate( - kind=:plant, - species=:oil_palm, - mapping=oil_palm_mapping, -) - -model = CompositeModel( - ObjectInstance(:palm_1, oil_palm; root=node1), - ObjectInstance(:palm_2, oil_palm; root=node2, overrides=( - stomatal_conductance = Tuzet(; g1=3.2), - )), -) -``` - -Implement: - -- template-level model specs and parameters; -- instance-level model/parameter overrides by process; -- object-level overrides for exceptional organs; -- conflict validation when two overrides target the same process/object. -- shared parameter/model storage when template instances do not override - anything, with explicit copy/ownership behavior when they do. - -Rules: - -- templates do not prescribe topology; they attach mappings to whatever object - tree the instance provides; -- default `Self()` selectors resolve inside the current instance; -- model-wide models must opt into wider scope. - -Acceptance tests: - -- four oil palm instances share model objects/parameters when not overridden; -- one palm instance can override one process parameter; -- allocation remains per plant while model LAI sees all leaves. - -MAESPA status: - -- the unified composite-model/object MAESPA path uses `CompositeModelTemplate` and - `ObjectInstance` for species A and B. - -## Phase 5B: Object Lifecycle And Cache Invalidation - -Goal: make growth, pruning, and moving organs update every compiled binding -through one mutation path. - -Implement public lifecycle hooks: - -```julia -register_object!(model, object; parent) -remove_object!(model, object) -reparent_object!(model, object, new_parent) -move_object!(model, object, geometry_or_position) -Advanced.refresh_bindings!(model) -``` - -Implement invalidation for: - -- object selector caches; -- model application target sets; -- `RefVector` or equivalent many-object carriers; -- temporal stream ownership; -- writer validation; -- environment bindings. - -Rules: - -- topology and geometry changes do not silently leave stale carriers; -- object creation should bind the new object to model applications selected by - `ModelSpec(...; on=...)` before the next timestep; -- moving an object should refresh environment bindings without rebuilding - unrelated model bindings unless the move changes object relations or labels. - -Acceptance tests: - -- creating a new leaf adds it to plant-local allocation and model LAI before - the next timestep; -- pruning/removing a leaf removes it from many-object carriers and temporal - stream ownership; -- changing a leaf insertion angle can refresh only the affected environment - binding when topology is unchanged. - -## Phase 6: Environment Binding Cache - -Goal: make environment and microclimate sampling automatic and fast. - -Implement: - -- `EnvironmentBinding` cache: - object id, backend/provider id, cell/layer id, required variables. -- default environment resolver: - global environment data for non-spatial backends; - object position for spatial backends; - parent position fallback; - global fallback or validation error. -- dirty flags and batched refresh: - `mark_environment_binding_dirty!`; - `update_geometry!(...; invalidate_environment=true)`; - automatic dirty marking on object creation, removal, reparenting, and - environment grid rebuild. -- explanation helper: - `Diagnostics.explain_environment_bindings(sim)`. -- minimal geometry accessors or traits: - `position`, `geometry`, and `bounds`. -- backend protocol: - `Advanced.EnvironmentAPI.bind_environment`, opaque handles, committed/transient `sample`, - `commit_environment!`, and `EnvironmentAPI.update_index!`. -- `Environment(...)` overrides for scenario-specific resolver/backend choices. -- `Environment(; sources=(CO2=:Ca,))` for scenario-specific environment source - remapping without changing model kernels. - -Runtime rule: - -```text -object -> cached binding -> backend cell/layer -> current environment values -``` - -Spatial lookup must happen only during binding refresh, not inside every model -call. - -Acceptance tests: - -- global environment data gives the same values to all objects; -- missing global environment variables fail during environment binding refresh when - the backend can enumerate variables; -- `Environment(; sources=...)` remaps backend variables to model-facing - `environment_inputs_` names and is visible in explanations; -- a model running every two hours over hourly global meteorology receives a - windowed weather sample rather than only the current raw row; -- model `environment_hint` reducers/windows are honored, and an - `Environment(; sources=...)` override changes the source without discarding - the reducer; -- all objects targeted by one application reuse one global weather sample per - application/timestep; -- mock grid backend binds leaves to cells once at initialization; -- moving one leaf marks only that leaf binding dirty and refreshes it before - the next timestep; -- model `environment_inputs_` changes update required variables without recomputing - spatial links unless necessary. -- `commit_environment!(context, accepted_state)` commits mutable microclimate - state back to the active backend. - -## Phase 7: Compiler, Scheduler, And Explanation Cleanup - -Goal: make the unified graph the source of truth. - -Implement: - -- one compiler that builds a global dependency graph over object addresses; -- materialization and multiscale reference wiring as internal carriers; -- object/scope dependency scheduling; -- writer validation through the same graph, including `Updates(...)`; -- model application scheduling from `ModelSpec(...; on=...)` target sets; -- multirate scheduling based on `Dates.Period` values in `ModelSpec(...; every=...)` and - input windows; -- typed compiled bindings that avoid selector resolution in timestep hot loops; -- typed homogeneous execution batches that move dynamic dispatch outside the - per-object inner loop while preserving ordered heterogeneous overrides; -- arbitrary value type preservation through status, input carriers, temporal - storage, and environment samples; -- structured explanation: - `Diagnostics.explain_objects`, `Diagnostics.explain_scopes`, `Diagnostics.explain_bindings`, - `Diagnostics.explain_calls`, `Diagnostics.explain_environment_bindings`, `Diagnostics.explain_schedule`, - `Diagnostics.explain_writers`. - -Acceptance tests: - -- old `MultiScaleModel` examples rewritten with `ModelSpec(...; inputs=...)` produce matching - outputs; -- historical cross-object examples rewritten with `ModelSpec(...; inputs=...)` produce - matching outputs; -- MAESPA hard-call example rewritten with `ModelSpec(...; calls=...)` produces matching - outputs; -- explanation helpers include enough concrete object ids, scales, processes, - and variables for an AI agent to repair bad mappings. -- `Diagnostics.explain_bindings(sim)` reports whether each dependency came from inference, - `dep(model)`, or `ModelSpec`, and reports carrier/copy semantics. -- no selector resolution occurs inside the per-object, per-model timestep loop - for static composite models. -- a warmed homogeneous execution batch performs no allocations beyond model, - output-stream, or backend work requested by the application itself; -- multirate simulations use the same object-address graph as same-rate - simulations. - -## Phase 8: Breaking API Removal And Migration Docs - -Goal: remove the old configuration surface once parity is proven. - -Removed: - -- `MultiScaleModel(...)` as public scenario configuration; -- superseded scenario containers and value-transfer authoring; -- superseded manual-dependency selectors. - -Write migration notes: - -- `MultiScaleModel([:x => [:Leaf => :y]])` -> `inputs=(:x => Many(scale=:Leaf, var=:y),)`; -- cross-object value declarations -> consumer `ModelSpec(...; inputs=...)`; -- manual dependency declarations -> `ModelSpec(...; calls=...)`; -- repeated species assemblies -> `CompositeModelTemplate` plus `ObjectInstance`; -- explicit environment wiring -> environment resolver/binding backend. -- `InputBindings(...)` -> source and temporal policy information inside - `ModelSpec(...; inputs=...)`; -- `MeteoBindings(...)` and `MeteoWindow(...)` -> `Environment(...)` and - environment sampling/window policy; -- `ModelSpec(...; output_routing=...)` -> model-application output policy; -- `PreviousTimeStep(...)` -> temporal policy/cycle-breaking marker in the - unified graph; -- `ScopeModel(...)` -> `ModelSpec(...; on=...)` plus selector scope. - -Regression tests must cover all migrated examples before removal. - -Migration documentation progress: - -- Added `docs/src/migration_composite_model.md` with direct translations for - `MultiScaleModel`, repeated object assemblies, - `TimeStepModel`, `InputBindings`, `MeteoBindings`, `ScopeModel`, and - `SameScale`. -- Documentation navigation and the home page now identify the composite-model/object API - as the target for new multiscale and multi-plant work. -- The documentation home page now uses executable composite-model/object examples as the - primary quickstart. It shows `CompositeModel`, `Object`, `ModelSpec`, `on`, - `inputs`, `every`, automatic same-object binding inference, multi-object - `Many(...)` inputs, and manual `ModelSpec(...; calls=...)` syntax. -- The repository README now mirrors the composite-model/object entry point instead of - teaching `ModelMapping` first. It includes smoke-tested `CompositeModel`/`Object` - quickstart code, `ModelSpec(...; inputs=...)` multi-object coupling, conceptual - `ModelSpec(...; calls=...)` syntax, and links to the migration guide. -- Added `docs/src/composite_model/quickstart.md` as the first native - composite-model/object tutorial page and promoted it in the documentation navigation. - The page contains docs-tested examples for one-object model chaining, - inferred same-object bindings, `OutputRequest` retention, multi-object - `ModelSpec(...; inputs=...)`, `RefVector` carrier explanations, and manual `ModelSpec(...; calls=...)` - syntax. -- The repository agent skill teaches the unified public vocabulary. -- The public API page now starts with curated composite-model/object groups for scenario - construction, selectors, coupling, lifecycle, environment, runtime, and - structured explanations. - -Current removal audit: - -- The unreleased intermediate scenario and runtime subsystem has been deleted, - including its carriers, dependency selectors, target helpers, tests, - examples, and documentation. -- Public manual-call control now uses vector-like `CallTargets`, - `run_call!(context, name)`, `call_targets`, and `run_call!(target)`. -- `RunContext` defines Symbol-named `run_call!` and `call_targets` directly. -- The legacy mapping transforms are removed: - `MultiScaleModel`, `SameScale`, `TimeStepModel`, `InputBindings`, - `MeteoBindings`, `MeteoWindow`, and `ScopeModel` are not retained as - compatibility constructors. -- `ModelMapping` is removed. Retained documentation mentions it only as - historical migration context. -- Historical MTG mapping and mapping-level multirate pages were removed from - the active documentation navigation. A future documentation cleanup can - replace - these historical pages with equivalent composite-model/object tutorials rather than - retaining them as migration reference. -- The model execution page has been rewritten as a composite-model/object-first guide. - It now documents compilation, same-rate reference carriers, temporal - `ModelSpec(...; inputs=...)`, manual `ModelSpec(...; calls=...)`, `Updates(...)`, `ModelSpec(...; every=...)`, - environment binding, output retention, lifecycle cache invalidation, and - compatibility translations from the historical mapping runtime. -- The detailed first simulation tutorial now starts from the composite-model/object API - instead of `ModelMapping`. It introduces model kernels, object status, - compiled applications, inferred same-object bindings, model outputs, and a - short compatibility note for historical mapping examples. -- The quick examples page now uses copy-pasteable composite-model/object examples for - Beer light interception, degree-days/LAI/light coupling, biomass growth, and - retained `OutputRequest` exports. `ModelMapping` appears only in the - compatibility note. -- The standard model coupling, model switching, and coupling more complex - models step-by-step tutorials now teach `CompositeModel`, `Object`, `ModelSpec`, - `on`, `every`, inferred soft `ModelSpec(...; inputs=...)`, and manual - `ModelSpec(...; calls=...)` first. Historical `PlantSimEngine.ModelMapping(...)` appears - only in compatibility notes on those pages. -- The home page has been replaced by native composite-model/object examples. The - repository README has also been replaced by native composite-model/object examples, - and a dedicated composite-model/object quickstart is now available in the main - documentation navigation. -- CompositeModel/object tests cover scheduling, temporal policies, binding inference - and overrides, environment contracts and aggregation, output routing and - application-qualified export, and structured explanations. Legacy mapping - regression tests were removed with the old runtime. -- CompositeModel/object tests now include public environment-contract validation parity for - missing environment variables, explicit `Environment(; sources=...)` - remapping, model-author `environment_hint` source defaults, and validation against - an explicit replacement environment object/backend. -- Test code uses the canonical `ModelSpec(...; every=...)` spelling and composite-model/object - modifiers. Legacy transform tests were removed with the old compatibility - constructors. -- The unified MAESPA path is implemented and tested through `on`, - `inputs`, `calls`, `call_targets`, and `run_call!`. - -## Resolved API Decisions - -- `SceneScope`, `Self`, `Subtree`, `SelfPlant`, `Ancestor`, `Scope`, and - `Relation` are the public topology selector names. Object labels use - `kind=`, `species=`, `scale=`, and `name=` keyword criteria. -- `ModelSpec(...; inputs=...)` is the only scenario-level value-binding - construction form. -- `ModelSpec(...; every=...)` is the canonical timestep configuration. -- `Environment(...)` owns provider/resolver/source configuration. Temporal - value windows belong to the consuming `ModelSpec(...; inputs=...)` selector. -- Object templates own reusable model applications and parameters, not plant - topology construction. They consume explicit object trees or MTGs adapted - through `objects_from_mtg`. -- The old multiscale and mapping-transform implementations have been removed. - -## Completion Evidence - -The requirement-by-requirement evidence and final verification commands are -recorded in `composite_model_completion_audit.md`. diff --git a/docs/src/dev/distributed_output_ownership.md b/docs/src/dev/distributed_output_ownership.md deleted file mode 100644 index ba2e0b7ee..000000000 --- a/docs/src/dev/distributed_output_ownership.md +++ /dev/null @@ -1,330 +0,0 @@ -# Distributed output ownership - -Status: implemented public and compiler contract. - -## Problem - -Most PlantSimEngine applications compute outputs on the same objects on which -they execute. Some scientifically useful models have a different shape: - -- one radiative-transfer model executes once on a complete scene and computes - values for every simulated organ; -- one plant allocation model executes once per plant and computes allocation - and reserve values for many organs; or -- one soil or microclimate model executes on a shared domain and computes local - values for several plant objects. - -These are not hard calls to per-object models. The scene or plant application -owns the computation and cadence, while each destination object owns its local -state value. - -Passing writable `Many(...; from_status=true)` carriers can modify those -values, but it does not declare the producing application as their writer. -Producer inference, scheduling, diagnostics, lifecycle refresh, and output -retention would then lose the scientific ownership of the result. - -## Terms - -- **Execution target:** the object on which an application runs. A scene light - model has one `Scene` execution target. -- **Output destination:** an object whose status owns one output computed by - that application. The same scene light application can have many `Leaf` and - `Internode` destinations. -- **Output binding:** the compiled relationship between one application, - execution target, destination selector, destination object IDs, and declared - output variables. -- **Destination ownership:** the writer relationship - `(destination_object_id, variable) => application_id`. - -Execution targets and output destinations are deliberately distinct. A model -does not need fake per-organ applications merely to publish values computed -elsewhere. - -## Public declaration and scene writer - -The scenario declares named destination groups with `outputs_to`: - -```julia -ModelSpec( - SceneLightModel(solve_light); - name=:scene_light, - on=One(scale=:Scene), - outputs_to=( - organs=OutputTo( - Many( - scale=(:Leaf, :Internode), - within=SceneScope(), - ); - vars=( - incident_par=Default(0.0), - absorbed_par=Default(0.0), - sky_fraction=Required(Float64), - ), - ), - ), -) -``` - -`Default(value)` creates the destination status variable when needed. -`Required(T)` requires it to exist on every selected destination. Only -`coverage=:exact`, the default, is accepted. - -The scene kernel looks up its compiled group and publishes an identified -solver table: - -```julia -PlantSimEngine.@process "scene light" verbose = false - -struct SceneLightModel{F} <: AbstractScene_LightModel - solve::F -end - -PlantSimEngine.inputs_(::SceneLightModel) = NamedTuple() -PlantSimEngine.outputs_(::SceneLightModel) = NamedTuple() - -function PlantSimEngine.run!( - model::SceneLightModel, - status, - environment, - constants, - context, -) - targets = output_targets(context, :organs) - result = model.solve( - runtime_model(context), - environment, - object_ids(targets), - ) - assign_outputs!(targets, result; id=:object_id) - return nothing -end -``` - -`result` may be any Tables.jl-compatible row or column table. Its row order is -independent of selector order because assignment uses `ObjectId` values. - -## `OutputTargets` runtime surface - -`output_targets(context, :organs)` performs a typed lookup on the current -`RunContext`; it does not resolve a selector or rebuild an identity index in -the model call. The group name must be a `Symbol` declared by the current -application. - -The returned `OutputTargets` supports `length`, `isempty`, `eachindex`, and -`object_ids`. Its destination carriers are available only through the explicit -column namespace: - -```julia -targets.columns.incident_par -targets.columns.absorbed_par -``` - -Output variables and compiled-binding metadata are deliberately not forwarded -into the public property namespace: `propertynames(targets)` exposes only -`:columns`. This keeps output names separate from implementation fields. An -output named `columns` remains accessible as `targets.columns.columns`. - -`object_ids(targets)` is an aligned, read-only identity carrier over the -compiled destination IDs. Positions have no botanical meaning. Direct -positional writes to `targets.columns.` are valid when the producing -algorithm already uses this exact identity order; identified external results -should use `assign_outputs!`. - -An `OutputTargets` value belongs to the current model invocation and lifecycle -generation. Kernels obtain it from `RunContext` on every call and do not store -it on their model. - -## Identified assignment - -The Tables.jl path is the general adapter: - -```julia -assign_outputs!(targets, result_table; id=:object_id) -``` - -The lower-level path accepts stable columns directly: - -```julia -assign_outputs!(targets, result_ids, result_columns) -``` - -Here `result_ids` is an `AbstractVector` and `result_columns` is a -`NamedTuple`. The lower-level overload avoids a Tables.jl adapter but uses the -same validation, identity mapping, and assignment implementation. - -Both forms require: - -- one result ID for every current destination; -- no unknown, duplicate, extra, or missing IDs; -- equal lengths for the ID and every declared result column; and -- every variable declared by the `OutputTo` group. - -Additional table or `NamedTuple` columns are metadata and are ignored. They -can carry solver-facing values such as `source_element`, `component_index`, or -geometry provenance without becoming status variables. Conversely, omitting a -declared output is an error even when that destination status already has a -value. - -No subset or retain-last coverage mode exists. An adapter handles filtered, -abscised, dead, or non-geometrized organs deliberately, or the destination -selector excludes them. - -## Identity and permutation cache - -Each compiled output binding stores destination IDs, an ID-to-position index, -and live reference-backed columns. The first assignment validates the result -IDs and compiles either an exact-order marker or a row-to-destination -permutation. - -The runtime cache is keyed by object identity of the result ID column. Reusing -the same ID-column object promises that its IDs and order are immutable; the -cache does not rescan or hash its contents on every timestep. Value columns may -be mutated and reused freely. Replace the ID-column object whenever its IDs or -order changes. - -This contract applies to both public overloads because the Tables.jl path -extracts its ID column before entering the lower-level implementation. Reusing -a table with the same ID carrier reuses the mapping; constructing a new ID -carrier triggers validation and recompilation. - -## Atomic validation and aliasing - -Coverage, column lengths, value conversions, and source/destination aliasing -are checked before any destination status is changed. If mapping validation -fails, the partially reused cache buffers are marked invalid so a later valid -assignment recompiles cleanly. - -A result column may share destination storage only when all three conditions -hold: - -1. the result IDs are already in exact destination order; -2. the source is the same declared output column; and -3. the source and destination use the same complete mapping. - -Permuted views, partially overlapping views, cross-column aliases, and -permuted self-assignment are rejected. Custom array wrappers that can share -storage must implement Julia's `Base.dataids` and `Base.mightalias` contract so -PlantSimEngine can detect that relationship before mutation. - -## Compilation, ownership, and ordinary consumers - -For every output group, compilation resolves: - -1. the producing application and execution object; -2. the destination selector and current destination IDs; -3. declared variables and their `Required` or `Default` initialization; -4. concrete reference-backed destination columns; -5. the destination ID index; and -6. writer ownership for every `(destination_id, variable)` pair. - -Destination status initialization occurs before consumer input compilation. -Writer collisions are rejected unless an existing `Updates(...; after=...)` -declaration establishes intentional ordering. - -A destination model consumes the value through its ordinary input contract: - -```julia -PlantSimEngine.inputs_(::LeafPhotosynthesis) = ( - absorbed_par=Required(Float64), -) - -ModelSpec( - LeafPhotosynthesis(); - name=:leaf_photosynthesis, - on=Many(scale=:Leaf), -) -``` - -Producer inference finds the scene application that owns -`(leaf_id, :absorbed_par)`, binds the leaf status field, and schedules the -scene writer before the leaf consumer. The consumer does not use -`output_targets`, `from_status=true`, a copy model, or an explicit -`after=:scene_light` dependency. - -## Lifecycle - -The scenario application graph and selectors remain immutable while object -membership may change. At a supported lifecycle barrier, PlantSimEngine: - -- rebuilds affected destination memberships and reference columns; -- rebuilds the destination ID index; -- advances the binding membership generation and invalidates result mappings; -- updates writer ownership and consumer scheduling metadata; -- initializes new destination status variables; and -- opens or closes retained streams as requested. - -An empty destination group remains a typed `OutputTargets` group and can gain -members after organ creation. The next model invocation receives the refreshed -view and recompiles its result mapping. Ordinary timesteps do not resolve the -selector or traverse the graph. - -## Output retention and diagnostics - -Retained streams are keyed by producing application, destination object, and -variable. Distributed destinations change which object IDs are enumerated, -not the scientific identity of the producer. - -Use the supported diagnostics rather than inspecting compiled fields: - -- `Diagnostics.explain_output_bindings` shows execution objects, groups, - destination IDs, column carrier types, coverage, and membership generation; -- `Diagnostics.explain_writers` shows the producing application for each - destination variable; -- `Diagnostics.explain_bindings` and `Diagnostics.explain_schedule` show - ordinary consumer coupling and execution order; and -- `Diagnostics.explain_output_retention` shows retained destination streams. - -`outputs=:none` does not allocate destination-history streams. Current values -remain visible through `final_state` because assignment writes directly to -destination statuses. - -## Performance characteristics - -The implementation keeps ordinary applications on their existing path. -Applications without distributed outputs use the empty compiled marker and an -empty output-target tuple; they do not resolve selectors or build assignment -caches. - -For distributed outputs: - -- destination selectors, IDs, indexes, ownership, and columns are compiled - before execution; -- stable ID carriers reuse their exact-order marker or permutation; -- exact-order assignment avoids indexed destination lookup in the write loop; -- homogeneous destination references produce typed `RefVector{T}` columns and - a concrete recursive column loop; -- heterogeneous destination reference types fall back to `ObjectRefVector`, - with conversion against each destination reference; and -- lifecycle changes rebuild only the affected compiled execution targets. - -After warm-up, the stable homogeneous exact-order and permuted columnar paths -can execute without allocations. Heterogeneous destinations preserve correct -per-object types but are an intentionally slower fallback. Benchmark -compilation, lifecycle refresh, steady-state assignment, and output collection -separately when changing these internals. - -## Alternatives rejected - -### A second `Many` of IDs - -This duplicates compiler-owned identity, lets selectors diverge, and treats -identity as a biological input. It also does not solve writer ownership, -scheduling, or retention. - -### Fake per-object applications - -These misrepresent computation and cadence, enlarge the application graph, and -make bookkeeping models appear scientifically meaningful. - -### Public `CallTargets` reuse - -`CallTargets` represents executable callee applications and includes model, -environment, status-view, and hard-call state. Distributed outputs need the -lighter columnar `OutputTargets` view. - -### Per-step table join - -A table join or MTG traversal in every model execution is avoidable work and -makes ordering errors possible. PlantSimEngine compiles the identity mapping -once per stable ID carrier and invalidates it when destination membership -changes. diff --git a/docs/src/dev/maespa_model_handoff.md b/docs/src/dev/maespa_model_handoff.md deleted file mode 100644 index b04d9edf1..000000000 --- a/docs/src/dev/maespa_model_handoff.md +++ /dev/null @@ -1,161 +0,0 @@ -# MAESPA-Style CompositeModel Example Handoff - -The executable acceptance example is `examples/maespa_model_example.jl`, with -focused coverage in `test/test-maespa-model-example.jl`. - -## CompositeModel Shape - -- One `:Scene` object owns canopy microclimate and model-scale fluxes. -- One shared `:Soil` object owns soil water state. -- Species A and B are reusable `CompositeModelTemplate`s mounted as independent - `ObjectInstance`s. -- Each plant instance contains one plant object, one internode object, and its - own leaf objects. -- Species parameters differ while the model application structure is shared. - -Leaf applications use the copied PlantBiophysics subsample models: - -- `Monteith` for `:energy_balance`; -- `Fvcb` for `:photosynthesis`; -- `Tuzet` for `:stomatal_conductance`. - -## Coupling - -The model energy-balance application controls iterative canopy-air, leaf, and -soil calls. `ModelSpec(...; calls=...)` expresses execution ownership only: the scene model -decides when subprocesses run. - -```julia -ModelSpec(scene_model; name=:scene_eb, on=One(scale=:Scene), inputs=(:psi_soil => - One(kind=:soil, scale=:Soil, application=:soil_water, var=:psi_soil),), calls=(:energy_balance => - Many(kind=:plant, scale=:Leaf, process=:energy_balance), - :soil => - One(kind=:soil, scale=:Soil, application=:soil_water),), environment=Environment(provider=:forcing, sink=:canopy), every=Dates.Hour(1)) -``` - -The scene receives above-canopy forcing from the `:forcing` provider and has -`:canopy` as its explicit commit sink. Trial leaf calls use -`run_call!(context, :energy_balance; environment=trial_environment)`, so all hard-called -leaves sample the trial canopy atmosphere through their compiled handles without -committing it. After convergence, the scene commits the accepted canopy -atmosphere with `commit_environment!(context, accepted_environment)` and publishes one -accepted leaf call against that committed environment. - -Scene/soil values are wired declaratively with `ModelSpec(...; inputs=...)`, not by manually -writing another object's status. The soil model receives accepted scene fluxes -through live references: - -```julia -ModelSpec(SoilWater(...); name=:soil_water, on=One(kind=:soil, scale=:Soil), inputs=(:transpiration => - One( - scale=:Scene, - within=SceneScope(), - application=:scene_eb, - var=:scene_transpiration, - ), - :infiltration => - One( - scale=:Scene, - within=SceneScope(), - application=:scene_eb, - var=:scene_infiltration, - ),), every=Dates.Hour(1)) -``` - -This creates a parent-controlled feedback loop: the scene reads mapped -`psi_soil` when it starts its energy-balance solve, computes accepted scene -water fluxes, writes `scene_transpiration` and `scene_infiltration`, then calls -the soil model. The soil call sees those scene values through input carriers -and publishes the updated soil state. If the intended science is an explicit -lag rather than same-step parent control, use `PreviousTimeStep(:psi_soil)` on -the scene input. - -CompositeModel LAI receives live references to every leaf area: - -```julia -ModelSpec(LAIModel(ground_area); name=:lai_dynamic, on=One(scale=:Scene), inputs=(:leaf_areas => Many( - kind=:plant, - scale=:Leaf, - within=SceneScope(), - process=:leaf_state, - var=:leaf_area, - ),), every=Dates.Day(1)) -``` - -The scene energy-balance model uses the same mapping mechanism for leaf-scale -values needed during the hard-call solve. It maps leaf area, leaf carbon, trial -leaf inputs (`Ra_SW_f`, `aPPFD`, `Ψₗ`), and accepted leaf fluxes (`Rn`, `λE`, -`H`, `A`) into scene-level vector inputs. The scene model then writes or reads -those vectors, while the referenced leaf statuses remain the single source of -truth. - -```julia -ModelSpec(scene_model; name=:scene_eb, on=One(scale=:Scene), inputs=(:leaf_areas => Many(kind=:plant, scale=:Leaf, within=SceneScope(), var=:leaf_area), - :leaf_carbon => Many(kind=:plant, scale=:Leaf, within=SceneScope(), var=:leaf_carbon), - :leaf_Ra_SW_f => Many(kind=:plant, scale=:Leaf, within=SceneScope(), var=:Ra_SW_f), - :leaf_aPPFD => Many(kind=:plant, scale=:Leaf, within=SceneScope(), var=:aPPFD), - :Ψₗ => Many(kind=:plant, scale=:Leaf, within=SceneScope(), var=:Ψₗ), - :leaf_rn => Many(kind=:plant, scale=:Leaf, within=SceneScope(), policy=HoldLast(), var=:Rn), - :leaf_lambda_e => Many(kind=:plant, scale=:Leaf, within=SceneScope(), policy=HoldLast(), var=:λE), - :leaf_h => Many(kind=:plant, scale=:Leaf, within=SceneScope(), policy=HoldLast(), var=:H), - :leaf_a => Many(kind=:plant, scale=:Leaf, within=SceneScope(), policy=HoldLast(), var=:A),)) -``` - -`HoldLast()` is intentional for the leaf flux vectors: it asks the compiler for -live references to the current held status values, so the parent scene solve can -iterate hard-call trial states without materializing temporal streams. - -Allocation is plant-local because its leaf selector uses `within=Subtree()`: - -```julia -ModelSpec(allocation; name=:allocation, on=One(scale=:Plant), inputs=(:leaf_carbon => Many(scale=:Leaf, within=Subtree(), var=:leaf_carbon)), every=Dates.Day(1)) -``` - -## Meteorology - -Input meteorology is above-canopy forcing wrapped in a -`MaespaSingleLayerEnvironment`. The backend stores two meteorological states: - -- `forcing`: the above-canopy `Weather`/time series sampled by the scene; -- `canopy`: the mutable canopy `Atmosphere` sampled by every leaf. - -The scene application uses `Environment(provider=:forcing, sink=:canopy)`. -Leaf energy-balance applications use `Environment(provider=:canopy)`. The -one-layer backend does not look at process names, geometry, or cells; all leaves -intentionally sample the same current canopy atmosphere. - -`canopy_air_update(...)` is a plain helper, not a model application. It reads -canopy-scale leaf fluxes aggregated in the scene, computes the MAESPA-style -canopy air update, and returns a new `Atmosphere`. The accepted solution is -committed directly with: - -```julia -commit_environment!(context, accepted_environment) -``` - -CompositeModel status also stores diagnostics for the resulting below-canopy -microclimate: - -- `canopy_tair`; -- `canopy_vpd`; -- `canopy_rh`; -- `canopy_htot`; -- `canopy_gcanop`. - -Trial iterations pass the candidate atmosphere through `run_call!`, preserving -the leaf applications' compiled provider handles. The accepted state is the -only state committed to the mutable environment backend. - -## Acceptance Checks - -The focused test verifies: - -- five leaves across two species and one shared soil object; -- instance membership and mounted application ids; -- model calls to all leaf energy-balance applications and the soil model; -- nested `Monteith -> Fvcb -> Tuzet` call bundles; -- live-reference LAI and plant-local allocation bindings; -- hourly energy balance and daily LAI/allocation schedules; -- exactly one accepted publication per manually called target and timestep; -- finite canopy microclimate, leaf energy, photosynthesis, soil feedback, and - species-specific allocation after a 25-hour run. diff --git a/docs/src/dev/public_api_refinement_completion_audit.md b/docs/src/dev/public_api_refinement_completion_audit.md deleted file mode 100644 index 023b63fb8..000000000 --- a/docs/src/dev/public_api_refinement_completion_audit.md +++ /dev/null @@ -1,43 +0,0 @@ -# Public API Refinement Completion Audit - -This audit records the supported contract and the evidence used to stabilize it. -It complements the [decision record](public_api_refinement_decisions.md) and the -[public symbol inventory](../API/public_symbols.md). - -## Contract evidence - -| Requirement | Supported contract | Evidence | -|:--|:--|:--| -| Public boundary | Composition, model-author, diagnostic, and extension symbols are exported by default; compiler/cache representations live under `PlantSimEngine.Advanced`. | `test-model-api-stabilization.jl` checks the namespace boundary; Documenter's missing-doc check covers exported docstrings. | -| Application identity | Repeated process applications require explicit names. Inputs, calls, outputs, overrides, and `Updates(...; after=...)` use canonical application IDs. Singular process references are rejected; `Many(process=...)` remains an explicit discovery query. | Stabilization, binding-inference, hard-call, output, override, and update tests cover repeated applications and actionable errors. | -| Selector grammar | `Self()` is one object, `Subtree()` is that object plus descendants, `SelfPlant()` is the containing plant, and `SceneScope()` is the model. `One`, `OptionalOne`, and `Many` share the same criteria across targeting, coupling, lookup, and outputs. | Multi-plant selector tests, instance/template tests, lifecycle tests, and XPalm downstream tests. | -| Outputs | `outputs=:none` is the safe default; `:all` and selector-based `OutputRequest`s are explicit. Request names are unique, application identity is preserved, and removed-object history remains collectable. | Output-boundary, runtime-matrix, multirate, lifecycle-history, and allocation tests. | -| Execution ownership | `run!` starts a fresh simulation; `continue!` and `step!` advance its live handle without resetting time, streams, schedules, or environment position. | Split-run equivalence, multirate-boundary, environment-resume, and lifecycle-continuation tests. | -| Construction and initialization | `CompositeModel(models...; status=...)` lowers to ordinary objects and `ModelSpec`s. `Diagnostics.explain_initialization` reports application, object, origin, defaults, expected/provided types, and remedies without running kernels. | Concise/explicit lowering equivalence and initialization report tests. | -| Diagnostics | Supported explanation functions accept `CompositeModel` directly and compiled views where useful; simulation overloads avoid field inspection. Results are structured vectors that can be filtered with ordinary Julia predicates. | Structured explanation assertions throughout the model test matrix and documentation examples. | -| Lifecycle | Registration, MTG growth, removal, reparenting, movement, and geometry updates are the supported mutation paths. Cycle/self-parent failures are atomic; structural and geometry invalidation remain targeted. | Stabilization, unified integration, environment, and lifecycle-output tests. | -| Model-author API | The kernel is `run!(model, status, environment, constants, context)`. Model parameters come from `model`; `runtime_model`, call-target accessors, traits, and lifecycle helpers are the supported context surface. | `test-model-contract.jl`, hard-call tests, growing-plant tutorial, and downstream model suites. | -| Compatibility | `tracked_outputs`, singular scenario `process=` references, output-request `process=`, and process-only overrides are removed. Mapping runtimes are not restored. | Migration guide plus rejection tests. | - -## Validation matrix - -The release gate is: - -1. complete PlantSimEngine package tests, including allocation gates and doctests; -2. a full Documenter build with missing-doc and executable-example checks; -3. full PlantBiophysics and XPalm downstream suites against this checkout; -4. benchmark smoke tests for native, multirate, PlantBiophysics, and XPalm paths; -5. `git diff --check` and searches for transitional spellings outside explicit - migration/history documentation. - -This matrix covers one/many objects, all selector multiplicities, soft inputs, -hard calls, duplicate writers, temporal policies, global/spatial environments, -templates, instances, overrides, lifecycle mutation, generic values, output -retention modes, fresh/continued execution, and homogeneous hot-loop allocation. - -## Deliberate compatibility boundary - -Compiled structs and cache controls are qualified advanced APIs and may evolve. -Direct mutation of `Object` or `CompositeModel` fields is unsupported. Historical -`ModelMapping`, executor, and status-vector runtimes are outside the compatibility -surface and must not be reintroduced. diff --git a/docs/src/dev/public_api_refinement_decisions.md b/docs/src/dev/public_api_refinement_decisions.md deleted file mode 100644 index fbc768925..000000000 --- a/docs/src/dev/public_api_refinement_decisions.md +++ /dev/null @@ -1,105 +0,0 @@ -# Public API refinement decisions - -This decision record defines the target public contract for the CompositeModel/Object -API. The CompositeModel/Object compiler and runtime remain the only supported scenario -runtime. - -## Terminology and identity - -- An **object** is one runtime entity with a stable `ObjectId`. -- A **model** is one scientific implementation of a process. -- A **process** is model metadata and may have several applications. -- An **application** is one named, configured occurrence of a model in a model. -- User declarations that identify a producer, writer, update predecessor, call - target, or output stream use application identity. -- Process queries are discovery filters. They are not substitutes for an - application identifier when more than one application matches. -- Every application receives a deterministic identifier. An explicit - `ModelSpec(...; name=...)` is used verbatim. An unnamed application uses its - process name only when that identifier is unique; repeated unnamed - applications are rejected with instructions to name them. -- Mounted template applications are qualified as - `instance_name__application_name`. - -## Object selectors and scope - -The same `One`, `OptionalOne`, and `Many` selector values are accepted by -application targeting, inputs, calls, object queries, and output requests. - -Scope names have one meaning: - -- `Self()` selects only the current object. -- `Subtree()` selects the current object and all of its descendants. -- `SelfPlant()` selects the current object's plant root and its descendants. -- `Ancestor(...)` selects the matching ancestor's subtree. -- `SceneScope()` searches the whole model. -- `Scope(name)` searches the named object's subtree. -- `Relation(...)` selects objects with the requested topological relationship. - -Selectors that require a current object fail when used without a context. -Cross-object coupling is always visible in the declaration through `Subtree`, -`SelfPlant`, `Ancestor`, `Scope`, `SceneScope`, or `Relation`. - -## Outputs - -`run!` uses an explicit `outputs` keyword: - -```julia -run!(model; outputs=:none) -run!(model; outputs=:all) -run!(model; outputs=request) -run!(model; outputs=requests) -``` - -The default is `outputs=:none`. Temporal dependency streams required by the -runtime are still retained with bounded histories; they are not user-retained -outputs. - -The former `tracked_outputs` keyword has been removed. Use `outputs` directly; -there is no dual spelling. - -An `OutputRequest` contains an object selector, a variable, an optional -application identifier, a unique result name, and optional temporal resampling -policy. `OutputRequest(:Leaf, :x)` remains a convenience spelling that lowers -to `OutputRequest(Many(scale=:Leaf), :x)` during migration. - -## Execution and continuation - -`run!(model; steps=n, ...)` starts a fresh result timeline at step one while -mutating model status. It returns a live `Simulation` execution handle. - -`continue!(simulation; steps=n)` advances that simulation from its current -step, preserving retained streams, temporal dependency history, environment -position, and multirate clock phase. It returns the same simulation. - -`step!(simulation)` is equivalent to `continue!(simulation; steps=1)`. - -Calling `run!` on an already-mutated model intentionally creates a new result -timeline. Users who intend temporal continuation use `continue!`; the distinct -operation prevents an accidental step-index reset. - -Lifecycle mutations between calls to `continue!` are compiled before the next -timestep using the existing targeted invalidation contract. - -## Public namespaces - -The default namespace is organized around: - -- model composition and execution; -- model-author declarations and kernel helpers; -- supported structured explanations; -- documented environment extension interfaces. - -Compiled representation types, cache dirty flags, raw compiler stages, and -low-level invalidation helpers are qualified advanced/internal APIs unless a -documented external extension requires them. Removing an export does not make a -symbol inaccessible through `PlantSimEngine.Symbol`; it removes the accidental -promise that ordinary users should depend on it. - -## Compatibility policy - -- Removed legacy mapping/executor APIs are not restored. -- Superseded CompositeModel/Object spellings are removed rather than retained - as aliases or fallback methods. -- Benchmarks, examples, documentation, PlantBiophysics, and XPalm target the - canonical API. diff --git a/docs/src/dev/release_notes_handoff.md b/docs/src/dev/release_notes_handoff.md deleted file mode 100644 index 9a5b694a9..000000000 --- a/docs/src/dev/release_notes_handoff.md +++ /dev/null @@ -1,521 +0,0 @@ -# Release Notes Handoff - -This page is the persistent release-note source for the composite-model/object redesign -and cleanup branch. Keep it factual: mark what is implemented, what is removed, -and what is only planned. - -## Implemented Breaking Cleanup - -Source details live in `code_cleanup_audit.md`. - -- Removed `ModelList`, `ModelMapping`, `GraphSimulation`, `MultiScaleModel`, - and the separate mapping dependency/runtime stack. Use `CompositeModel`, `Object`, - and model applications. -- Removed direct and batch mapping `run!` methods. -- Removed string scale names. Use symbols, for example `:Leaf`. -- Removed mapping-specific type-promotion configuration. -- Removed `ModelMapping` completely; it is not retained as a qualified - compatibility API. -- Removed old multiscale output indexing helpers. Convert outputs explicitly - before indexing. -- Replaced mapping-specific same-scale rename sentinels with - `inputs=(:local => One(within=Self(), var=:source),)`. -- Removed unused parallel-executor traits after deleting the executor runtime. -- Removed dead mapping-era wrappers and traits: `UninitializedVar`, - `RefVariable`, `TreeAlike`, and `StatusView`. -- Removed the unreleased `CompositeModelTemplate(...; mapping=...)` alias and dead - selector-to-mapping conversion helpers. -- Removed stale `PlantSimEngine.Examples` exports for the deleted - `ToyInternodeEmergence` example. -- Replaced many source-side validation `@assert`s with explicit errors. -- Added `Updates(:var; after=:application)` for ordered duplicate writers. -- Added `runtime_model(runtime)` as the sanctioned live-model accessor for - `RunContext` and `Simulation`; kernels no longer need to inspect - `context.compiled.model`. -- Added `Diagnostics.explain_initialization(model)` with structured `:required`, - `:defaulted`, `:supplied`, `:generated`, `:producer_bound`, and - `:environment_bound` dispositions. -- Added `CompositeModel(model, models...; status=...)` as a thin one-object constructor - that lowers to the normal object and `ModelSpec` representation. -- Calendar-aligned windows remain unsupported. Temporal windows use - duration-based `Dates.Period` semantics. - -## Removed Unreleased Scenario Prototype - -An experimental scenario runtime was developed and replaced on this branch -before release. Its source, tests, examples, and documentation were removed -rather than retained as compatibility code. - -The removed API included `Domain`, `SimulationMapping`, `Route`, -`AllDomains`, and `HardDomains`, together with the domain scheduler, run loops, -route materialization, environment bridge, graph runner, and output publisher. -Because this API was never released, there is no compatibility layer or user -migration path for it. - -The reusable behavior now lives in the composite-model/object runtime: object selectors, -compiled `ModelSpec(...; inputs=...)`, manual `ModelSpec(...; calls=...)`, `Dates`-based scheduling, -environment backends, dynamic object lifecycle handling, and structured -explanations. - -Dynamic MTG growth now has one public high-level operation: `add_organ!`. -An MTG-backed `CompositeModel` retains the accessors and status initializer used during -initial adaptation. By default, `add_organ!` reuses that policy for new nodes, merges -explicit initial values, attaches the resulting `Status`, registers the model -object, and invalidates runtime bindings. `register_object!` remains available -as the low-level registry operation. XPalm and PlantGeom were migrated away -from package-local wrappers that duplicated this lifecycle sequence. - -Topology engines that have already made a new node's attributes authoritative -may call `add_organ!(...; use_status_adapter=false)`. This advanced opt-out -skips only the stored adapter status initializer; PlantSimEngine still copies -the node attributes, applies explicit initial values with the normal -precedence, forces the exact node identity, registers the object, and -invalidates bindings. Callers must not disable the adapter when it contributes -fields that are absent from the new node. - -## Implemented MAESPA-Style Example Changes - -The current `examples/maespa_model_example.jl` is the main executable example -for multi-plant model coupling. - -- Uses copied PlantBiophysics subsample models: - `Monteith`, `Fvcb`, and `Tuzet`. -- Uses two plant instances with different parameters and shared scale names - such as `:Plant` and `:Leaf`. -- Uses a shared soil model. -- Uses `SceneEB` with `ModelSpec(...; calls=(...))` to manually run leaf - `:energy_balance` and soil `:soil_water` targets. -- Ports MAESPA-style canopy air temperature and VPD update through the - `canopy_air_update(...)` helper and `gbcanms`. -- Treats input meteorology as above-canopy forcing, runs trial leaves with - `run_call!(...; environment=trial_state)`, commits accepted canopy - meteorology with `commit_environment!`, and writes - below-canopy microclimate diagnostics to model status fields: - `canopy_tair`, `canopy_vpd`, `canopy_rh`, `canopy_htot`, and - `canopy_gcanop`. -- Adds `LAIModel` and declares plant leaf-area materialization with - `ModelSpec(...; inputs=(...))`. -- Computes plant allocation daily from plant-local `leaf_carbon` vectors. -- Adds `run_call!` for manually executing compiled model call targets. -- Adds model-level `Input(...)` and `Call(...)` dependency defaults through - `dep(model)`, with scenario-level `ModelSpec(...; inputs=...)` and `ModelSpec(...; calls=...)` overriding - those defaults in `ModelSpec`. -- Adds initial registry-backed model selector resolution with - `resolve_object_ids` and `resolve_objects` for global, self-relative, - plant-relative, ancestor-relative, and named-scope object selections. -- Adds `Diagnostics.explain_scopes(model)` for structured scope diagnostics. It reports - the model scope, object subtree scopes, named `Scope(...)` entries, and - scale/kind/species label groups with concrete object ids. -- Selector failures now include context, matched object ids, requested - criteria, available labels, and near-match suggestions. Misspelled labels - such as `scale=:Leef` therefore suggest `:Leaf` instead of returning only a - cardinality count. -- `Relation(...)` now supports `:self`, `:parent`, `:children`, `:ancestors`, - `:descendants`, and `:siblings` in object-relative input, call, and query - selectors. Relation results are compiled to concrete object ids and may be - constrained by an explicit scope. Application targets reject relations - because they have no current object context. -- Selector labels now have one spelling (`scale=:Leaf`, `kind=:plant`, - `species=:oil_palm`, and `name=:leaf_1`); the duplicate `Scale`, `Kind`, and - `Species` wrappers were removed. `Diagnostics.ObjectAddress` preserves all normalized - object, routing, temporal, and status-ordering fields, while positional - topology selectors such as `Relation(:parent)` remain supported. -- Adds the first compiled composite-model/object view with `Advanced.compile_composite_model`, - `Advanced.CompiledCompositeModel`, `Advanced.CompiledModelApplication`, `Advanced.CompiledModelInputBinding`, - `Advanced.CompiledModelCallBinding`, `Diagnostics.explain_applications`, - `Diagnostics.explain_bindings`, and `Diagnostics.explain_calls`. -- The compiled model view resolves `ModelSpec(...; on=...)`, `ModelSpec(...; inputs=...)`, and - `ModelSpec(...; calls=...)` to object ids ahead of runtime, and reports temporal policy, - window, carrier hints, and callee application ids for agent-readable - diagnostics. -- Unscoped composite-model/object dependency selectors now infer scope from the consumer: - model consumers default to `SceneScope()`, while non-model consumers default - to `Self()`. Cross-scope shared dependencies, such as leaf models reading - soil state, should use `within=SceneScope()` explicitly. -- Adds status-backed compiled input carriers for the composite-model/object view: - scalar shared refs, homogeneous `RefVector`s, and `Advanced.ObjectRefVector` fallback - carriers. `Diagnostics.input_carrier`, `Diagnostics.input_value`, and `Diagnostics.has_reference_carrier` expose - them for tests, diagnostics, and future runtime execution. -- Same-rate model inputs are now wired into consumer `Status` references once - during compilation. Scalar and `Many(...)` inputs remain live references, - missing bound input fields are compiler-generated without inventing - canonical values for `Required(T)`, and repeated non-temporal input - materialization is allocation-free. -- Same-rate `ModelSpec(...; inputs=...)` carriers preserve arbitrary concrete value types. - Regression coverage passes a dual-like `BigFloat` wrapper through a typed - `RefVector`, model arithmetic, source mutation, and output publication - without conversion to `Float64`. -- Same-object variable renaming now uses normal `ModelSpec(...; inputs=...)` syntax instead of - `SameScale()`. Renamed inputs share the producer reference and contribute the - expected producer-to-consumer scheduling edge. -- `Diagnostics.explain_bindings` now reports stable carrier kind and copy/reference - semantics, making reference-wired inputs and materialized temporal values - explicit for users and agents. -- Adds model binding cache helpers: - `Advanced.refresh_bindings!`, `Advanced.bindings_dirty`, `Advanced.compiled_bindings`, and - `Advanced.model_revision`. Object registration, removal, and reparenting invalidate - cached compiled bindings before the next refresh. -- Adds composite-model/object environment binding cache helpers: - `Advanced.refresh_environment_bindings!`, `Advanced.compile_environment_bindings`, - `Advanced.CompiledEnvironmentBinding`, `Advanced.CompiledEnvironmentBindings`, - `Advanced.environment_bindings_dirty`, `Advanced.compiled_environment_bindings`, - `Advanced.environment_revision`, and `Diagnostics.explain_environment_bindings`. -- Adds `geometry`, `position`, and `bounds` accessors for model objects/statuses. - Environment binding refreshes call `EnvironmentAPI.update_index!(backend, changed_entities, removed_object_ids)` before - binding objects to backend cells/layers, so spatial backends can precompute - model-wide lookup structures. -- Spatial environment binding now falls back to the nearest ancestor geometry - for objects without their own geometry. Binding explanations expose the - geometry provenance, and moving an ancestor refreshes only descendants that - inherit its geometry. -- Environment binding refresh can now update changed `environment_inputs_` metadata - without repeating spatial indexing or cell lookup when the - application/object/provider/geometry contract is otherwise unchanged. -- `Environment(; sources=(CO2=:Ca,))` now remaps model-facing environment - variables to backend source variables. CompositeModel environment binding refresh - validates missing source variables for enumerable backends such as - `EnvironmentAPI.GlobalConstant`, and explanations expose both `required_inputs` and - `source_inputs`. -- `validate_environment_inputs(model)` and - `validate_environment_inputs(compiled_scene, environment_or_backend)` now validate - composite-model/object environment contracts directly. Missing-variable diagnostics use - model application ids, and validation honors both scenario - `Environment(; sources=...)` remaps and model-author `environment_hint` defaults. -- Object movement now invalidates environment bindings without rebuilding the - structural object/model binding cache. -- Adds public geometry lifecycle helpers: - `update_geometry!(model, object, geometry; invalidate_environment=true)` and - object-scoped `mark_environment_binding_dirty!(model, object)`. They - currently invalidate the model environment binding cache and leave room for - finer-grained dirty tracking later. -- Adds the first composite-model/object runtime with `run!(model; steps=...)`. - It materializes compiled `ModelSpec(...; inputs=...)` carriers, samples bound environment - inputs, and executes generic model kernels on object `Status` values. -- CompositeModel/object compiler now infers simple same-object value bindings from - `inputs_`/`outputs_` when one producer is unambiguous. `Diagnostics.explain_bindings` - reports each binding origin, including `:model_default`, `:model_spec`, and - `:inferred_same_object`. -- Compiled input bindings now validate `ModelSpec(...; inputs=...)` `process=`/`application=` - filters when they are provided, and `Diagnostics.explain_bindings` reports - `source_application_ids`, `process`, and `application`. -- `Advanced.compile_composite_model` now errors for required `inputs_(model)` variables that are - neither bound through `ModelSpec(...; inputs=...)`/inference nor present on the target object - `Status`. -- `Advanced.compile_composite_model` now prepares model-owned status schemas automatically: - model-targeted objects may omit `Status`, declared outputs and `Default` - inputs are inserted from their initial values, and bound `Required` inputs - are installed through their compiled carriers. External unbound `Required` - inputs still need explicit initialization. -- `Advanced.compile_composite_model` now rejects `ModelSpec(...; inputs=...)` entries whose receiving variable is - not declared by the model's `inputs_`, making binding typos explicit at - compile time. -- `Advanced.compile_composite_model` now validates status-backed non-temporal `ModelSpec(...; inputs=...)` - source availability, so bindings that select existing source objects but no - source `Status` reference fail at compile time instead of becoming no-ops. -- CompositeModel/object runtime now publishes model outputs to model-local temporal - streams and resolves temporal `ModelSpec(...; inputs=...)` with `HoldLast`, `Integrate`, - and `Aggregate` policies before consumer execution. -- CompositeModel temporal `ModelSpec(...; inputs=...)` now use producer `output_policy(...)` traits as - the default when the selector omits `policy=...` and resolves to a unique - source application. Explicit selector policies override the trait. -- CompositeModel applications now infer model-author default environment source remaps - from `environment_hint(...).bindings` when the scenario does not provide explicit - environment bindings. Scenario `Environment(; sources=...)` remains the override. -- CompositeModel/object runtime exposes `run_call!(...; environment=trial_state)` - for non-committing trial meteorology and `commit_environment!` for accepted - mutable environment - commits from model kernels. -- CompositeModel/object root applications now honor `ModelSpec(...; every=...)` values backed by - `Dates.Period` scheduling. `Diagnostics.explain_schedule` reports normalized clocks and - whether an application is root-scheduled or manual-call-only. -- CompositeModel/object root applications now also honor `timespec(...)` model traits - when no explicit `ModelSpec(...; every=...)` is provided. Scenario-level `ModelSpec(...; every=...)` - remains the override. -- CompositeModel/object root applications now validate `timestep_hint(...)` required - bounds for clocks derived from the model base step. Hints remain - compatibility constraints, not scheduling overrides. -- CompositeModel/object execution now uses a stable topological application order - compiled from `ModelSpec(...; inputs=...)` producer edges and `Updates(...)` ordering. - Dependencies on manual-call-only applications are redirected to their parent - caller, same-timestep cycles fail during compilation, and - `Diagnostics.explain_schedule` reports `execution_index`. -- `Advanced.CompiledCompositeModel` now pre-indexes input and call bindings by application and - object id. Runtime input materialization and hard-call lookup no longer scan - all model bindings for every object/model invocation. -- `Advanced.CompiledCompositeModel` now pre-indexes applications by application id, removing - application scans from hard-call target resolution and dictionary rebuilding - from ordered execution setup. -- `Advanced.CompiledEnvironmentBindings` now pre-indexes environment bindings by - application and object id, removing the model-wide binding scan from - environment sampling and output scattering. -- Adds `RunContext` and `CallTarget`; composite-model/object models can use - `run_call!(context, :name)` plus `call_targets(context, :name)` for fine-grained manual `ModelSpec(...; calls=...)` - execution. -- Applications selected by `ModelSpec(...; calls=...)` are skipped by the root - `run!(model)` loop and execute only through explicit `run_call!`, preserving - parent-controlled hard-call execution. -- Adds composite-model/object duplicate-writer validation in `Advanced.compile_composite_model`. A variable - may have only one canonical writer per object unless later writers declare - `Updates(:var; after=...)`, where `after` can match a previous application - id/name or process. -- Adds `Diagnostics.explain_writers(compiled)` to report object-variable writer groups, - duplicate writers, and the `Updates(...)` declarations that validate ordered - updates. -- Adds the first reusable object-template path with `CompositeModelTemplate` and - `ObjectInstance`. Templates bundle reusable `ModelSpec`s and default - `kind`/`species` labels; instances mount them inside a named object subtree. -- `CompositeModel(...)` accepts mounted instances whose roots are either owned objects - or references to separately supplied model objects. -- Template applications are scoped to their instance and receive stable - instance-prefixed application ids. Unmodified instances share the template's - model objects, while instance overrides can replace one application by name - or process when the replacement implements the same process. -- Adds `Override(...)` and `ObjectInstance(...; object_overrides=...)` for - exceptional organs. Overrides are resolved during compilation to concrete - object ids without splitting the logical application or changing its - dependency bindings. -- Template models, template parameter metadata, and replacement models are - retained by reference. The runtime does not copy models or mutate fields to - apply parameter overrides. -- Override validation requires the same process and declared status/environment - variable names. Application explanations report model storage, dispatch - mode, overridden object ids, and replacement model types. -- Adds `Diagnostics.explain_instances(model)` and instance membership in - `Diagnostics.explain_objects(model)`. Instance rows expose roots, current object - membership, mounted applications, overrides, template labels, and - reference-based parameter ownership. -- Objects created below a mounted instance inherit missing template `kind` and - `species` labels. Membership explanations use the current topology rather - than a copied instance object list. -- CompositeModel hard calls now support trial microclimate through - `run_call!(context, name; environment=local_state)`, so hard-called - descendants resample the temporary environment through their normal - environment bindings. -- CompositeModel hard calls now default to `publish=false`. Trial calls mutate target - status without publishing temporal samples or environment writes; accepted - states must use `run_call!(target; publish=true)`. Iterative-call tests verify - that several trials followed by one accepted call publish exactly once. -- `Diagnostics.explain_calls(compiled)` now exposes the manual-call publication contract - through `publication_policy`, `default_publish`, and `accepted_publish` - fields. -- `ModelSpec` now keeps provenance for `ModelSpec(...; inputs=...)` and `ModelSpec(...; calls=...)`. - Declarations coming from `dep(model)` are `:model_default`, scenario-level - declarations and overrides are `:model_spec`, and structured explanations - expose these origins for release-note and migration diagnostics. -- Compiled `OptionalOne(...)` inputs and calls now accept zero matches. - Optional inputs keep their declared model default, optional calls return an - empty target collection, and both remain visible in structured explanations. -- Model kernels read their own parameters from the `model` argument. Hard-call - models and targets are available through focused context APIs such as - `call_targets`, `run_call!`, and `runtime_model`. -- CompositeModel duplicate-writer validation now ignores manual-call-only applications - when validating canonical root writers, so hard-dependency children are not - treated as independent root writers for variables they update inside a parent - call stack. -- Adds `build_maespa_scene(...)` and `run_maespa_example(...)`. - This unified composite-model/object MAESPA path uses `CompositeModelTemplate`, - `ObjectInstance`, `on`, `inputs`, `calls`, and - `every=Dates.Period` with two plant species, one shared soil object, - model LAI, and model energy balance. -- `test/test-maespa-model-example.jl` verifies the unified composite-model/object - MAESPA path. -- `run!(model)` now returns a `Simulation` wrapper containing the mutated - model, compiled object bindings, compiled environment bindings, and - model-local temporal output streams. -- Adds model output inspection helpers: - `outputs(sim::Simulation)`, `collect_outputs(sim)`, and - `Diagnostics.explain_outputs(sim)`. These expose object ids, variables, publishing - application ids, sample counts, time bounds, and value types. -- `run!(model; tracked_outputs=...)` now accepts `OutputRequest` for - composite-model/object runs. Requested outputs are collected from retained typed model - streams after the run, can be read with `collect_outputs(sim)` or - `collect_outputs(sim, :request_name)`, support the standard temporal - policies and `Dates.Period` export clocks, and respect dynamic object - lifetimes by exporting each object only across its own sample interval. This - now prunes retained streams at publisher level: `tracked_outputs=nothing` - keeps all streams, explicit requests keep requested application/variable - streams plus streams required by temporal `ModelSpec(...; inputs=...)`, and - `tracked_outputs=OutputRequest[]` keeps no streams unless temporal - dependencies require them. Dependency-only streams now have bounded - policy-specific histories: latest-only for `HoldLast`, the required window - for `Integrate`/`Aggregate`, and sufficient recent source samples for - `Interpolate`/`PreviousTimeStep`. Requested and default retain-all streams - still preserve complete histories, and export remains post-run rather than - fully online. -- Adds `Diagnostics.explain_output_retention(sim)` for structured diagnostics of retained - model output streams, their reasons, and the compiled retention horizon for - dependency-only streams. -- CompositeModel temporal streams are now keyed by application id, object id, and - variable, so two applications can publish the same variable on the same - object without overwriting each other's stream samples. -- CompositeModel output-export tests now cover requested-output `DataFrame` - materialization, canonical publisher inference without `process=...`, - rejection when only stream-only publishers exist, and ambiguity when an - explicit process matches both a stream-only and a canonical publisher. -- `OutputRequest(...)` now accepts `application=...` for composite-model/object runs. - This disambiguates repeated applications of the same process and permits - explicit export of a named `:stream_only` publisher. -- CompositeModel temporal streams now retain a concrete value type per - application/object/output stream. Type changes fail explicitly, while - generic values such as `BigFloat` remain typed through publication, - interpolation, and integration. -- CompositeModel temporal `ModelSpec(...; inputs=...)` now implement the complete `Interpolate(...)` - policy used by the existing multirate runtime: linear interpolation when - samples bracket the requested time, online linear extrapolation from the - last two samples, and configurable hold behavior. Interpolation modes are - validated during model compilation, and arithmetic preserves generic value - types such as `BigFloat` instead of coercing model values to `Float64`. -- CompositeModel `ModelSpec(...; inputs=...)` accepts - `PreviousTimeStep(:input) => One(...)` or `Many(...)` for explicit lagged - dependencies. These bindings read the previous model timestep, use the - consumer status initialization before history exists, and are excluded from - same-timestep dependency edges so feedback loops can be compiled. -- CompositeModel `output_routing=(var=:stream_only,)` is honored by canonical writer - validation and same-object input inference. Stream-only outputs are excluded - from canonical ownership, but remain available in output streams and explicit - `inputs=(... One(application=:name), ...)` bindings. -- CompositeModel execution now refreshes dirty structural bindings between timesteps. - Objects created, removed, or reparented by a model update application target - sets, input carriers, call targets, writer validation, and scheduling before - the next timestep. -- Geometry-only changes refresh environment bindings at the next timestep - without rebuilding structural bindings. `Simulation` returns the final - compiled structural and environment state, including changes made during the - last timestep. -- Geometry-only environment invalidation is now object-scoped. Moving or - explicitly marking one organ dirty preserves unaffected compiled environment - bindings and rebinds only model applications targeting that object; - structural model changes still trigger a full rebuild. -- Added runtime lifecycle coverage for organ creation, pruning, plant-local - `RefVector` refresh, historical output retention for removed objects, and - movement between mock microclimate cells. -- CompositeModel root execution now uses compiled homogeneous target batches. Models, - statuses, input bindings, and environment bindings are - prebound, so dynamic dispatch happens once per batch instead of once per - object. Heterogeneous object overrides split into ordered concrete batches. -- Adds `Diagnostics.explain_execution_plan(scene_or_simulation)` and a zero-allocation - warmed 128-leaf inner-loop regression gate. -- Manual `ModelSpec(...; calls=...)` handles now use the public - vector-like `run_call!(context, name)` execute-all API, with - `call_targets(context, name)` followed by `run_call!(target)` for fine-grained control. -- Removed the unreleased intermediate authoring and runtime subsystem after - composite-model/object feature parity was established. -- Adds `objects_from_mtg(root; ...)` and `CompositeModel(mtg; ...)` so existing MTG - topology can be adapted once into the unified registry while preserving - node-derived identity, parent relations, labels, geometry, and existing - status objects. -- CompositeModel applications now sample global tabular meteorology at their compiled - `Dates.Period` clock. PlantMeteo reducers and windows from `environment_hint` are - honored; `Environment(; sources=...)` overrides the source while preserving - the reducer. Prepared samplers are shared, and one sampled row is cached per - application/timestep for all selected objects. - -## Downstream and Performance Validation - -The first complete remote XPalm performance artifact passed on 31 July 2026 in -[GitHub Actions run 30617676544](https://github.com/VirtualPlantLab/PlantSimEngine.jl/actions/runs/30617676544). -The Ubuntu 24.04 / Julia 1.12.1 single-threaded job resolved the four -path-developed repositories, then passed all four correctness assertions in -the complete 4,160-step profile. The correctness matrix took 12 minutes -10 seconds; the full job, including resolution and precompilation, took -16 minutes 55 seconds. - -The retained artifact is -`xpalm-full-performance-30617676544-1` (artifact id `8788421206`, 90-day -retention). It contains the resolved `Manifest.toml` and 363 CSV measurement -rows covering all 16 stages. Every output-retention mode reached the same -committed final state: step 4,160, 344 phytomers, LAI -`5.0587602356164405`, and FTSW `0.7991179101191218`. - -The CSV records these exact source revisions: - -- PlantSimEngine: `a715e2bf4407870f7dcae5fedaca7eab00f2a826` -- XPalm: `460d3b5161732a195517d5be9a4bdbb4cfc41846` -- PlantBiophysics: `b733e05032cde9b60e527cc2b33a472281c995fb` -- PlantGeom: `f53e5633da1a56d546c4a4433e6d1cd3a898ecf9` - -On that runner, the minimum complete-cycle measurements were 20.255 seconds -with no retained outputs, 22.492 seconds with the small request, 23.246 seconds -with the reference request, 61.324 seconds with all outputs, and 15.650 seconds -for the historical end-to-end helper. These runner-specific measurements are a -persisted comparison baseline, not a replacement for the faster local -acceptance-machine targets. - -## Compatibility Boundary - -The composite-model/object runtime and its MAESPA acceptance path are implemented. -Historical mapping APIs and the unreleased intermediate prototype were -removed. The design, implementation history, and completion evidence are -documented in: - -- `composite_model_design.md` -- `composite_model_implementation_plan.md` -- `composite_model_completion_audit.md` - -The completed public migration is: - -- replace historical tutorials with native composite-model/object tutorials where - long-term coverage is still valuable; -- model mappings should be described as model applications: - `ModelSpec(model; name=..., on=..., inputs=(...), calls=(...))`; -- `MultiScaleModel(...)` -> `ModelSpec(...; inputs=...)`. -- `dep(model)` remains the model-level trait for default dependency intent: - defaults can become `Input(...)` value bindings or `Call(...)` manual model - calls, and scenario-level `ModelSpec` configuration overrides them. -- model target scales -> `ModelSpec(...; on=...)` object selectors. -- `InputBindings(...)` -> source, policy, and window information on - `ModelSpec(...; inputs=...)`. -- `MeteoBindings(...)` and `MeteoWindow(...)` -> automatic environment - binding plus `Environment(...)` provider/source overrides. -- `ModelSpec(...; output_routing=...)` -> model-application output policy. -- `ScopeModel(...)` -> `ModelSpec(...; on=...)` plus selector scopes. -- `PreviousTimeStep(...)` remains supported as a temporal/cycle-breaking - marker in the unified object-address graph. -- explicit per-model environment wiring -> automatic environment resolver plus - cached environment bindings. - -Historical mapping examples, tests, and runtime files were removed after the -composite-model/object acceptance path reached feature parity. Migration information is -kept in this release-note handoff and the user-facing migration guide. - -## Migration Documentation Added - -- Added `docs/src/migration_composite_model.md` as the user-facing migration guide - from historical mappings to the composite-model/object API. -- Updated documentation navigation, home-page guidance, multiscale warnings, - and the canonical repository agent skill to direct new - scenarios toward `CompositeModel`, `Object`, `on`, `inputs`, `calls`, - `Updates`, `every`, and `Environment`. -- Replaced the documentation home-page quickstart with executable - composite-model/object examples. The page now introduces `CompositeModel`, `Object`, - `ModelSpec`, `on`, `inputs`, `every`, inferred same-object - bindings, multi-object `Many(...)` inputs, and manual `ModelSpec(...; calls=...)` syntax - before linking to the migration guide. -- Replaced the repository README examples with composite-model/object-first examples. - The README now introduces `CompositeModel`, `Object`, model applications, - multi-object `ModelSpec(...; inputs=...)`, and `ModelSpec(...; calls=...)`. -- Added a native composite-model/object quickstart page to the main documentation - navigation. It provides docs-tested examples for one-object model chaining, - inferred bindings, requested output retention, multi-object `ModelSpec(...; inputs=...)`, - reference carrier explanations, and manual `ModelSpec(...; calls=...)` syntax. -- Rewrote the model execution page as the current composite-model/object execution - guide. It now covers compilation, reference carriers, temporal `ModelSpec(...; inputs=...)`, - manual `ModelSpec(...; calls=...)`, `Updates(...)`, `ModelSpec(...; every=...)`, environment binding, - retained outputs, lifecycle invalidation, and migration translations for - historical mapping constructs. -- Rewrote the detailed first simulation tutorial to use the composite-model/object API. - It now introduces `CompositeModel`, `Object`, `ModelSpec`, `on`, `every`, - compiled applications, inferred same-object bindings, model outputs, and a - migration note for historical examples. -- Rewrote the quick examples page to use native composite-model/object snippets for - Beer light interception, degree-days/LAI/light coupling, biomass growth, and - retained `OutputRequest` exports. Historical mapping usage is confined to - migration records. -- Rewrote the standard model coupling, model switching, and coupling more - complex models tutorials around the composite-model/object API. These pages now show - inferred same-object value bindings, switching one `ModelSpec` application, - execution-plan explanations, and `ModelSpec(...; calls=...)` manual-call wiring. -- Removed legacy mapping transforms and their runtime implementations: - `MultiScaleModel`, `SameScale`, `TimeStepModel`, `InputBindings`, - `MeteoBindings`, `MeteoWindow`, and `ScopeModel`. -- Added a curated unified composite-model/object map to the public API page. diff --git a/docs/src/developers.md b/docs/src/developers.md index f3dc1a6e5..92591f76c 100644 --- a/docs/src/developers.md +++ b/docs/src/developers.md @@ -63,6 +63,8 @@ written to `docs/build/`. The build also checks that exported pages, assets, and Bonito session data resolve locally. `docs/check_static_export.jl` adjusts Bonito 5.2's site-relative links for this manual's nested pages and for version links in pull-request previews; its assertions flag upstream changes that need review. +`docs/bonito_rendering.jl` preserves code blocks and tables returned by `@eval`; +it defers to Bonito if a later version supplies the missing document-root renderer. Serve the build directory over HTTP to inspect the theme, search, and static examples: @@ -111,6 +113,11 @@ If a change affects public APIs or execution behavior, check both `CI` and `Integration` before merging. Benchmark results are useful for regressions, but should be interpreted alongside the test results. +Downstream tests run the test suites of packages that use PlantSimEngine, such +as PlantBiophysics, against the proposed changes. If you maintain a package that +depends on PlantSimEngine, you can propose adding it to the integration workflow +through a pull request. + ## Graph Viewer Frontend The static viewer and HTTP editor share the React application under @@ -165,6 +172,20 @@ change. ## Documentation impact +The manual has two practical paths: **Couple models** for simulation users and +**Write models** for model authors. Add an example to the appropriate path and +link to deeper reference material only when the reader needs it. Prefer one +tested example that develops gradually over several parallel quickstarts. + +Describe current behavior in the user guides and API reference. Completed work +plans and handoff notes do not belong in the manual; keep lasting explanations +with the feature they describe. + +Documentation fixes are welcome through +[GitHub issues](https://github.com/VirtualPlantLab/PlantSimEngine.jl/issues) or a +pull request. A short report of a confusing example is useful even without a +proposed fix. + Changes in PlantSimEngine often require documentation updates beyond the page you were editing. diff --git a/docs/src/documentation_improvement.md b/docs/src/documentation_improvement.md deleted file mode 100644 index 713c838f7..000000000 --- a/docs/src/documentation_improvement.md +++ /dev/null @@ -1,11 +0,0 @@ -# Help Improve The Documentation - -Good documentation is essential if PlantSimEngine is meant to stay usable for -both new users and contributors. - -If a page is unclear, incomplete, or out of date, the preferred way to report it -is to open an issue or submit a pull request on -[GitHub](https://github.com/VirtualPlantLab/PlantSimEngine.jl/issues). - -Documentation fixes do not need to be large. Small corrections, clarified -examples, and reports of missing release notes are all useful contributions. diff --git a/docs/src/guides/data/environment_inputs.md b/docs/src/guides/data/environment_inputs.md deleted file mode 100644 index abd2c9907..000000000 --- a/docs/src/guides/data/environment_inputs.md +++ /dev/null @@ -1,10 +0,0 @@ -# Weather And Environment Inputs - -An environment may be a constant named tuple, one tabular row, or regular -multi-row weather. Every row in a timed sequence needs a positive fixed -`duration`; inconsistent base durations and application substeps are rejected. - -Use `Environment(sources=...)` to map model-facing environment names to -provider columns. Values retain compatible user numeric types. Spatial -providers implement the same model-facing contract and refresh bindings after -object movement or geometry changes. diff --git a/docs/src/guides/data/forcing_observations.md b/docs/src/guides/data/forcing_observations.md index 0b1448d5b..f44784497 100644 --- a/docs/src/guides/data/forcing_observations.md +++ b/docs/src/guides/data/forcing_observations.md @@ -1,10 +1,82 @@ -# Forcing Observed Variables +# Use Observed Values In A Simulation -Supply a constant observed value in object status when it does not vary. For a -time-varying observation, use a small environment-driven source model that -publishes the canonical variable; downstream applications remain unchanged. +You can supply measured leaf area index (LAI) while calculating light +interception. This is useful for testing the light model independently of a +model that predicts LAI. Here LAI is m² leaf per m² ground, and incident PAR +is an energy flux in W m⁻² ground. -Replace a process for an entire template instance through instance overrides, -or use `Override` for one exceptional object. The replacement must implement -the same process and input/output contract. +## One fixed observation +Supply a constant LAI in status and use only the light model: + +```@example observed_lai +using PlantSimEngine, Dates, DataFrames +using PlantSimEngine.Examples + +fixed_model = CompositeModel( + Beer(0.6); + status=(LAI=2.0,), id=:canopy, scale=:Canopy, + environment=(Ri_PAR_f=100.0, duration=Day(1)), +) +fixed_simulation = run!(fixed_model; outputs=:all) +final_state(fixed_simulation).aPPFD +``` + +Do not add a second model that also writes LAI: its output would replace the +supplied value. A status value is an initial or fixed input, not a rule that +overrides a running producer. + +## A sequence of observations + +For time-varying observations, use a model that reads each observation and +publishes LAI. The small `ObservedLAI` definition below only copies the value; +it performs no fitting, interpolation, or unit conversion. Include it from the +downloadable source to run the example: + +```@example observed_lai +include("observed_lai.jl") + +observations = DataFrame( + duration=fill(Day(1), 3), + measured_LAI=[1.0, 2.0, 3.0], + Ri_PAR_f=fill(100.0, 3), +) +model = CompositeModel( + ObservedLAI(), Beer(0.6); + id=:canopy, scale=:Canopy, environment=observations, +) +simulation = run!(model; steps=nrow(observations), outputs=:all) +rows = collect_outputs(simulation; sink=DataFrame) +filter(row -> row.variable == :LAI || row.variable == :aPPFD, rows) +``` + +```@example observed_lai +@assert final_state(simulation).LAI == 3.0 # hide +@assert count(==(:LAI), rows.variable) == 3 # hide +nothing # hide +``` + +`Beer` receives the published LAI through the usual same-object connection. +To predict LAI instead, replace `ObservedLAI()` with `ToyLAIModel()` and supply +the thermal time that model requires. A change of model can change the inputs +you must provide; [compare alternatives](../../step_by_step/model_switching.md) +before making a replacement. + +## The observation model + +The complete [source file](observed_lai.jl) is short: + +```@eval +Main.DocsSources.section("docs/src/guides/data/observed_lai.jl", "struct ObservedLAI") +``` + +Before using real measurements, check their units, area basis, time stamps, +and missing values. The example has one observation per regular daily row. +For sparse measurements, choose and document an interpolation or holding rule +before supplying the forcing. This workflow prescribes a measured variable; +it is not a data-assimilation method that estimates uncertainty or updates +other state variables. + +See [Collect and plot results](outputs_plotting.md) for displaying predictions +alongside observations and [Parameter fitting](../../working_with_data/fitting.md) +when the objective is to estimate model parameters. diff --git a/docs/src/guides/data/observed_lai.jl b/docs/src/guides/data/observed_lai.jl new file mode 100644 index 000000000..6d45678bd --- /dev/null +++ b/docs/src/guides/data/observed_lai.jl @@ -0,0 +1,11 @@ +# A measured-value alternative to ToyLAIModel, used by forcing_observations.md. +struct ObservedLAI <: PlantSimEngine.Examples.AbstractLai_DynamicModel end + +PlantSimEngine.inputs_(::ObservedLAI) = NamedTuple() +PlantSimEngine.outputs_(::ObservedLAI) = (LAI=0.0,) +PlantSimEngine.environment_inputs_(::ObservedLAI) = (measured_LAI=0.0,) + +function PlantSimEngine.run!(::ObservedLAI, status, environment, constants, context) + status.LAI = environment.measured_LAI + return nothing +end diff --git a/docs/src/guides/data/outputs_plotting.md b/docs/src/guides/data/outputs_plotting.md index 55139df24..93f5b90d6 100644 --- a/docs/src/guides/data/outputs_plotting.md +++ b/docs/src/guides/data/outputs_plotting.md @@ -1,63 +1,139 @@ # Collecting And Plotting Outputs -Run a model with `outputs=:all` or an explicit `OutputRequest`, then call -`collect_outputs(sim)` for ordinary analysis. Rows identify application, -object, variable, timestep/time, and value, so repeated processes cannot -overwrite one another. Convert the rows to a `DataFrame`, filter by -application/object/variable, group, and plot. Use `final_state(sim)` when only -the latest values are needed. - -Runs default to `outputs=:none`. Use `outputs=:all` only when complete stream -history is intentional; selected requests are the memory-safe choice for large -composite models. Raw rows have the stable columns `timestep`, `time`, `application_id`, -`object_id`, `variable`, and `value`. Requested/resampled rows additionally -identify `scale` and `process`. A temporal request emits `missing` when its -policy cannot produce a value for a scheduled output time. -`time` is expressed in model base-step coordinates; application clock metadata -is reported by `Diagnostics.explain_schedule(simulation)`. Values retain their concrete -types, so unit-bearing model outputs remain unit-bearing in collected rows. - -Each publication snapshots its value, including arrays and nested mutable -values. Updating a model's live status later does not rewrite earlier samples. -`final_state` also returns an independent snapshot; use `model_status` when -you intend to access the live state. - -`OutputRequest` controls requested retention or resampling. Dependency streams -may also be retained for runtime correctness. Use -`Diagnostics.explain_output_retention(sim)` to see why each stream exists. Removed objects -retain accepted historical rows. +Plot absorbed light through a day, then compare two canopies with different +leaf area indices. This page uses the existing `Beer` model, so you can focus +on running a simulation and analysing its results. The radiation values are +illustrative, not observations from an experiment. + +The [tutorial installation](../../prerequisites/installing_plantsimengine.md) +includes DataFrames for tables and CairoMakie for plots. + +## Run and plot one canopy + +Supply thirteen hourly PAR fluxes, from 06:00 to 18:00, in W m⁻² of ground. +The canopy has 2 m² of leaves per m² of ground throughout this example. ```@example collect-output -using Dates -using DataFrames -using PlantSimEngine - -PlantSimEngine.@process "docs_output_counter" verbose = false -struct DocsOutputCounter <: AbstractDocs_Output_CounterModel end -PlantSimEngine.inputs_(::DocsOutputCounter) = NamedTuple() -PlantSimEngine.outputs_(::DocsOutputCounter) = (value=0,) -PlantSimEngine.run!(::DocsOutputCounter, status, environment, constants, context) = - (status.value += 1) - -model = CompositeModel(DocsOutputCounter(); environment=(duration=Hour(1),)) -simulation = run!( - model; - steps=3, +using Dates, DataFrames, CairoMakie, PlantSimEngine +using PlantSimEngine.Examples + +incident_par = [0.0, 50, 150, 300, 500, 650, 700, 650, 500, 300, 150, 50, 0] +weather = [(Ri_PAR_f=par, duration=Hour(1)) for par in incident_par] +model = CompositeModel( + Beer(0.6); + id=:canopy, scale=:Canopy, + status=(LAI=2.0,), environment=weather, +) +simulation = run!(model; steps=length(weather), outputs=:all) +rows = collect_outputs(simulation; sink=DataFrame) +light = rows[rows.variable .== :aPPFD, :] +first(light, 3) +``` + +Each row identifies the model application, object, variable, time and value. +`aPPFD` is absorbed PAR in μmol m⁻² of ground s⁻¹. The `time` column uses +model base-step coordinates, with the first record at 1. In this fixed hourly +example, `6 + (time - 1)` gives the hour of day. Use the actual forcing dates +and durations when your data use another time grid. + +```@example collect-output +light.hour_of_day = 6 .+ (light.time .- 1) +figure_one = Figure(size=(740, 380), fontsize=16) +axis_one = Axis(figure_one[1, 1], + xlabel="Hour of day", + ylabel="Absorbed PAR\n(μmol m⁻² ground s⁻¹)", + xticks=6:2:18, +) +scatterlines!(axis_one, light.hour_of_day, light.value; + color=:seagreen, linewidth=2.5, markersize=7) +@assert nrow(light) == length(incident_par) +@assert first(light.value) == last(light.value) == 0.0 +figure_one +``` + +With fixed leaf area, absorption follows the supplied radiation. This curve +shows a mean flux for each hourly record, not a cumulative +amount. To obtain an amount over time, include the represented duration in +the calculation; see [different model cadences](../../journeys/users/cadences.md). + +## Keep two canopies separate + +Apply the same model to two independent canopies. Only their LAI differs. +An application name such as `:light` identifies this configured use of `Beer`. + +```@example collect-output +two_canopies = CompositeModel( + Object(:open_canopy; scale=:Canopy, status=Status(LAI=1.0)), + Object(:dense_canopy; scale=:Canopy, status=Status(LAI=3.0)); + applications=(ModelSpec(Beer(0.6); name=:light, on=Many(scale=:Canopy)),), + environment=weather, +) +comparison = run!(two_canopies; steps=length(weather), outputs=:all) +comparison_rows = collect_outputs(comparison; sink=DataFrame) +comparison_light = comparison_rows[ + (comparison_rows.application_id .== :light) .& (comparison_rows.variable .== :aPPFD), :] +comparison_light.hour_of_day = 6 .+ (comparison_light.time .- 1) + +figure_two = Figure(size=(740, 380), fontsize=16) +axis_two = Axis(figure_two[1, 1], + xlabel="Hour of day", + ylabel="Absorbed PAR\n(μmol m⁻² ground s⁻¹)", + xticks=6:2:18, +) +for series in groupby(comparison_light, [:application_id, :object_id]) + sort!(series, :time) + scatterlines!(axis_two, series.hour_of_day, series.value; + label=string(first(series.object_id)), linewidth=2.5, markersize=6) +end +axislegend(axis_two; position=:lt, framevisible=false) +@assert nrow(comparison_light) == 2 * length(incident_par) +figure_two +``` + +The denser canopy absorbs more PAR per unit ground area under the same +incoming light. Grouping by application and object keeps the two curves +separate. Filtering only by variable and drawing one line would incorrectly +join different objects. + +## Retain only the outputs you need + +`outputs=:all` is convenient for these small examples. Larger simulations +can retain selected variables with `OutputRequest`. Here, the new run keeps +only the light result requested from the two canopies: + +```@example collect-output +selected_simulation = run!( + two_canopies; + steps=length(weather), outputs=OutputRequest( - One(scale=:Scene), - :value; - name=:counter, - application=:docs_output_counter, + Many(scale=:Canopy), :aPPFD; + name=:absorbed_light, application=:light, ), ) -rows = collect_outputs(simulation, :counter; sink=nothing) -table = DataFrame(rows) -@assert table.value == [1, 2, 3] -table +selected = collect_outputs(selected_simulation, :absorbed_light; sink=DataFrame) +@assert nrow(selected) == 2 * length(incident_par) +first(selected, 4) ``` -For plotting, filter the table first and map `time` to the horizontal axis and -`value` to the vertical axis. Group by `application_id` and `object_id` before -drawing lines; grouping by variable alone can accidentally connect different -objects. CairoMakie and other plotting packages consume the resulting columns -without any PlantSimEngine-specific adapter. +A new `run!` starts a fresh timeline. Use `step!` or `continue!` when you want +to extend an existing simulation instead. Runs default to `outputs=:none`; +`final_state(simulation)` remains available when you only need the latest +values. + +Requested output history and internal dependency history have different +purposes. PlantSimEngine may retain an internal stream because another model +needs it, even if you did not request it for analysis. Inspect +`Diagnostics.explain_output_retention(simulation)` when memory use matters. + +## Reading the result tables + +Raw rows have the columns `timestep`, `time`, `application_id`, `object_id`, +`variable` and `value`. Requested or resampled rows also identify `scale` and +`process`. A temporal request can return `missing` when the available history +cannot supply the requested value. Check the model schedule and the start of +the run before interpreting these as gaps in observations. + +Published values, including arrays, are snapshots: subsequent state changes +do not rewrite earlier rows. Removed organs retain their historical outputs. +Values also retain compatible numerical types, including unit-bearing values +when the model produces them. diff --git a/docs/src/guides/modelers/port_existing_model.md b/docs/src/guides/modelers/port_existing_model.md index b12231b21..bc5c2ee49 100644 --- a/docs/src/guides/modelers/port_existing_model.md +++ b/docs/src/guides/modelers/port_existing_model.md @@ -1,147 +1,138 @@ # Port an existing model -Start from the scientific calculation and separate four concerns: immutable -parameters, object state, environment forcing, and produced values. The -`run!` method should remain a readable account of one target and one -timestep, while PlantSimEngine owns object selection, scheduling, and value -transport. +Start from a calculation you already understand and test it before moving it. +Separate fixed parameters, changing object state, environmental inputs, and +the values it produces. The goal is to preserve the equation while making +those roles explicit. -## A complete small kernel +## Begin with an existing calculation -This pedagogical model grows leaf area index from temperature. Its numerical -values illustrate the interface only; they are not a calibrated plant model. +This small teaching function computes a new leaf area index from its current +value and a linear temperature response. The coefficients are arbitrary; +this is not a calibrated growth model. It represents one update over a fixed +interval. ```@example port-existing-model -using Dates -using PlantSimEngine +using Dates, Test, PlantSimEngine -PlantSimEngine.@process "docs_lai_growth" verbose=false +old_lai_step(lai, temperature, response) = lai + response * temperature +expected = old_lai_step(1.0f0, 10.0f0, 0.02f0) +expected +``` + +Before porting a real model, record the units, interval, assumptions, and +expected results from its original implementation. If it returns a rate, +identify where time integration occurs. + +## Map each quantity to its role +| Quantity | Role in this example | Place in PlantSimEngine | +|---|---|---| +| Response coefficient | Fixed parameter for one update | Model field | +| Current LAI | Changing input state, leaf area per ground area | `status.lai` | +| Air temperature | Environmental input, degrees Celsius | `environment.T` | +| New LAI | Result of this update | `status.lai_next` | + +The complete declarations are: + +```@example port-existing-model +@process "docs_lai_growth" verbose=false struct DocsLAIGrowth{T} <: AbstractDocs_Lai_GrowthModel - rate::T + response::T end PlantSimEngine.inputs_(::DocsLAIGrowth) = (lai=Required(Real),) PlantSimEngine.outputs_(model::DocsLAIGrowth) = ( - lai_next=zero(model.rate), + lai_next=zero(model.response), ) PlantSimEngine.environment_inputs_(model::DocsLAIGrowth) = ( - T=zero(model.rate), + T=zero(model.response), ) PlantSimEngine.environment_outputs_(::DocsLAIGrowth) = NamedTuple() +``` + +`Required` means the simulation must supply the current LAI. The output and +environment declarations use the parameter's numerical type. A real model +should reuse its package's existing process where appropriate; see +[New process or new model?](@ref). + +## Preserve the physical meanings +The input and output LAI use the same area basis. Temperature has its own +contract: + +```@example port-existing-model const DOCS_LAI_CONTRACT = VariableContract( - unit=:m2_leaf_per_m2_ground, - basis=:ground, - temporal=:instantaneous, - aggregation=:state, - extent=:intensive, + unit=:m2_leaf_per_m2_ground, basis=:ground, temporal=:instantaneous, + aggregation=:state, extent=:intensive, ) const DOCS_TEMPERATURE_CONTRACT = VariableContract( - unit=:degree_celsius, - basis=:air, - temporal=:instantaneous, - aggregation=:state, - extent=:intensive, + unit=:degree_celsius, basis=:air, temporal=:instantaneous, + aggregation=:state, extent=:intensive, ) - PlantSimEngine.variable_contracts_(::DocsLAIGrowth) = ( lai=DOCS_LAI_CONTRACT, lai_next=DOCS_LAI_CONTRACT, T=DOCS_TEMPERATURE_CONTRACT, ) +``` + +Contracts describe meaning at connections. They do not silently convert a +quantity or establish its scientific validity. + +## Keep the calculation readable +The function now reads from the declared locations and assigns the result: + +```@example port-existing-model function PlantSimEngine.run!( - model::DocsLAIGrowth, - status, - environment, - constants, - context, + model::DocsLAIGrowth, status, environment, constants, context, ) - status.lai_next = status.lai + model.rate * environment.T + status.lai_next = status.lai + model.response * environment.T return nothing end ``` -The struct contains only fixed parameters. Status fields contain values that -can change between timesteps. The environment contains sampled forcing. -`VariableContract` records scientific meaning at coupling boundaries without -wrapping the numerical values. - -Keep parameter and status types generic. `Required(Real)`, -`zero(model.rate)`, and a parametric model field allow compatible values such -as `Float32`, measurements with uncertainty, or automatic-differentiation -numbers. Do not convert inputs to `Float64` inside the kernel. +Use helpers for substantial equations, reused calculations, or numerical +algorithms. Keep a short equation visible rather than splitting every +arithmetic step into a separate function. -## Test before composing - -Call the kernel directly first: +## Compare with the original before composing ```@example port-existing-model growth = DocsLAIGrowth(0.02f0) status = Status(lai=1.0f0, lai_next=0.0f0) -PlantSimEngine.run!( - growth, - status, - (T=10.0f0,), - nothing, - nothing, -) -(lai_next=status.lai_next, value_type=typeof(status.lai_next)) +PlantSimEngine.run!(growth, status, (T=10.0f0,), nothing, nothing) +@test status.lai_next == expected +@test status.lai_next isa Float32 +@test Authoring.validate_model(growth; strict=true).valid +status.lai_next ``` -Then exercise initialization, environment binding, and scheduling through the -ordinary runtime: +Then check the ordinary simulation path with the same inputs: ```@example port-existing-model -model = CompositeModel( +scenario = CompositeModel( growth; status=(lai=1.0f0,), environment=(T=10.0f0, duration=Day(1)), ) - -validation = Authoring.validate_model(growth; strict=true) -@assert validation.valid -initialization = Diagnostics.explain_initialization(model) -simulation = run!(model) -@assert final_state(simulation).lai_next == 1.2f0 -final_state(simulation) +result = final_state(run!(scenario)).lai_next +@test result == expected +result ``` -These levels separate a scientific-equation error from a model-contract error -and a scenario-binding error. Continue with -[Model repository layout and tests](@ref) for the full test pyramid. - -## Keep the scientific narrative visible - -Prefer one continuous kernel that can be read from inputs to outputs. Extract a -helper only when it is: - -- a named scientific equation worth testing or citing separately; -- reused by several model implementations; -- an iterative or numerical algorithm that would obscure the process - equation; or -- a measured optimization whose details should be isolated. - -Do not split every arithmetic line into a helper. Conversely, if users need to -replace a subprocess independently, implement that subprocess as its own -PlantSimEngine model instead of hiding it in a helper or a large -`variant` branch. - -Reset instantaneous outputs before an early return, keep timestep-varying state -out of the model struct, and finish `run!` with `return nothing`. Model code -should not search the object graph or infer its own timestep; declare those -requirements through ports, selectors, hard calls, and traits. - -## Porting checklist - -1. Identify fixed parameters, status inputs, environment inputs, outputs, and - mutable state. -2. Decide whether independently replaceable subprocesses need separate models. -3. Replace implicit defaults and sentinels with `Required(T)` or a - scientifically meaningful `Default(value)`. -4. Declare units, basis, temporal meaning, aggregation, and extent with - `VariableContract` where values are coupled. -5. Convert hidden calls into a declared `Call` only when the parent must own - child execution. -6. Test the kernel, then a minimal composition, before a full scenario. +This example computes a single new value. For repeated updates, decide +explicitly how that value becomes the next input; see +[State, History, And Repeated Updates](@ref). + +## Extend only what your model needs + +Keep time-varying memory in each object's status and fixed parameters in the +model. Preserve compatible numerical types rather than converting everything +to `Float64`. Assign instantaneous outputs on every execution path, +including early returns. + +If users need to replace or schedule a subprocess independently, expose it +as a separate model. Use [Coupling models](@ref) for its connections and +[Model repository layout and tests](@ref) for broader validation. diff --git a/docs/src/guides/modelers/repository_and_tests.md b/docs/src/guides/modelers/repository_and_tests.md index b236eecf7..5587d9b53 100644 --- a/docs/src/guides/modelers/repository_and_tests.md +++ b/docs/src/guides/modelers/repository_and_tests.md @@ -1,11 +1,10 @@ # Model repository layout and tests -A model package should make process ownership, alternative hypotheses, and -validation levels visible from the directory tree. The exact hierarchy may -follow domains or plant scales, but keep one file per concrete model and group -alternatives under their shared process. +Organize a model package so a colleague can find the equation, its alternative +hypotheses, and the evidence used to test it. A small package can start with +one model file and one test file, then grow as needed. -## Recommended layout +## Group alternatives by scientific process ```text src/ @@ -30,87 +29,84 @@ docs/src/models/ └── growth.md ``` -Use `process.jl` only when this package owns the process declaration. If a -dependency already declares the process, import and subtype its abstract model -type rather than declaring a second identity. - -This convention retains the useful pattern from -`PlantBiophysics/src/processes//`, where alternatives such as FvCB and -constantA live beside their shared process. XPalm similarly makes process -identity and alternatives visible through files such as `0-process.jl`, -`rue.jl`/`rue_ftsw.jl`, and `Q10.jl`/`Q10_BP.jl`. Keep the invariant—one shared -process identity and one readable file per hypothesis—but prefer the explicit -name `process.jl` over the historical ordering prefix `0-process.jl`. These -packages are design references, not moving dependencies of this tutorial or -its tests. - -Keep includes and exports explicit in the package module. A process page should -compare the available implementations side by side: assumptions, ports, -`VariableContract`s, parameters, domain of validity, references, and scientific -validation status. Put incomplete experiments outside the public `src/` -include path, or mark their maturity unambiguously. - -Large scenario assemblies deserve named fragments by domain or scale, for -example `leaf_applications(parameters)`, `plant_applications(parameters)`, -and `soil_applications(parameters)`. These functions return ordinary -`ModelSpec` collections; they do not introduce another scenario runtime. - -## Test pyramid - -Test failures are easiest to interpret when each level adds one responsibility: - -1. **Schema and contracts:** inspect `inputs_`, `outputs_`, - `environment_inputs_`, `environment_outputs_`, and `variable_contracts_`. -2. **Direct kernel:** call `run!` on a minimal `Status` and sampled environment. -3. **One-object composition:** check defaults, inferred same-object bindings, - and scheduling. -4. **Explicit coupling:** test renamed, cross-object, `Many`, temporal, and - hard-call inputs used by the model. -5. **Diagnostics:** assert the intended application, source, carrier, call, - writer, and schedule rows. -6. **Scenario regression:** compare a small scientifically interpretable - scenario with a trusted result. -7. **Generic numerics and performance:** exercise supported numeric types and - allocation-sensitive organ loops. - -The first two levels should remain small enough to run after every model edit: +Use `process.jl` only when this package owns the process declaration. +Otherwise import the abstract process type from its owner. Keep one readable +file per hypothesis and make includes and exports explicit in `MyModels.jl`. + +A process page should compare the alternatives: equations, assumptions, +required data, units, parameters, validity domain, references, and validation +status. Label teaching models and unfinished experiments clearly. + +## Test from the equation outward + +Each level answers a different question: + +| Check | Question it answers | +|---|---| +| Direct equation test | Does the implementation reproduce a known calculation? | +| Declaration check | Are required inputs, outputs, and physical meanings explicit? | +| Small composition | Can the model obtain its inputs and run on the intended object? | +| Coupled scenario | Does it interact correctly with the other models used in this study? | + +Start with the canonical biomass example from +[Implement a basic model](@ref): ```@example repository-tests -using Test -using PlantSimEngine -using PlantSimEngine.Examples - -model = ToyDevelopmentModel(0.5f0) -@test keys(PlantSimEngine.inputs_(model)) == (:TT, :stress) -@test keys(PlantSimEngine.outputs_(model)) == (:growth,) -@test isempty(PlantSimEngine.environment_inputs_(model)) -@test isempty(PlantSimEngine.environment_outputs_(model)) - -status = Status(TT=8.0f0, stress=0.75f0, growth=0.0f0) -PlantSimEngine.run!(model, status, nothing, nothing, nothing) -@test status.growth == 3.0f0 -@test status.growth isa Float32 - -composite = CompositeModel(model; status=(TT=8.0f0,)) -simulation = run!(composite) -@test final_state(simulation).growth == 4.0f0 +using Dates, Test, PlantSimEngine +include(joinpath( + pkgdir(PlantSimEngine), "skills", "plantsimengine", + "assets", "minimal-model.jl", +)) +using .MinimalModelExample + +model = RadiationUseEfficiency(1.5f0) +status = Status(intercepted_par=10.0f0, biomass_increment=0.0f0) +PlantSimEngine.run!(model, status, NamedTuple(), nothing, nothing) +@test status.biomass_increment == 15.0f0 +@test status.biomass_increment isa Float32 +@test Authoring.validate_model(model; strict=true).valid + +scenario = CompositeModel( + model; status=(intercepted_par=10.0f0,), timestep=Day(1), +) +@test final_state(run!(scenario)).biomass_increment == 15.0f0 ``` -A green full-scenario test does not replace the kernel test: it usually cannot -show whether an error came from the equation, a contract, object selection, or -the scenario data. Likewise, a direct kernel test does not prove that -cross-object bindings or lifecycle behavior compile correctly. - -## Review checklist for a new hypothesis - -- The concrete type subtypes the intended existing process. -- Parameters are immutable and generic where the mathematics permits. -- Every status and environment read is declared. -- Every written status field is declared as an output. -- Connected scientific variables have complete, matching contracts. -- The main `run!` reads as a continuous scientific calculation. -- Alternative models do not hide behind a mode flag. -- Direct, composition, coupling, and generic-numeric tests cover the model's - actual responsibilities. -- Documentation states what is measured, tested, proposed, or still - scientifically unvalidated. +These small checks isolate the equation from input routing and scheduling. +For a scientific model, add edge cases and a trusted reference calculation +or dataset. Agreement with the reference needs a numerical tolerance +appropriate to the calculation. + +## Add checks for the features you use + +A model with cross-object inputs needs tests that the intended objects supply +them. A model with several cadences needs checks at their update boundaries. +A growing scenario needs checks before and after organs are added or removed. +An iterative controller needs tests of both rejected trials and accepted +publication. + +Use `Diagnostics` to inspect the relevant connections and schedules, then +assert the scientific relationship you intend to preserve. For example, +check that a plant total uses its own leaves and excludes a neighbouring +plant's leaves. + +Exercise the numerical types your model supports. Measure performance where +large organ counts or repeated calls matter; keep such benchmarks separate +from equation checks. See [Numerical Reliability](@ref) and the +[benchmarking guidance](../../developers.md). + +## Review a new hypothesis + +Before sharing a model, check that: + +- it belongs to the intended process; +- its inputs, outputs, units, bases, and timing are documented; +- its equation and fixed parameters are easy to find; +- each object's changing state stays separate; +- direct tests and the relevant coupled tests pass; +- scientific validation and remaining uncertainty are stated. + +When a scenario becomes long, group its application definitions into named +functions by domain, such as leaf, plant, and soil processes. Keep those +functions as ordinary collections of `ModelSpec` values so the scenario +remains inspectable. diff --git a/docs/src/guides/modelers/stateful_models.md b/docs/src/guides/modelers/stateful_models.md index cc6b59ffb..7058b72d2 100644 --- a/docs/src/guides/modelers/stateful_models.md +++ b/docs/src/guides/modelers/stateful_models.md @@ -1,11 +1,151 @@ # State, History, And Repeated Updates -Object `Status` is current state, not timestep storage. Use -`PreviousTimeStep(:x)` for a one-step lag, or keep a model-owned ring buffer -when the algorithm requires deeper history. Accepted output streams provide -simulation history. +A model may need yesterday's value or a short memory of earlier values. +Keep that changing memory in each object's `Status`. The model itself holds +fixed parameters and may be shared by many objects. -Several writers to one canonical variable are rejected unless the application -declares `Updates`. Iterative trial execution belongs to `calls`; use -`publish=false` until a state is accepted. +These are different needs: +| Need | Where it belongs | +|---|---| +| Current state used by the next calculation | The object's `Status` | +| A previous value from a model output | An explicit `PreviousTimeStep` input | +| Internal memory needed by an algorithm | Declared state fields in each object's `Status` | +| A record to plot or analyse after the run | Retained simulation outputs | + +## Example: a mean of three daily observations + +Suppose we want the mean of today's soil-water fraction and the two preceding +daily observations: + +**mean = (today + previous observation + older observation) / 3** + +All three observations are dimensionless fractions. This is a teaching +example of memory ownership, not a model of plant water stress. + +```@example object_memory +using Dates, Test, PlantSimEngine + +@process "docs_three_day_mean" verbose=false +struct DocsThreeDayMean <: AbstractDocs_Three_Day_MeanModel end + +PlantSimEngine.inputs_(::DocsThreeDayMean) = ( + water_fraction=Required(Real), +) +PlantSimEngine.outputs_(::DocsThreeDayMean) = ( + previous_fraction=0.0, + older_fraction=0.0, + mean_fraction=0.0, +) +PlantSimEngine.environment_inputs_(::DocsThreeDayMean) = NamedTuple() +PlantSimEngine.environment_outputs_(::DocsThreeDayMean) = NamedTuple() +``` + +The two memory fields are outputs because this model updates them. Their +initial values must represent observations before the first simulated day. +Each object can supply different initial values. + +```@example object_memory +const FRACTION_SAMPLE = VariableContract( + unit=:fraction, basis=:soil_water, temporal=:instantaneous, + aggregation=:state, extent=:intensive, +) +const FRACTION_MEAN = VariableContract( + unit=:fraction, basis=:soil_water, temporal=:three_daily_samples, + aggregation=:mean, extent=:intensive, +) +PlantSimEngine.variable_contracts_(::DocsThreeDayMean) = ( + water_fraction=FRACTION_SAMPLE, + previous_fraction=FRACTION_SAMPLE, + older_fraction=FRACTION_SAMPLE, + mean_fraction=FRACTION_MEAN, +) + +function PlantSimEngine.run!( + ::DocsThreeDayMean, status, environment, constants, context, +) + status.mean_fraction = + (status.water_fraction + status.previous_fraction + status.older_fraction) / 3 + status.older_fraction = status.previous_fraction + status.previous_fraction = status.water_fraction + return nothing +end +``` + +Calculate the mean before shifting the memory: reversing that order would +use today's observation twice. + +## Check the calculation directly + +```@example object_memory +model = DocsThreeDayMean() +sample = Status( + water_fraction=0.6, previous_fraction=0.3, older_fraction=0.3, + mean_fraction=0.0, +) +PlantSimEngine.run!(model, sample, nothing, nothing, nothing) +@test sample.mean_fraction ≈ 0.4 +@test sample.previous_fraction == 0.6 +@test sample.older_fraction == 0.3 +sample.mean_fraction +``` + +## Keep two objects' histories independent + +Here the observations stay constant during the example. A real scenario +would supply the daily values from forcing data or another model. + +```@example object_memory +dry = Object( + :dry_soil; scale=:Soil, + status=Status(water_fraction=0.6, previous_fraction=0.3, older_fraction=0.3), +) +wet = Object( + :wet_soil; scale=:Soil, + status=Status(water_fraction=0.9, previous_fraction=0.9, older_fraction=0.9), +) +scenario = CompositeModel( + dry, wet; + applications=(ModelSpec(model; name=:water_mean, on=Many(scale=:Soil)),), + environment=(duration=Day(1),), +) +@test Authoring.validate_model(model; strict=true).valid + +simulation = run!(scenario; outputs=:all) +day_one = ( + dry=final_state(simulation, :dry_soil).mean_fraction, + wet=final_state(simulation, :wet_soil).mean_fraction, +) +@test day_one.dry ≈ 0.4 +@test day_one.wet ≈ 0.9 + +step!(simulation) +day_two = ( + dry=final_state(simulation, :dry_soil).mean_fraction, + wet=final_state(simulation, :wet_soil).mean_fraction, +) +@test day_two.dry ≈ 0.5 +@test day_two.wet ≈ 0.9 +(day_one=day_one, day_two=day_two) +``` + +Both objects use the same model. Their separate status fields keep the dry +soil's earlier observations from affecting the wet soil's mean. If an +algorithm needs a mutable history array, each object must own its own array +as well; do not put one shared buffer in the model. + +## Delays, repeated calls, and output history + +Use `PreviousTimeStep` when an input should read a producer's previous +accepted timestep. The [model execution reference](../../model_execution.md) +shows that declaration. [Control Advanced Execution](@ref) explains intentional +multiple writers with `Updates`. + +A call with `publish=false` suppresses publication; it does not automatically +restore every state field the scientific algorithm mutates. An iterative +controller must manage trial state and acceptance deliberately. Advance a +history like the one above once per accepted observation. + +Use [Collecting And Plotting Outputs](@ref) to retain and analyse a result +series. Keeping outputs and maintaining a model's own short memory serve +different purposes. diff --git a/docs/src/guides/multiscale/from_one_object.md b/docs/src/guides/multiscale/from_one_object.md deleted file mode 100644 index 3ebfa4ca1..000000000 --- a/docs/src/guides/multiscale/from_one_object.md +++ /dev/null @@ -1,10 +0,0 @@ -# From One Object To A Multiscale CompositeModel - -First run all models on one object with the concise `CompositeModel(models...; -status=...)` constructor. Then move organ-specific applications to leaf -objects and aggregate their outputs on a plant with `Many(..., -within=SelfPlant())` or an instance-local selector. - -Compare collected, application-qualified outputs with `isapprox`. Exact bitwise -identity is not generally a valid requirement after changing reduction order. - diff --git a/docs/src/guides/multiscale/import_mtg.md b/docs/src/guides/multiscale/import_mtg.md index 70f7e2b2b..0bbda33ba 100644 --- a/docs/src/guides/multiscale/import_mtg.md +++ b/docs/src/guides/multiscale/import_mtg.md @@ -1,10 +1,76 @@ -# Importing An MTG +# Import A Plant From An MTG -`objects_from_mtg(root)` converts MTG topology and labels into ordinary model -objects. `CompositeModel(root; applications=...)` performs the same adaptation and then -uses the normal CompositeModel compiler. The MTG is an input representation, not a -second runtime. +A MultiScaleTreeGraph (MTG) stores a plant's organs and their relationships. +Use it when you already have a measured or generated architecture. The models +and value connections are the same as in [One multiscale plant](../../journeys/users/one_plant.md); +the MTG supplies the objects and their parent links. -For growth, prefer `add_organ!`: it creates the MTG node, applies the model's -status policy by default, attaches status, and registers the corresponding -object. +Install `MultiScaleTreeGraph` in your project to run this example. We create a +small MTG here so no external data file is needed: + +```@example import_mtg +using PlantSimEngine, MultiScaleTreeGraph, DataFrames +using PlantSimEngine.Examples + +root = Node(NodeMTG("/", :Plant, 1, 0)) +leaf_1 = Node(root, NodeMTG("/", :Leaf, 1, 1)) +leaf_2 = Node(root, NodeMTG("/", :Leaf, 2, 1)) +leaf_1[:carbon_biomass] = 50.0 +leaf_2[:carbon_biomass] = 100.0 +nothing # hide +``` + +The `/` links describe leaves as components at a finer scale than the plant; +this reduced architecture omits stems and petioles. For an existing MTG file, replace these lines with +`root = read_mtg("my_plant.mtg")`. Inspect its symbols first: this example uses +`:Plant` and `:Leaf`, but your file may use different names. + +## Choose which attributes become simulation state + +Node IDs and parent links are imported automatically. By default, `scale` +comes from the MTG symbol. Numerical attributes are **not** automatically +copied into model status: supply the values your models need explicitly. + +```@example import_mtg +initial_status(node) = MultiScaleTreeGraph.symbol(node) == :Leaf ? + Status(carbon_biomass=node[:carbon_biomass]) : Status() + +model = CompositeModel( + root; + status=initial_status, + applications=( + ModelSpec(ToyLeafSurfaceModel(0.02); + name=:leaf_surface, on=Many(scale=:Leaf)), + ModelSpec(ToyPlantLeafSurfaceModel(); + name=:plant_surface, on=One(scale=:Plant), + inputs=(leaf_surfaces=Many( + scale=:Leaf, within=Subtree(), + application=:leaf_surface, var=:surface, + ),)), + ), +) + +DataFrames.select(DataFrame(Diagnostics.explain_objects(model)), :id, :scale, :parent) +``` + +The teaching leaf model uses carbon biomass in g C and a specific leaf area +of 0.02 m² per g C. It gives leaf areas of 1 and 2 m², whose sum is 3 m²: + +```@example import_mtg +simulation = run!(model; outputs=:all) +plant_area = final_state(simulation, One(scale=:Plant)).surface +@assert plant_area ≈ 3.0 # hide +plant_area +``` + +## Keep the link to the MTG when the plant grows + +`CompositeModel(root; ...)` retains the MTG adapter. It can resolve a source +node to an object with `object_id(model, leaf_1)`, and `add_organ!` reuses the +chosen status import rule when new organs appear. Required attributes must be +available when that rule runs, or the rule must deliberately initialize them. +See [Growth within a time step](../../tutorials/growing_plant/part1_growth.md). + +Use `objects_from_mtg(root; status=initial_status)` when you only want a +one-time list of `Object`s to assemble yourself. This projection does not keep +the MTG identity index needed for later node lookup or organ creation. diff --git a/docs/src/guides/multiscale/visualizing_structure.md b/docs/src/guides/multiscale/visualizing_structure.md index abb2acf14..62780c5a8 100644 --- a/docs/src/guides/multiscale/visualizing_structure.md +++ b/docs/src/guides/multiscale/visualizing_structure.md @@ -1,6 +1,62 @@ -# Visualizing Composite model Structure +# Visualize Plant Structure -Use `Diagnostics.explain_objects`, `Diagnostics.explain_scopes`, and `Diagnostics.explain_instances` to -obtain stable rows for plotting. Draw nodes by object ID and edges from parent -to child; color by scale, species, or instance. Keep simulation visualization -outside model kernels so model code remains independent of topology packages. +A structure diagram helps check which organs belong to which plant. A model +dependency diagram answers a different question: which calculation supplies +which input. Use the [graph viewer](../graph_visualizer_editor.md) for model +dependencies, and object parent links for plant structure. + +## Draw the parent links + +This small example needs `DataFrames` and `CairoMakie` in your project. It +draws one plant with two leaves from the public object diagnostics: + +```@example structure_plot +using PlantSimEngine, DataFrames, CairoMakie + +model = CompositeModel( + Object(:plant; scale=:Plant, kind=:plant), + Object(:leaf_1; scale=:Leaf, parent=:plant), + Object(:leaf_2; scale=:Leaf, parent=:plant), +) +rows = Diagnostics.explain_objects(model) +select(DataFrame(rows), :id, :scale, :parent) +``` + +The coordinates below are chosen for a readable diagram. They are not organ +positions or a reconstructed 3D plant. + +```@example structure_plot +positions = Dict(:plant => (0.0, 0.0), + :leaf_1 => (-1.0, 1.0), :leaf_2 => (1.0, 1.0)) +fig = Figure(size=(620, 320)) +ax = Axis(fig[1, 1]; title="One plant, two leaves", aspect=DataAspect()) + +for row in rows + x, y = positions[row.id] + if !isnothing(row.parent) + px, py = positions[row.parent] + lines!(ax, [px, x], [py, y]; color=:gray55, linewidth=2) + end + color = row.scale == :Plant ? :sienna : :seagreen + scatter!(ax, [x], [y]; color, markersize=22) + text!(ax, x, y; text=string(row.id), offset=(0, 16), align=(:center, :bottom)) +end +limits!(ax, -1.6, 1.6, -0.3, 1.6) +hidedecorations!(ax) +hidespines!(ax) +save("plant-structure.svg", fig) # hide +nothing # hide +``` + +![](plant-structure.svg) + +For a larger structure, choose a tree-layout algorithm or positions from your +geometry data. Keep object IDs as the link between results, geometry, and +labels. After growth or pruning, call `Diagnostics.explain_objects(model)` +again to draw the current topology; retained simulation outputs still include +the history of removed organs. + +For plant instances, `Diagnostics.explain_instances(model)` identifies their +roots. `Diagnostics.explain_scopes(model)` helps check the groups used by +selectors. Rendering stays outside the process equations, so the same model +can be used with or without a visualization. diff --git a/docs/src/guides/time/advanced_time_environment.md b/docs/src/guides/time/advanced_time_environment.md index 1b1087749..eb4ddf9c6 100644 --- a/docs/src/guides/time/advanced_time_environment.md +++ b/docs/src/guides/time/advanced_time_environment.md @@ -1,17 +1,72 @@ -# Advanced Time And Environment Configuration - -Disambiguate a producer with an explicit selector containing `application`, -`var`, and `within`. Put temporal `policy` and `window` on that input. Configure -environment source renaming and reducers with `Environment`; scenario values -override model-level `environment_hint` entries. - -Use `Diagnostics.explain_bindings`, `Diagnostics.explain_environment_bindings`, and -`Diagnostics.explain_schedule` to inspect the final source, reducer, window, cadence, and -clock origin. All periods that require seconds must be fixed `Dates` periods; -`Month(1)` is intentionally rejected. - -The simulation advances on a fixed base step. `ModelSpec(...; every=...)` -requires a positive integer multiple of that step, including for subsecond -periods. Choose a finer common base step when cadences do not fit: hourly and -90-minute applications need a base step of 30 minutes or finer. The scheduler -does not insert intermediate or adaptive steps automatically. +# Choose Compatible Time Steps + +Use this page when a simulation rejects an application cadence, or when +several models need different time steps. For choosing how values pass between +those models, start with [Different model cadences](../../journeys/users/cadences.md). + +## Find a common base step + +The simulation advances on a fixed base step, supplied by the environment's +`duration`. Each `every` must be a positive integer multiple of that duration. +PlantSimEngine does not insert intermediate steps automatically. + +| Model cadences | A suitable base step | +|:--|:--| +| Every hour and every day | One hour | +| Every hour and every 90 minutes | 30 minutes | +| Every 250 ms and every second | 250 ms | + +A smaller step may also work, but increases the number of simulation steps. +Choose one supported by the equations and by the available forcing data. +Resampling measurements requires an explicit interpolation or aggregation +choice; changing `duration` alone does not create the missing observations. + +## Example: hourly and 90-minute sampling + +Two temperature readers illustrate the schedule without introducing a new +scientific model. Both start at the first time point, then run at their own +cadence: + +```@example compatible_steps +using PlantSimEngine, Dates, DataFrames +using PlantSimEngine.Examples + +model = CompositeModel( + Object(:hourly; scale=:Sensor, name=:hourly), + Object(:ninety_minutes; scale=:Sensor, name=:ninety_minutes); + applications=( + ModelSpec(ToyEnvironmentReaderModel(); name=:hourly, + on=One(name=:hourly), every=Hour(1)), + ModelSpec(ToyEnvironmentReaderModel(); name=:ninety_minutes, + on=One(name=:ninety_minutes), every=Minute(90)), + ), + environment=(T=20.0, duration=Minute(30)), +) +simulation = run!(model; steps=7, outputs=:all) +rows = collect_outputs(simulation; sink=DataFrame) +combine(groupby(rows, :application_id), nrow => :samples) +``` + +The seven base time points cover 0 to 3 hours: the hourly reader publishes +four samples, and the other publishes three. The first sample is at the +origin, not after a completed hour. + +```@example compatible_steps +@assert count(==(:hourly), rows.application_id) == 4 # hide +@assert count(==(:ninety_minutes), rows.application_id) == 3 # hide +nothing # hide +``` + +## Check the resolved configuration + +Use `Diagnostics.explain_schedule(model)` to inspect cadence and clock origin. +Use `Diagnostics.explain_bindings(model)` for the selected producer, temporal +policy, and window, and `Diagnostics.explain_environment_bindings(model)` for +environment sources and reducers. + +`every` overrides a model's default `timespec`. When cadence comes from the +environment base step, a `timestep_hint` can check compatibility; an explicit +`every` is the scenario author's choice and must suit the equations. Fixed +periods such as `Day(1)` are supported. Calendar months have varying lengths, +so `Month(1)` is rejected. Windows are rolling durations, not automatically +aligned civil days or previous complete calendar periods. diff --git a/docs/src/guides/time/hourly_daily_weekly.md b/docs/src/guides/time/hourly_daily_weekly.md index 775241252..1e1080572 100644 --- a/docs/src/guides/time/hourly_daily_weekly.md +++ b/docs/src/guides/time/hourly_daily_weekly.md @@ -1,66 +1,114 @@ # Hourly, Daily, And Weekly Models -An hourly leaf application publishes rates. A daily plant application -integrates them over a day, and a weekly application consumes the daily -amounts. Each application remains a normal `ModelSpec`; only its `every` -value and input policy differ. - -Runtime dependency streams are retained because consumers need them. Output -resampling is independent: create named `OutputRequest`s for hourly, daily, -and weekly analysis, then compare `Diagnostics.explain_schedule` sample counts with rows -from `collect_outputs`. - -The following reduced example checks the important physical contract: two -leaf rates are integrated independently for 24 hourly samples and then summed -on their plant. Rates are per second, so the expected amount is -`(1 + 2) × 24 × 3600 = 259200`. The default `Integrate()` sums samples without -duration weighting; the explicit reducer below supplies the physical integral. +Compare daily and weekly water uptake from the same hourly leaf rates. The +rates below are deliberately constant teaching values: 1 and 2 mg of water +per leaf per second. They demonstrate timing and aggregation, not a model of +plant water demand. + +Each total integrates the hourly rates over its own rolling window, then adds +the two leaf amounts. The daily and weekly applications both read the hourly +source, so the weekly result does not add overlapping daily windows. + +## Load the two equations + +The reusable [teaching models](teaching_models.jl) publish a supplied rate and +sum supplied amounts. Their definitions are included at the end of this page; +you only need to load them to configure this simulation. + +```@example hourly-daily +using Dates, DataFrames, PlantSimEngine +include(joinpath(pkgdir(PlantSimEngine), "docs", "src", "guides", "time", "teaching_models.jl")) +using .TeachingTimeModels +``` + +## Choose the clocks and windows + +`every` says when a model runs; `window` says how much source history its +input uses. Hourly, daily and weekly durations all fit an hourly base step. +The reducer multiplies each rate by its represented duration in seconds, +converting mg s⁻¹ into mg before the plant adds the leaf amounts. ```@example hourly-daily -using Dates -using PlantSimEngine - -PlantSimEngine.@process "docs_hourly_flux" verbose = false -PlantSimEngine.@process "docs_daily_total" verbose = false -struct DocsHourlyFlux <: AbstractDocs_Hourly_FluxModel end -struct DocsDailyTotal <: AbstractDocs_Daily_TotalModel end -PlantSimEngine.inputs_(::DocsHourlyFlux) = (rate=Required(Float64),) -PlantSimEngine.outputs_(::DocsHourlyFlux) = (flux=0.0,) -PlantSimEngine.run!(::DocsHourlyFlux, status, environment, constants, context) = - (status.flux = status.rate) -PlantSimEngine.inputs_(::DocsDailyTotal) = (fluxes=Required(Vector{Float64}),) -PlantSimEngine.outputs_(::DocsDailyTotal) = (total=0.0,) -PlantSimEngine.run!(::DocsDailyTotal, status, environment, constants, context) = - (status.total = sum(status.fluxes)) +integrate_rate = Integrate((values, durations_seconds) -> sum(values .* durations_seconds)) model = CompositeModel( Object(:plant; scale=:Plant), - Object(:leaf_1; scale=:Leaf, parent=:plant, status=Status(rate=1.0)), - Object(:leaf_2; scale=:Leaf, parent=:plant, status=Status(rate=2.0)); + Object(:leaf_1; scale=:Leaf, parent=:plant, status=Status(rate_mg_s=1.0)), + Object(:leaf_2; scale=:Leaf, parent=:plant, status=Status(rate_mg_s=2.0)); applications=( - ModelSpec(DocsHourlyFlux(); name=:hourly, on=Many(scale=:Leaf), every=Hour(1)), + ModelSpec(HourlyWaterRate(); name=:hourly, on=Many(scale=:Leaf), every=Hour(1)), ModelSpec( - DocsDailyTotal(); - name=:daily, - on=One(scale=:Plant), - inputs=( - :fluxes => Many( - scale=:Leaf, within=Subtree(), application=:hourly, var=:flux, - policy=Integrate((values, durations_seconds) -> sum(values .* durations_seconds)), window=Day(1), - ), - ), + SumWaterAmounts(); name=:daily, on=One(scale=:Plant), + inputs=(amounts_mg=Many( + scale=:Leaf, within=Subtree(), application=:hourly, + var=:water_rate_mg_s, policy=integrate_rate, window=Day(1), + ),), every=Day(1), ), + ModelSpec( + WeeklyWaterAmount(); name=:weekly, on=One(scale=:Plant), + inputs=(amounts_mg=Many( + scale=:Leaf, within=Subtree(), application=:hourly, + var=:water_rate_mg_s, policy=integrate_rate, window=Week(1), + ),), + every=Week(1), + ), ), - environment=[(duration=Hour(1),) for _ in 1:25], + environment=[(duration=Hour(1),) for _ in 1:169], ) -simulation = run!(model; steps=25) -@assert only(object.status.total for object in model_objects(model) - if object.id == ObjectId(:plant)) == 259200.0 + +simulation = run!(model; steps=169, outputs=:all) +rows = collect_outputs(simulation; sink=DataFrame) +totals = rows[in.(rows.application_id, Ref((:daily, :weekly))), + [:timestep, :application_id, :variable, :value]] +totals ``` -A weekly consumer uses the same pattern with `every=Week(1)` and a -seven-day window over the daily application. Sum daily amounts with -`Integrate()`; integrate rates per second with the duration-aware reducer -shown above. Choose `Aggregate(reducer)` for states or observations whose physical meaning -is a mean, minimum, maximum, or custom statistic. +The first execution is at step 1. Both totals initially contain only one +hour of available history: `(1 + 2) × 3600 = 10800 mg`. That is a startup +value, not a complete day or week. The daily application runs again at steps +25, 49, and so on; the weekly application runs again at step 169. + +```@example hourly-daily +first_daily = only(totals[(totals.application_id .== :daily) .& (totals.timestep .== 1), :value]) +full_day = only(totals[(totals.application_id .== :daily) .& (totals.timestep .== 25), :value]) +full_week = only(totals[(totals.application_id .== :weekly) .& (totals.timestep .== 169), :value]) +@assert first_daily == 3 * 3600 +@assert full_day == 3 * 24 * 3600 +@assert full_week == 3 * 7 * 24 * 3600 +(startup_mg=first_daily, daily_mg=full_day, weekly_mg=full_week) +``` + +These are fixed-duration rolling windows, not calendar-aligned civil days or +weeks. All application durations must be integer multiples of the base step; +choose a finer common step if necessary. Calendar months and adaptive steps +are not supported by this scheduler. + +`Integrate()` without a reducer only sums samples. Use it for values that are +already the amounts you intend to add, after checking the selected window and +sample boundaries. Rates require duration weighting as above. Use +`Aggregate(reducer)` when the intended result is a mean or another statistic. + +## Scientific contracts and outputs + +The teaching models leave variable contracts undeclared so this page can +isolate the numeric time policies. These policies do not change contract +metadata. A production model with declared rate and amount contracts needs +an explicit conversion model between those contracts; see +[Coupling models](../coupling.md). + +Input windows retain the history needed by the simulation. Your analysis +outputs are a separate choice: see [Collecting And Plotting Outputs](../data/outputs_plotting.md) +for retaining selected results. Inspect the schedule when checking how many +daily or weekly publications to expect, including startup. + +## Teaching model source + +These simple equations are shared with the [cadence tutorial](../../journeys/users/cadences.md). +They copy each leaf's supplied rate and sum the converted leaf amounts. The +weekly model uses a separate output name so both totals coexist on the plant. + +```@eval +using Markdown, PlantSimEngine +Markdown.MD([Markdown.Code("julia", read(joinpath(pkgdir(PlantSimEngine), "docs", "src", "guides", "time", "teaching_models.jl"), String))]) +``` diff --git a/docs/src/guides/time/multirate_concepts.md b/docs/src/guides/time/multirate_concepts.md deleted file mode 100644 index 40f5fe700..000000000 --- a/docs/src/guides/time/multirate_concepts.md +++ /dev/null @@ -1,13 +0,0 @@ -# Understanding Model Cadence - -`ModelSpec(...; every=Dates.Period)` sets application cadence. Without -`every`, an application uses `timespec(model)` when non-default and otherwise -the environment base step. `timestep_hint` validates a scientifically -acceptable range; it does not silently change cadence. - -Temporal input policies have different physical meanings: `HoldLast` samples -the latest state, `Aggregate` reduces observations, and `Integrate()` sums -values over a window. To integrate rates per second, use a duration-aware -reducer: `Integrate((values, durations_seconds) -> sum(values .* durations_seconds))`. `PreviousTimeStep` deliberately breaks a same-step -cycle. Windows are rolling fixed-duration windows. Civil-day or -previous-complete-calendar-period alignment is not a supported public feature. diff --git a/docs/src/guides/time/teaching_models.jl b/docs/src/guides/time/teaching_models.jl new file mode 100644 index 000000000..abbd73513 --- /dev/null +++ b/docs/src/guides/time/teaching_models.jl @@ -0,0 +1,48 @@ +module TeachingTimeModels + +using PlantSimEngine + +export HourlyWaterRate, SumWaterAmounts, WeeklyWaterAmount + +PlantSimEngine.@process "teaching_water_rate" verbose=false +PlantSimEngine.@process "teaching_water_amount" verbose=false +PlantSimEngine.@process "teaching_weekly_water_amount" verbose=false + +"""Publish a supplied constant uptake rate, in mg of water per leaf per second.""" +struct HourlyWaterRate <: AbstractTeaching_Water_RateModel end + +PlantSimEngine.inputs_(::HourlyWaterRate) = (rate_mg_s=Required(Real),) +PlantSimEngine.outputs_(::HourlyWaterRate) = (water_rate_mg_s=0.0,) + +function PlantSimEngine.run!(::HourlyWaterRate, status, environment, constants, context) + status.water_rate_mg_s = status.rate_mg_s + return nothing +end + +"""Add already converted water amounts, in mg, from the selected sources.""" +struct SumWaterAmounts <: AbstractTeaching_Water_AmountModel end + +PlantSimEngine.inputs_(::SumWaterAmounts) = (amounts_mg=Required(AbstractVector{<:Real}),) +PlantSimEngine.outputs_(::SumWaterAmounts) = (water_amount_mg=0.0,) + +function PlantSimEngine.run!(::SumWaterAmounts, status, environment, constants, context) + status.water_amount_mg = sum(status.amounts_mg) + return nothing +end + +"""Keep a separately named weekly total, in mg of water per plant.""" +struct WeeklyWaterAmount <: AbstractTeaching_Weekly_Water_AmountModel end + +PlantSimEngine.inputs_(::WeeklyWaterAmount) = (amounts_mg=Required(AbstractVector{<:Real}),) +PlantSimEngine.outputs_(::WeeklyWaterAmount) = (weekly_water_mg=0.0,) + +function PlantSimEngine.run!(::WeeklyWaterAmount, status, environment, constants, context) + status.weekly_water_mg = sum(status.amounts_mg) + return nothing +end + +# These constant-rate teaching models isolate temporal sampling. Their variable +# contracts are deliberately undeclared: the pages explain the explicit adapter +# required when a contracted rate is converted to a contracted amount. + +end # module diff --git a/docs/src/index.md b/docs/src/index.md index 6b728f35e..14a2c914b 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -184,12 +184,8 @@ thermal time, thermal time drives LAI, and LAI and radiation determine absorbed photosynthetically active radiation (PAR). The phenology and LAI models are teaching examples, not a calibrated crop model. -First install PlantSimEngine and the packages used to read weather and results: - -```julia -using Pkg -Pkg.add(["PlantSimEngine", "PlantMeteo", "DataFrames"]) -``` +First follow the [installation instructions](prerequisites/installing_plantsimengine.md) +to get the package version used by this manual and the tutorial dependencies. Read the weather supplied with PlantSimEngine. This file stores daily radiation totals in MJ m⁻² d⁻¹; the three conversions below give the mean fluxes in diff --git a/docs/src/journeys/modelers/basic_model.md b/docs/src/journeys/modelers/basic_model.md index 27919ac95..3dc9ae82f 100644 --- a/docs/src/journeys/modelers/basic_model.md +++ b/docs/src/journeys/modelers/basic_model.md @@ -1,166 +1,159 @@ # Implement a basic model -This teaching example turns a simple biomass-growth equation into a reusable -model. A **process** is a scientific calculation; a model implements one choice -of equations for that process. You will declare the equation's inputs, outputs, -and units, test it on its own, then run it in a simulation. +This teaching example describes daily biomass production as: -The example is one hypothesis for the `biomass_production` process. Its -coefficients are pedagogical, not a calibrated crop model. Before adding your -own model, follow [New process or new model?](@ref) to decide whether it answers -an existing scientific question or introduces a new process. +**biomass increment = radiation-use efficiency × intercepted radiation** -## Read the complete model +For an efficiency of 1.5 g dry matter per mol of photons and 10 mol of +intercepted photons per plant, the result is 15 g dry matter per plant. +These values illustrate the interface; they are not a calibrated crop model. -This is the complete, copyable source shipped with the package. The model -declaration comes first; `direct_example` tests the equation on its own and -`single_object_scenario` runs it on one simulated entity, called an **object**. +We will give that equation a name, declare its variables and units, test it, +and run it on two plants. Before adding your own model, use +[New process or new model?](@ref) to choose its process. + +## Name the process and its model + +A **process** identifies the scientific calculation. A **model** implements +one hypothesis for it. This example declares a biomass-production process: ```@eval -using Markdown, PlantSimEngine -source = read(joinpath( - pkgdir(PlantSimEngine), "skills", "plantsimengine", "assets", "minimal-model.jl", -), String) -Markdown.MD([Markdown.Code("julia", source)]) +Main.DocsSources.section( + "skills/plantsimengine/assets/minimal-model.jl", + "PlantSimEngine.@process", "const INTERCEPTED_PAR_CONTRACT", +) ``` -## Load and inspect it +The model stores one fixed parameter, `rue`. The `{T}` allows its numerical +type to follow the supplied parameter. -The documentation and agent skill use the same executable source instead of -maintaining two copies: +```@eval +Main.DocsSources.section( + "skills/plantsimengine/assets/minimal-model.jl", + "struct RadiationUseEfficiency", "PlantSimEngine.inputs_", +) +``` -```@example modeler_basic -using Dates, PlantSimEngine - -asset = joinpath( - pkgdir(PlantSimEngine), - "skills", - "plantsimengine", - "assets", - "minimal-model.jl", +## Declare the values the equation uses + +| Value | Role | Meaning | +|---|---|---| +| `rue` | Fixed parameter | g dry matter per mol intercepted photons | +| `intercepted_par` | Input | Daily intercepted photons, mol per plant | +| `biomass_increment` | Output | Daily biomass production, g dry matter per plant | + +`Required(Real)` says the simulation must supply a real-valued input. +`zero(model.rue)` initializes the output with the parameter's numerical type. +This model reads no environmental variables directly. + +```@eval +Main.DocsSources.section( + "skills/plantsimengine/assets/minimal-model.jl", + "PlantSimEngine.inputs_(::RadiationUseEfficiency)", + "PlantSimEngine.variable_contracts_", ) -include(asset) -using .MinimalModelExample +``` + +A `VariableContract` states the physical meaning of a value at a connection +between models. Here both quantities are daily totals for one plant: -boundary = RadiationUseEfficiency(1.5f0) -description = Authoring.describe_model(boundary) -validation = Authoring.validate_model(boundary; strict=true) -( - runtime_process=process(boundary), - described_process=description.process, - inputs=inputs(boundary), - outputs=outputs(boundary), - contracts=variable_contracts(boundary), - description_provenance=description.provenance, - field_provenance=description.field_provenance, - structurally_valid=validation.valid, +```@eval +Main.DocsSources.section( + "skills/plantsimengine/assets/minimal-model.jl", + "const INTERCEPTED_PAR_CONTRACT", "\"\"\"", ) ``` -The loaded declaration is deliberately short. `RadiationUseEfficiency{T}` -stores `rue`; its schemas declare `intercepted_par` and -`biomass_increment`, plus explicitly empty environment inputs and outputs. The -two ports receive complete contracts, and the complete kernel equation is -`status.biomass_increment = model.rue * status.intercepted_par` followed by -`return nothing`. This description is derived from the asset included above, -so the tutorial does not maintain a second untested copy of its source. +Attach those descriptions to the corresponding variables: -The model struct stores only the fixed radiation-use-efficiency parameter. -`Required(Real)` is a type requirement, not an initial value. The output -initial value follows the parameter's numeric type. The complete -`VariableContract` declarations state that intercepted radiation and biomass -increment are daily, plant-scale totals with explicit units. +```@eval +Main.DocsSources.section( + "skills/plantsimengine/assets/minimal-model.jl", + "PlantSimEngine.variable_contracts_(::RadiationUseEfficiency)", + "PlantSimEngine.Authoring.model_metadata", +) +``` -The kernel reads as the scientific calculation from input to output. It does -not select objects, find producers, choose a cadence, or retain output rows; -those are scenario responsibilities. +Matching contracts help check connections. Converting between physical bases +or units requires an explicit [adapter model](../../guides/coupling.md). -## Test the kernel directly +## Write the equation -Use a minimal `Status` before involving the compiler: +The `run!` function reads the parameter from `model`, reads the input from +`status`, and writes its result back to `status`: -```@example modeler_basic -direct = direct_example(Float32) -( - biomass_increment=direct.biomass_increment, - value_type=typeof(direct.biomass_increment), +```@eval +Main.DocsSources.section( + "skills/plantsimengine/assets/minimal-model.jl", + "function PlantSimEngine.run!(", "\"\"\"Run the kernel directly", ) ``` -This test isolates the equation and proves that the fixture preserves -`Float32`. Model packages should also test edge cases and supported enriched -number types. +The remaining arguments carry environmental forcing, constants, and execution +context. This simple equation does not need them. Object selection and +simulation timing belong in the scenario. -## Compose it on one object +## Test one calculation -The asset provides the smallest full scenario with the required input supplied -as initial status: +These displayed definitions come from the package's executable +`skills/plantsimengine/assets/minimal-model.jl` example. To load the complete +example in your session: ```@example modeler_basic -one_object = single_object_scenario(Float32) -( - initialization=Diagnostics.explain_initialization(one_object), - final=final_state(run!(one_object)), -) +using Dates, Test, PlantSimEngine +asset = joinpath(pkgdir(PlantSimEngine), "skills", "plantsimengine", "assets", "minimal-model.jl") +include(asset) +using .MinimalModelExample + +model = RadiationUseEfficiency(1.5f0) +status = Status(intercepted_par=10.0f0, biomass_increment=0.0f0) +PlantSimEngine.run!(model, status, NamedTuple(), nothing, nothing) + +@test status.biomass_increment == 15.0f0 +@test status.biomass_increment isa Float32 +status.biomass_increment +``` + +The `f0` notation chooses `Float32`. The tests check the equation and that +the implementation preserves this numerical type. Also check the declarations: + +```@example modeler_basic +validation = Authoring.validate_model(model; strict=true) +@test validation.valid +validation.valid ``` -`Diagnostics.explain_initialization` distinguishes supplied inputs, model -defaults, produced outputs, environment bindings, and unresolved requirements. -Inspect this report before running a larger scenario. +That check validates the interface. Scientific validation needs appropriate +observations or reference results. -## Reuse the same kernel over several objects +## Run the model on two plants -Object selection remains outside the model. Change only the application -multiplicity and provide each object with initial radiation: +Each plant has its own intercepted radiation. The same model applies to both: ```@example modeler_basic -development = RadiationUseEfficiency(1.5f0) -several_objects = CompositeModel( - Object( - :plant_1; - scale=:Plant, - status=Status(intercepted_par=10.0f0), - ), - Object( - :plant_2; - scale=:Plant, - status=Status(intercepted_par=6.0f0), - ); +plants = CompositeModel( + Object(:plant_1; scale=:Plant, status=Status(intercepted_par=10.0f0)), + Object(:plant_2; scale=:Plant, status=Status(intercepted_par=6.0f0)); applications=( - ModelSpec( - development; - name=:biomass_production, - on=Many(scale=:Plant), - ), + ModelSpec(model; name=:biomass_production, on=Many(scale=:Plant)), ), + environment=(duration=Day(1),), ) -final_state(run!(several_objects), Many(scale=:Plant)) +simulation = run!(plants) +result_1 = final_state(simulation, :plant_1).biomass_increment +result_2 = final_state(simulation, :plant_2).biomass_increment +@test (result_1, result_2) == (15.0f0, 9.0f0) +(plant_1=result_1, plant_2=result_2) ``` -Do not loop over objects inside `RadiationUseEfficiency.run!`. -PlantSimEngine compiles homogeneous targets and invokes the one-target kernel -for each selected object. - -## Continue the authoring path - -- [Port an existing model](@ref) explains the readable-kernel convention. -- [Model repository layout and tests](@ref) shows where to place alternatives, - documentation, and each test level. -- [Implement Cross-Object Values](@ref) adds `One`, `Many`, and `Subtree()` - bindings. -- [Coupling models](@ref) explains when `OptionalOne` is valid and - distinguishes value coupling, hard calls, and explicit physical adapters. -- [Model compatibility and replacement](@ref) checks whether another - hypothesis is genuinely substitutable. - -## Model-author recap - -- **You implemented:** one immutable parameter type, declared ports, complete - scientific contracts, and a continuous one-target kernel. -- **PlantSimEngine inferred:** initialization, target execution, and generic - status construction. -- **The scenario author keeps explicit:** object identities, target - multiplicity, initial radiation, cadence, and output retention. -- **New API names:** `AbstractModel`, `inputs_`, `outputs_`, - `variable_contracts_`, `VariableContract`, `Required`, `Status`, and `run!`. +PlantSimEngine calls the equation for each selected plant. Its state remains +separate, and the model contains no loop over plants. + +## Continue with your own model + +- [Port an existing model](@ref): separate a calculation from its original script. +- [Model repository layout and tests](@ref): organize a package and its checks. +- [Implement Cross-Object Values](@ref): read another object's result or sum several. +- [Model compatibility and replacement](@ref): compare another hypothesis. +- [Loaded model catalog](@ref): discover and inspect models already loaded. diff --git a/docs/src/journeys/modelers/cross_object_values.md b/docs/src/journeys/modelers/cross_object_values.md index e14cd548c..a2a2d874b 100644 --- a/docs/src/journeys/modelers/cross_object_values.md +++ b/docs/src/journeys/modelers/cross_object_values.md @@ -1,21 +1,34 @@ # Implement Cross-Object Values -**New concept:** scalar and vector-like inputs use the same one-step kernel -contract. Object selection and topology remain scenario concerns. +A model reads its inputs from `status`, whether the values came from the +same object, another object, or several objects. The scenario chooses those +sources. This page shows what the model author writes in each case. -See [Build One Multiscale Plant](@ref) for the simulation-user construction -journey. +The equations and numbers below are teaching examples. For the corresponding +scenario walkthrough, see [Build One Multiscale Plant](@ref). -Every block below executes during the documentation build using the tested, -shipped example models. +## Read one value from another object -## Model 4: consume one scalar from another object +The development example calculates: -`ToyDevelopmentModel` already declares `stress=Default(1.0)`. A scenario may -replace that fallback with one live scalar from a soil object: +**growth increment = efficiency × thermal time × stress factor** + +Here is its actual input declaration and calculation, extracted from +`examples/ToyModelDeveloper.jl`: + +```@eval +Main.DocsSources.section( + "examples/ToyModelDeveloper.jl", + "PlantSimEngine.inputs_(::ToyDevelopmentModel)", + "\"\"\"\n ToyDailyDevelopmentModel", +) +``` + +`stress=Default(1.0)` means the model can run without stress reduction when +that is appropriate. A scenario can instead supply a soil object's value: ```@example modeler_cross_object -using Dates, PlantMeteo, PlantSimEngine, DataFrames +using Dates, Test, PlantSimEngine using PlantSimEngine.Examples cross_object = CompositeModel( @@ -27,131 +40,91 @@ cross_object = CompositeModel( name=:development, on=One(scale=:Leaf), inputs=( - :stress => One( - scale=:Soil, - within=SceneScope(), - var=:stress, - from_status=true, + stress=One( + scale=:Soil, within=SceneScope(), + var=:stress, from_status=true, ), ), ), ), ) -cross_simulation = run!(cross_object) -( - leaf=final_state(cross_simulation, :leaf), - binding=only(Diagnostics.explain_bindings(cross_object)), -) +simulation = run!(cross_object) +growth = final_state(simulation, :leaf).growth +@test growth == 2.0 +growth ``` -The model kernel still reads only `status.stress`. It does not search for soil, -know object ids, or copy the scalar each step. The scenario's `One` selector -resolves a shared `Ref`. +`from_status=true` reads the explicitly supplied soil state. When a soil model +produces that value, select its application as described in +[Coupling models](@ref). The development equation itself stays unchanged. -## Model 5: consume a vector-like multiscale value +## Read several values and combine them -`ToyMaintenanceRespirationModel` runs once per leaf and publishes `Rm`. -`ToyPlantRmModel` declares one vector-like input and reduces it: +Suppose leaf respiration amounts are available for the same interval. A +plant-level model can add them: -```@example modeler_cross_object -plant_respiration = ToyPlantRmModel() -plant_status = Status(Rm_organs=[0.2, 0.3], Rm=-Inf) -PlantSimEngine.run!( - plant_respiration, - plant_status, - NamedTuple(), - nothing, - nothing, -) -( - inputs=PlantSimEngine.inputs_(plant_respiration), - outputs=PlantSimEngine.outputs_(plant_respiration), - total=plant_status.Rm, +**plant respiration = sum of leaf respiration** + +The model asks for a vector of real values and sums it. It does not need to +know how many leaves exist: + +```@eval +Main.DocsSources.section( + "examples/ToyMaintenanceRespirationModel.jl", + "struct ToyPlantRmModel", ) ``` -The scenario decides that `Rm_organs` means all descendant leaf outputs: +Test the equation with an ordinary vector first: ```@example modeler_cross_object -respiration = ToyMaintenanceRespirationModel( - 2.0, - 0.06, - 25.0, - 0.5, - 0.02, -) +plant_respiration = ToyPlantRmModel() +sample = Status(Rm_organs=[0.2, 0.3], Rm=0.0) +PlantSimEngine.run!(plant_respiration, sample, nothing, nothing, nothing) +@test sample.Rm == 0.5 +sample.Rm +``` -multiscale = CompositeModel( +## Choose which leaves contribute + +This scenario supplies two illustrative leaf amounts, then selects only the +leaves belonging to the plant: + +```@example modeler_cross_object +plant = CompositeModel( Object(:plant; scale=:Plant), - Object( - :leaf_1; - scale=:Leaf, - parent=:plant, - status=Status(carbon_biomass=10.0), - ), - Object( - :leaf_2; - scale=:Leaf, - parent=:plant, - status=Status(carbon_biomass=20.0), - ); + Object(:leaf_1; scale=:Leaf, parent=:plant, status=Status(Rm=0.2)), + Object(:leaf_2; scale=:Leaf, parent=:plant, status=Status(Rm=0.3)); applications=( ModelSpec( - respiration; - name=:maintenance, - on=Many(scale=:Leaf), - ), - ModelSpec( - ToyPlantRmModel(); - name=:plant_maintenance, + plant_respiration; + name=:plant_respiration, on=One(scale=:Plant), inputs=( - :Rm_organs => Many( - scale=:Leaf, - within=Subtree(), - application=:maintenance, - var=:Rm, + Rm_organs=Many( + scale=:Leaf, within=Subtree(), + var=:Rm, from_status=true, ), ), ), ), - environment=Atmosphere( - T=25.0, - Wind=1.0, - Rh=0.7, - duration=Hour(1), - ), -) - -multiscale_simulation = run!(multiscale) -( - plant=final_state(multiscale_simulation, :plant), - leaves=final_state(multiscale_simulation, Many(scale=:Leaf)), -) -``` - -```@example modeler_cross_object -select( - DataFrame(Diagnostics.explain_bindings(multiscale)), - :application_id, - :input, - :source_ids, - :carrier_kind, - :copy_semantics, + environment=(duration=Day(1),), ) +plant_result = final_state(run!(plant), :plant).Rm +@test plant_result == 0.5 +plant_result ``` -The `Many` carrier is a live `RefVector`; the aggregation kernel operates on an -`AbstractVector` and stays independent of object count and identity. +`Many` provides the selected values in a vector-like input. `Subtree()` +restricts the search to this plant's descendants. This avoids accidentally +adding another plant's leaves. -## Model-author recap +For a real process chain, the producer and consumer must use compatible +units, physical bases, and time intervals. An average per unit leaf area +cannot become a plant total by an unweighted sum; supply the necessary areas +and write that conversion explicitly. -- **You implemented:** scalar or vector-compatible input schemas and ordinary - one-step arithmetic. -- **PlantSimEngine inferred:** shared `Ref` and `RefVector` carriers plus - producer-before-consumer order. -- **The scenario author keeps explicit:** cross-object scope, multiplicity, - source application, and variable remapping. -- **New API names:** `One`, `Many`, `SceneScope`, `Subtree`, `from_status`, - `RefVector`, and `input_carrier`. +Use `Diagnostics.explain_bindings(plant)` to inspect the selected sources. +The implementation uses live references, so the aggregation equation can +remain independent of object identity and count. diff --git a/docs/src/journeys/modelers/environment_and_cadence.md b/docs/src/journeys/modelers/environment_and_cadence.md index a8b927ac6..a7d71106f 100644 --- a/docs/src/journeys/modelers/environment_and_cadence.md +++ b/docs/src/journeys/modelers/environment_and_cadence.md @@ -1,142 +1,103 @@ # Implement Environment And Cadence Traits -**New concept:** model-authored runtime traits. Environment declarations name -the fields a kernel samples, while cadence and output-policy traits state when -the same kernel runs and how its values cross clocks. +A process may read air temperature from its environment and may have a +preferred time step. Declare those requirements beside the model; the scenario +chooses the data provider and can configure its execution cadence. -Simulation users configure providers in [Understand Environments](@ref) and -application clocks in [Give Models Different Cadences](@ref). +Start with [Understand Environments](@ref) and +[Give Models Different Cadences](@ref) for the scenario-user perspective. -## Model 6: declare sampled environment inputs +## Declare an environmental input -`ToyMaintenanceRespirationModel` reads object state and sampled temperature. -The tested contract names that environment field explicitly: +This teaching model simply copies the environmental temperature to an output. +It isolates the interface before introducing a biological equation. +These are its actual definitions in `examples/ToySpatialEnvironment.jl`: -```@example modeler_environment_time -using Dates, PlantMeteo, PlantSimEngine, DataFrames -using PlantSimEngine.Examples - -respiration = ToyMaintenanceRespirationModel( - 2.0, - 0.06, - 25.0, - 0.5, - 0.02, -) -( - inputs=PlantSimEngine.inputs_(respiration), - environment_inputs=PlantSimEngine.environment_inputs_(respiration), - environment_outputs=PlantSimEngine.environment_outputs_(respiration), - outputs=PlantSimEngine.outputs_(respiration), +```@eval +Main.DocsSources.section( + "examples/ToySpatialEnvironment.jl", + "struct ToyEnvironmentReaderModel", + "\"\"\"\n ToyEnvironmentControllerModel", ) ``` -The kernel reads model parameters from `model`, object state from `status`, and -forcing from `environment`: +`environment_inputs_` declares `T`, and `run!` reads it from +`environment.T`. The model does not choose a weather file or spatial provider. + +Test that read directly: ```@example modeler_environment_time -direct_status = Status(carbon_biomass=10.0, Rm=-Inf) -PlantSimEngine.run!( - respiration, - direct_status, - (T=25.0,), - nothing, - nothing, -) -direct_status +using Dates, Test, PlantSimEngine +using PlantSimEngine.Examples + +reader = ToyEnvironmentReaderModel() +sample = Status(temperature_seen=0.0) +PlantSimEngine.run!(reader, sample, (T=25.0,), nothing, nothing) +@test sample.temperature_seen == 25.0 +sample.temperature_seen ``` +Then supply an environment through the simulation: + ```@example modeler_environment_time model = CompositeModel( - Object( - :leaf; - scale=:Leaf, - status=Status(carbon_biomass=10.0), - ); - applications=( - ModelSpec( - respiration; - name=:maintenance, - on=One(scale=:Leaf), - ), - ), - environment=Atmosphere( - T=25.0, - Wind=1.0, - Rh=0.7, - duration=Hour(1), - ), -) - -( - declared=PlantSimEngine.environment_inputs_(respiration), - final=final_state(run!(model)), + reader; + environment=(T=25.0, duration=Hour(1)), ) +result = final_state(run!(model)).temperature_seen +@test result == 25.0 +result ``` -The model does not name a provider or inspect raw weather storage. -`Environment(...)` remains scenario configuration. +A scientific model should also declare the temperature's units and meaning +with `variable_contracts_`, as in [Implement a basic model](@ref). +Use [Port an existing model](@ref) for an equation that combines environmental +temperature with object state. -## Model 7: declare cadence and output semantics +## Give a model a default cadence -`ToyDailyDevelopmentModel` accumulates one increment whenever it runs. Its -model-level traits say “every 24 simulation steps, starting at step 1” and -“consumers may hold the last daily value between publications”: +The next teaching model adds a fixed increment whenever it runs. Its source +declares a default of 24 simulation steps and allows consumers to hold its +last output between updates: -```@example modeler_environment_time -daily = ToyDailyDevelopmentModel(2.0) -( - clock=PlantSimEngine.timespec(daily), - outputs=PlantSimEngine.output_policy(daily), +```@eval +Main.DocsSources.section( + "examples/ToyModelDeveloper.jl", + "PlantSimEngine.inputs_(::ToyDailyDevelopmentModel)", ) ``` -`ClockSpec` is expressed in simulation steps. Use -`ModelSpec(...; every=Day(1))` when a scenario should express a -duration-relative cadence or override the model default. +`timespec` gives a model default. `output_policy` gives a default interpretation +for consumers reading between publications. The equation still updates only +when the application runs. + +`ClockSpec(24.0, 1.0)` means every 24 base steps, starting at step 1. It +corresponds to a day only when the base step is an hour. In a scenario, +`every=Day(1)` expresses the intended duration directly: ```@example modeler_environment_time +daily = ToyDailyDevelopmentModel(2.0) daily_model = CompositeModel( Object(:plant; scale=:Plant); applications=( ModelSpec( - daily; - name=:daily_development, - on=One(scale=:Plant), + daily; name=:daily_development, on=One(scale=:Plant), + every=Day(1), ), ), - environment=[ - (duration=Hour(1),) - for _ in 1:25 - ], + environment=[(duration=Hour(1),) for _ in 1:25], ) -daily_simulation = - run!(daily_model; steps=25, outputs=:all) -( - traits=( - clock=PlantSimEngine.timespec(daily), - outputs=PlantSimEngine.output_policy(daily), - ), - schedule=DataFrame(Diagnostics.explain_schedule(daily_model)), - final=final_state(daily_simulation), - retained=DataFrame( - Diagnostics.explain_outputs(daily_simulation), - ), -) +daily_simulation = run!(daily_model; steps=25, outputs=:all) +growth = final_state(daily_simulation).daily_growth +@test growth == 4.0 +growth ``` -The model runs at steps 1 and 25. A downstream application can override -`HoldLast` with an explicit selector policy when its scientific interpretation -requires `Integrate`, `Aggregate`, or `Interpolate`. - -## Model-author recap +The model runs at steps 1 and 25, adding 2 each time. A consumer can override +`HoldLast` when averaging or accumulating values has the appropriate physical +meaning. See [Give Models Different Cadences](@ref) before choosing that policy. -- **You implemented:** declared environment reads, a model default clock, and - per-output temporal meaning. -- **PlantSimEngine inferred:** environment validation/sampling, execution - steps, and the default cross-clock policy. -- **The scenario author keeps explicit:** concrete environment provider, - simulation base step, cadence overrides, and input-policy overrides. -- **New API names:** `environment_inputs_`, `timespec`, `ClockSpec`, - `output_policy`, and `HoldLast`. +Keep a model default only when it belongs to the scientific implementation. +Data providers, scenario-specific timing, and the choice of output history +remain part of the simulation configuration. diff --git a/docs/src/journeys/modelers/hard_dependencies.md b/docs/src/journeys/modelers/hard_dependencies.md index 2e3307487..e893a4326 100644 --- a/docs/src/journeys/modelers/hard_dependencies.md +++ b/docs/src/journeys/modelers/hard_dependencies.md @@ -1,131 +1,106 @@ # Implement A Hard Dependency -**New concept:** a process-level hard dependency, used only when the parent -kernel must control another model's execution. Value dependencies should stay -in `inputs_`. +Use a **hard dependency** when one model must decide when or how often another +model runs. For example, an energy-balance calculation may evaluate gas +exchange at several trial temperatures before accepting a solution. -Simulation users wire and inspect advanced calls in -[Control Advanced Execution](@ref). +When a model only needs another model's result, use an ordinary input instead. +[Coupling models](@ref) explains the difference, and +[Control Advanced Execution](@ref) covers scenario configuration. -The examples below execute the shipped, tested -`ToySelectiveCallControllerModel`. +## Declare the model you need to call -## Model 8: declare and execute the call +The teaching controller below selects one leaf, tries two prescribed +temperatures, and finally accepts a third. It demonstrates the call mechanism; +it does not solve an energy-balance equation. -`ToySelectiveCallControllerModel` declares a reusable default by process, -scale, and relative scope. It cannot know future application names: +Its declaration selects leaf readers by process within the current plant. +These definitions are extracted from `examples/ToyAdvancedControl.jl`: -```@example modeler_hard_dependency -using PlantSimEngine, DataFrames -using PlantSimEngine.Examples +```@eval +Main.DocsSources.section( + "examples/ToyAdvancedControl.jl", + "PlantSimEngine.dep(::ToySelectiveCallControllerModel)", + "function PlantSimEngine.outputs_", +) +``` -controller = ToySelectiveCallControllerModel( - (28.0, 31.0), - 22.0; - selected_object=:sun_leaf, +`Call` declares the requirement. The controller will execute it explicitly. +Process and relative scope allow reuse without knowing a future scenario's +application names. + +## Run trials and accept one result + +Here is the actual controller calculation: + +```@eval +Main.DocsSources.section( + "examples/ToyAdvancedControl.jl", + "function PlantSimEngine.run!(\n model::ToySelectiveCallControllerModel,", + "\"\"\"\n ToyStockWriterModel", ) -PlantSimEngine.dep(controller) ``` -Inside `run!`, the controller inspects the vector-like target collection, -restricts the declared call by object id, runs trials with `publish=false`, and -publishes one accepted execution with `publish=true`. The complete scenario -below executes that real kernel, so this page does not duplicate an untested -source excerpt. +`call_targets` finds the declared targets; the object filter chooses the leaf +for this example. Each trial uses `publish=false`, so it does not create an +accepted output sample. The final call uses `publish=true`. + +An actual iterative model must define its own trial calculation, convergence +criterion, and treatment of state. Publication suppression does not undo +arbitrary changes made by a trial. -The shipped model is exercised by the example-model contract suite. Build a -scenario without a `calls` keyword to use that model default: +## Compose a small scenario ```@example modeler_hard_dependency +using Test, PlantSimEngine +using PlantSimEngine.Examples + +controller = ToySelectiveCallControllerModel( + (28.0, 31.0), 22.0; selected_object=:sun_leaf, +) environment = ToySpatialEnvironment( - Dict( - :sun => (T=26.0,), - :shade => (T=18.0,), - ); + Dict(:sun => (T=26.0,), :shade => (T=18.0,)); step_seconds=3600.0, ) model = CompositeModel( Object(:plant; scale=:Plant), - Object( - :sun_leaf; - scale=:Leaf, - parent=:plant, - geometry=(cell=:sun,), - ), - Object( - :shade_leaf; - scale=:Leaf, - parent=:plant, - geometry=(cell=:shade,), - ); + Object(:sun_leaf; scale=:Leaf, parent=:plant, geometry=(cell=:sun,)), + Object(:shade_leaf; scale=:Leaf, parent=:plant, geometry=(cell=:shade,)); applications=( ModelSpec( ToyEnvironmentReaderModel(); - name=:reader, - on=Many(scale=:Leaf), + name=:reader, on=Many(scale=:Leaf), environment=Environment(backend=environment), ), - ModelSpec( - controller; - name=:controller, - on=One(scale=:Plant), - ), + ModelSpec(controller; name=:controller, on=One(scale=:Plant)), ), ) - -DataFrame(Diagnostics.explain_calls(model)) -``` - -```@example modeler_hard_dependency simulation = run!(model; outputs=:all) +accepted = final_state(simulation, :plant) +@test accepted.trial_temperature_seen == 31.0 +@test accepted.accepted_temperature_seen == 22.0 ( - controller=final_state(simulation, :plant), - leaves=final_state(simulation, Many(scale=:Leaf)), - publications=filter( - row -> row.application_id == :reader, - DataFrame(Diagnostics.explain_outputs(simulation)), - ), + last_trial=accepted.trial_temperature_seen, + accepted=accepted.accepted_temperature_seen, ) ``` -`origin=:model_default` identifies the `dep(model)` contract. A concrete -scenario can replace it with `ModelSpec(...; calls=...)` when application -identity or target selection differs. - -## Keep the common path bulk and concrete - -The selective example above intentionally materializes one public target -because it chooses an object and gives each trial its own sampled value. If the -algorithm executes every resolved target with the same already-sampled -environment, use the bulk `run_call!` path with `context`, `:readers`, -`sampled_environment=environment`, and `publish=false`. - -This executes the compiler's cached typed batches directly. It avoids creating -or indexing `CallTarget` wrappers inside a timestep loop. - -Some iterative algorithms must inspect a singular dependency model before -executing it. Keep that dispatch concrete with -`reader_model = call_model(context, :reader)`, let the application build its -own scientifically meaningful trial state from that concrete model, then -execute the same declared call in bulk. PlantSimEngine does not define or infer -that application-specific trial builder. - -`call_model` requires exactly one resolved target. Use `call_targets` when the -algorithm also needs target status, object selection, a custom order, or -several distinct sampled environments. - -The dependency definition is immutable after compilation, while its selected -objects are not. Growth, removal, and reparenting refresh the affected target -buffers once at the lifecycle barrier; normal timesteps continue through the -same compiled plan. - -## Model-author recap - -- **You implemented:** a process-level `Call` requirement and explicit - execution inside the parent kernel. -- **PlantSimEngine inferred:** call-only scheduling, resolved targets, nested - context, and publication boundaries. -- **The scenario author keeps explicit:** application overrides and any - architecture-specific target choice. -- **New API names:** `dep`, `Call`, `call_model`, `call_targets`, - `CallTargets`, `run_call!`, `sampled_environment`, and `publish`. +The controller's default `dep` declaration supplies the call. A scenario can +override its selection with `ModelSpec(...; calls=...)`. Inspect +`Diagnostics.explain_calls(model)` to check the resolved targets. + +## Choose the simplest call operation + +| Your algorithm needs… | Use | +|---|---| +| To execute every declared target | `run_call!(context, :readers)` | +| To inspect one resolved model's parameters or type | `call_model(context, :reader)` | +| To select objects, inspect target state, or use distinct trial values | `call_targets(context, :readers)`, then call the selected target | + +`call_model` requires exactly one resolved target. The bulk `run_call!` +path can take `sampled_environment=value` when every target uses the same +already-sampled environment. + +Supported structural changes refresh the selected objects. Keep ordinary +calls through these public operations so your controller continues to use +the current targets. diff --git a/docs/src/journeys/modelers/mutable_environment.md b/docs/src/journeys/modelers/mutable_environment.md index 5b233ffdf..8c8820182 100644 --- a/docs/src/journeys/modelers/mutable_environment.md +++ b/docs/src/journeys/modelers/mutable_environment.md @@ -1,109 +1,103 @@ # Implement A Mutable Environment Controller -**New concept:** accepted mutable environment state. A controller declares -which variables it may commit, evaluates typed trial states, and commits one -accepted state explicitly. - -Simulation users first encounter this workflow in -[Modify The Environment](@ref). Backend packages implement the separate -[Environment Backend Extensions](@ref) contract. - -The examples below execute the shipped, tested -`ToyEnvironmentControllerModel`. +Some coupled calculations also update their environment. A canopy controller, +for example, may evaluate a trial temperature before accepting the +environmental state that other processes should use. + +This teaching example uses prescribed temperatures to explain that sequence. +It does not implement a physical canopy solver. Start with +[Modify The Environment](@ref) for the scenario perspective and +[Implement A Hard Dependency](@ref) for model calls. + +## Declare the accepted values you can write + +A controller declares both the model it calls and the environmental variables +it may commit. Here `environment_outputs_` names temperature `T`. These are +the actual declarations in `examples/ToySpatialEnvironment.jl`: + +```@eval +Main.DocsSources.section( + "examples/ToySpatialEnvironment.jl", + "PlantSimEngine.inputs_(::ToyEnvironmentControllerModel)", + "function PlantSimEngine.run!(\n model::ToyEnvironmentControllerModel,", +) +``` -## Model 9: declare commit permission +Object outputs record what the controller observed. +`environment_outputs_` separately declares what it may write to the +environment provider. -`ToyEnvironmentControllerModel` declares its reader hard dependency and the -environment variable it may commit: +## Evaluate a trial, then commit an accepted state -```@example modeler_mutable_environment -using PlantSimEngine, DataFrames -using PlantSimEngine.Examples +The controller's implementation makes the sequence explicit: -controller = ToyEnvironmentControllerModel(30.0, 22.0) -( - dependency=PlantSimEngine.dep(controller), - commit_schema=PlantSimEngine.environment_outputs_(controller), +```@eval +Main.DocsSources.section( + "examples/ToySpatialEnvironment.jl", + "function PlantSimEngine.run!(\n model::ToyEnvironmentControllerModel,", ) ``` -Its tested kernel keeps trial, commit, and accepted publication separate. It -runs the reader against the trial environment with `publish=false`, calls -`commit_environment!` only for the accepted environment, then publishes one -accepted reader execution. The complete scenario below executes that source -instead of repeating it as an unchecked excerpt. +The first call evaluates the reader against a trial environment without +publishing an accepted sample. `commit_environment!` writes the accepted +environmental values; the final reader call publishes the accepted result. -`environment_outputs_` is commit permission, not object-status output. -PlantSimEngine validates that the accepted state provides every declared -variable before invoking the backend through the controller's compiled handle. +In a scientific controller, the trial calculation and acceptance criterion +belong to your algorithm. Also account for state changed during a rejected +trial: suppressing publication is not a general rollback operation. -## Compose the controller +## Connect the controller to a provider -The reader needs only a provider. The controller additionally receives -`sink=:cells`; the backend defines what that sink means: +The example provider stores temperature in a named canopy cell. The reader +can sample it; the controller additionally receives `sink=:cells`, which +permits the supported write operation for this provider. ```@example modeler_mutable_environment +using Test, PlantSimEngine +using PlantSimEngine.Examples + environment = ToySpatialEnvironment( - Dict(:canopy => (T=20.0,)); - step_seconds=3600.0, + Dict(:canopy => (T=20.0,)); step_seconds=3600.0, ) +controller = ToyEnvironmentControllerModel(30.0, 22.0) model = CompositeModel( - Object( - :leaf; - scale=:Leaf, - geometry=(cell=:canopy,), - ); + Object(:leaf; scale=:Leaf, geometry=(cell=:canopy,)); applications=( ModelSpec( ToyEnvironmentReaderModel(); - name=:reader, - on=One(scale=:Leaf), + name=:reader, on=One(scale=:Leaf), environment=Environment(backend=environment), ), ModelSpec( controller; - name=:controller, - on=One(scale=:Leaf), - environment=Environment( - backend=environment, - sink=:cells, - ), + name=:controller, on=One(scale=:Leaf), + environment=Environment(backend=environment, sink=:cells), ), ), ) -( - call=DataFrame(Diagnostics.explain_calls(model)), - environment=DataFrame( - Diagnostics.explain_environment_bindings(model), - ), -) -``` - -```@example modeler_mutable_environment simulation = run!(model; outputs=:all) +state = final_state(simulation) +@test state.trial_temperature_seen == 30.0 +@test state.accepted_temperature_seen == 22.0 +@test environment.cells[:canopy].T == 22.0 ( - final=final_state(simulation), - committed=environment.cells[:canopy], - publications=filter( - row -> row.application_id == :reader, - DataFrame(Diagnostics.explain_outputs(simulation)), - ), + trial=state.trial_temperature_seen, + accepted=state.accepted_temperature_seen, + committed_temperature=environment.cells[:canopy].T, ) ``` -The rejected `T=30` trial mutates only the reader's trial status. The accepted -`T=22` state is committed once and produces the reader's only retained sample. -If the controller itself runs as an unpublished ancestor call, PlantSimEngine -also suppresses its descendant publications and environment writes. - -## Model-author recap - -- **You implemented:** declared commit variables, typed trial construction, - acceptance logic, explicit commit, and one accepted publication. -- **PlantSimEngine inferred:** permission validation, backend/handle routing, - nested trial suppression, and retained output history. -- **The scenario author keeps explicit:** provider, commit sink, concrete - backend, and any hard-call override. -- **New API names:** `environment_outputs_`, `commit_environment!`, - `Environment`, `environment`, and `publish`. +The provider starts at 20, the trial reads 30, and the accepted value is 22. +Use `Diagnostics.explain_environment_bindings(model)` to inspect the provider +and sink, and `Diagnostics.explain_outputs(simulation)` to inspect retained +samples. + +The accepted state must provide every declared environmental output. If the +controller is itself inside an unpublished outer trial, its descendant +publications and environment writes are suppressed too. + +A package providing a different spatial environment implements the separate +[Environment Backend Extensions](@ref) interface. A process-model author +normally uses that provider through `Environment` and the public call and +commit operations shown here. diff --git a/docs/src/journeys/users/cadences.md b/docs/src/journeys/users/cadences.md index 61eaeba03..e8d606580 100644 --- a/docs/src/journeys/users/cadences.md +++ b/docs/src/journeys/users/cadences.md @@ -2,9 +2,10 @@ ## New concept: application clocks and temporal input policies -Running a whole composite over many timesteps was introduced on the first -journey. This page changes one thing: applications no longer all run at the -environment base step. +Let canopy development update daily while light interception responds every +hour. Then convert hourly water-uptake rates to daily amounts. These teaching +examples show how to choose what a slower or faster model receives from its +source. Choose a base step that divides every application cadence: hourly and 90-minute applications can share a 30-minute base step. A duration such as @@ -28,24 +29,24 @@ hourly_forcing = [ ] model = CompositeModel( - Object(:plant; scale=:Plant, kind=:plant); + Object(:canopy; scale=:Canopy, kind=:canopy, status=Status(TT_cu=600.0)); applications=( ModelSpec( ToyDegreeDaysCumulModel(); name=:degree_days, - on=One(scale=:Plant), + on=One(scale=:Canopy), every=Day(1), ), ModelSpec( ToyLAIModel(); name=:lai, - on=One(scale=:Plant), + on=One(scale=:Canopy), every=Day(1), ), ModelSpec( Beer(0.6); name=:light, - on=One(scale=:Plant), + on=One(scale=:Canopy), inputs=( :LAI => One( within=Self(), @@ -64,6 +65,23 @@ model = CompositeModel( simulation = run!(model; steps=25, outputs=:all) ``` +First inspect the outputs at the beginning of the run and around the daily +update. LAI is in m² of leaves per m² of ground; absorbed PAR is in μmol m⁻² +of ground s⁻¹. The initial 600 °C d is an illustrative development stage, +chosen so the LAI changes are visible. + +```@example journey_cadences +rows = collect_outputs(simulation; sink=DataFrame) +light_samples = rows[(rows.variable .== :aPPFD) .& in.(rows.timestep, Ref((1, 2, 24, 25))), + [:timestep, :value]] +light_samples +``` + +The daily models execute at steps 1 and 25, rather than waiting until the end +of the first day. The first LAI value is held for hourly light calculations +until the next daily update. A model that needs a complete preceding day +must handle its initial history explicitly. + The schedule reports physical cadence in seconds and in base steps: ```@example journey_cadences @@ -109,38 +127,17 @@ latest value remains meaningful. `Integrate(reducer)` can integrate rates using sample durations. The default `Integrate()` only sums values; it does not multiply by elapsed time. The explicit reducer below uses durations in seconds. If a leaf publishes a constant -rate in units per second, integrating 24 hourly samples produces a daily -amount. The consumer below sums the independently integrated amounts from two -leaves. +water-uptake rate in mg per second, integrating 24 hourly samples produces a +daily amount in mg. The plant adds the amounts from two leaves. The constant +rates are teaching values, not predictions of water demand. + +Load the [teaching models](../../guides/time/teaching_models.jl). One copies a +leaf's supplied water-uptake rate; the other sums amounts from leaves. Their +source is shown in [Hourly, Daily, And Weekly Models](../../guides/time/hourly_daily_weekly.md). ```@example journey_cadences -PlantSimEngine.@process "cadence_hourly_flux" verbose = false -PlantSimEngine.@process "cadence_daily_amount" verbose = false - -struct CadenceHourlyFlux <: AbstractCadence_Hourly_FluxModel end -struct CadenceDailyAmount <: AbstractCadence_Daily_AmountModel end - -PlantSimEngine.inputs_(::CadenceHourlyFlux) = (rate=Required(Real),) -PlantSimEngine.outputs_(::CadenceHourlyFlux) = (flux=0.0,) -PlantSimEngine.run!( - ::CadenceHourlyFlux, - status, - environment, - constants, - context, -) = (status.flux = status.rate) - -PlantSimEngine.inputs_(::CadenceDailyAmount) = ( - leaf_amounts=Required(AbstractVector{<:Real}), -) -PlantSimEngine.outputs_(::CadenceDailyAmount) = (amount=0.0,) -PlantSimEngine.run!( - ::CadenceDailyAmount, - status, - environment, - constants, - context, -) = (status.amount = sum(status.leaf_amounts)) +include(joinpath(pkgdir(PlantSimEngine), "docs", "src", "guides", "time", "teaching_models.jl")) +using .TeachingTimeModels ``` ```@example journey_cadences @@ -150,31 +147,31 @@ flux_model = CompositeModel( :leaf_1; scale=:Leaf, parent=:plant, - status=Status(rate=1.0), + status=Status(rate_mg_s=1.0), ), Object( :leaf_2; scale=:Leaf, parent=:plant, - status=Status(rate=2.0), + status=Status(rate_mg_s=2.0), ); applications=( ModelSpec( - CadenceHourlyFlux(); + HourlyWaterRate(); name=:hourly_flux, on=Many(scale=:Leaf), every=Hour(1), ), ModelSpec( - CadenceDailyAmount(); + SumWaterAmounts(); name=:daily_amount, on=One(scale=:Plant), inputs=( - :leaf_amounts => Many( + :amounts_mg => Many( scale=:Leaf, within=Subtree(), application=:hourly_flux, - var=:flux, + var=:water_rate_mg_s, policy=Integrate((values, durations_seconds) -> sum(values .* durations_seconds)), window=Day(1), ), @@ -185,13 +182,26 @@ flux_model = CompositeModel( environment=[(duration=Hour(1),) for _ in 1:25], ) -flux_simulation = run!(flux_model; steps=25) -amount = final_state(flux_simulation, One(scale=:Plant)).amount +flux_simulation = run!(flux_model; steps=25, outputs=:all) +amount = final_state(flux_simulation, One(scale=:Plant)).water_amount_mg @assert amount == 259200.0 amount ``` -The result is `(1 + 2) × 24 × 3600 = 259200` rate-seconds. Use +The complete-day result is `(1 + 2) × 24 × 3600 = 259200 mg` of water. +The first execution has only one hour of available history, so it produces +10800 mg; step 25 has a complete 24-hour rolling window: + +```@example journey_cadences +water_rows = collect_outputs(flux_simulation; sink=DataFrame) +daily_rows = water_rows[water_rows.application_id .== :daily_amount, [:timestep, :value]] +@assert daily_rows.value == [10800.0, 259200.0] +daily_rows +``` + +These windows use fixed durations, not calendar-aligned civil days. The +[weekly example](../../guides/time/hourly_daily_weekly.md) extends this same +calculation to seven days and checks its initial history. Use `Aggregate(reducer)` instead when the desired quantity is a mean, minimum, maximum, or another reduction of observations rather than a time integral. diff --git a/docs/src/journeys/users/environments.md b/docs/src/journeys/users/environments.md index d3ab194d7..10bcd54e0 100644 --- a/docs/src/journeys/users/environments.md +++ b/docs/src/journeys/users/environments.md @@ -2,25 +2,10 @@ ## New concept: declared sampling from global and spatial sources -The first simulation used a weather file as supplied forcing. This page now -makes that contract explicit. A model declares the names it reads from its -model-facing environment: - -```@example journey_environments -using PlantSimEngine, Dates, DataFrames -using PlantSimEngine.Examples - -( - degree_days=PlantSimEngine.environment_inputs_( - ToyDegreeDaysCumulModel(), - ), - light=PlantSimEngine.environment_inputs_(Beer(0.6)), -) -``` - -`ToyDegreeDaysCumulModel` reads `environment.T`; `Beer` reads -`environment.Ri_PAR_f`. These are not status inputs and are not outputs owned -by the target object. +Use weather whose column names differ from those expected by the models, +then give two canopies different radiation supplies. An **environment** +provides external values such as air temperature and incident light. +This page uses teaching models and illustrative forcing. ## Global sampling and source names @@ -29,6 +14,9 @@ has `air_temperature` and `incident_par`; `Environment(...; sources=...)` remaps them for the two model applications. ```@example journey_environments +using PlantSimEngine, Dates, DataFrames +using PlantSimEngine.Examples + forcing = ( air_temperature=20.0, incident_par=300.0, @@ -36,12 +24,12 @@ forcing = ( ) global_model = CompositeModel( - Object(:plant; scale=:Plant, kind=:plant); + Object(:canopy; scale=:Canopy, kind=:canopy); applications=( ModelSpec( ToyDegreeDaysCumulModel(); name=:degree_days, - on=One(scale=:Plant), + on=One(scale=:Canopy), environment=Environment( provider=:global, sources=(T=:air_temperature,), @@ -50,12 +38,12 @@ global_model = CompositeModel( ModelSpec( ToyLAIModel(); name=:lai, - on=One(scale=:Plant), + on=One(scale=:Canopy), ), ModelSpec( Beer(0.6); name=:light, - on=One(scale=:Plant), + on=One(scale=:Canopy), environment=Environment( provider=:global, sources=(Ri_PAR_f=:incident_par,), @@ -71,6 +59,31 @@ global_state = final_state(global_simulation) (TT_cu=global_state.TT_cu, LAI=global_state.LAI, aPPFD=global_state.aPPFD) ``` +`T` and `air_temperature` are in °C. `Ri_PAR_f` and `incident_par` are +mean PAR fluxes in W m⁻² of ground; changing a source name does not convert +its units. `aPPFD` is absorbed PAR in μmol m⁻² of ground s⁻¹. + +## Inspect the input names + +A model declares which external values it needs. You can inspect those names +without changing the model: + +```@example journey_environments +using PlantSimEngine, Dates, DataFrames +using PlantSimEngine.Examples + +( + degree_days=PlantSimEngine.environment_inputs_( + ToyDegreeDaysCumulModel(), + ), + light=PlantSimEngine.environment_inputs_(Beer(0.6)), +) +``` + +`ToyDegreeDaysCumulModel` reads `environment.T`; `Beer` reads +`environment.Ri_PAR_f`. These are not status inputs and are not outputs owned +by the target object. + The environment diagnostic distinguishes the variables seen by each model from the actual source names: @@ -93,9 +106,10 @@ before simulation. ## Spatial sampling Spatial backends keep the same model-facing declaration. They additionally -compile an opaque handle for each application/object target. The small -`ToySpatialEnvironment` example maps object geometry to either a sunny or -shaded cell: +associate each object with its source location. The small +`ToySpatialEnvironment` example maps a cell label to either a sunny or shaded +canopy. This label-based fixture demonstrates sampling; it does not compute +light transport from a geometric scene: ```@example journey_environments spatial_environment = ToySpatialEnvironment( @@ -108,16 +122,16 @@ spatial_environment = ToySpatialEnvironment( spatial_model = CompositeModel( Object( - :sun_leaf; - scale=:Leaf, - kind=:leaf, + :sun_canopy; + scale=:Canopy, + kind=:canopy, geometry=(cell=:sun,), status=Status(LAI=2.0), ), Object( - :shade_leaf; - scale=:Leaf, - kind=:leaf, + :shade_canopy; + scale=:Canopy, + kind=:canopy, geometry=(cell=:shade,), status=Status(LAI=2.0), ); @@ -125,18 +139,23 @@ spatial_model = CompositeModel( ModelSpec( Beer(0.6); name=:light, - on=Many(scale=:Leaf), + on=Many(scale=:Canopy), environment=Environment(backend=spatial_environment), ), ), ) spatial_simulation = run!(spatial_model) -spatial_states = final_state(spatial_simulation, Many(scale=:Leaf)) +spatial_states = final_state(spatial_simulation, Many(scale=:Canopy)) Dict(id => state.aPPFD for (id, state) in spatial_states) ``` -The one `Many` application retains two distinct compiled handles: +Both canopies have LAI = 2 m² m⁻². The sunny canopy receives four times the +incident PAR of the shaded canopy, so its absorbed PAR is also four times +as large. Both results use ground area, not individual leaf area. + +The optional diagnostic below shows two different cached source locations +(called handles), one for each canopy: ```@example journey_environments select( diff --git a/docs/src/journeys/users/maespa_synthesis.md b/docs/src/journeys/users/maespa_synthesis.md index 3b298f3a1..c4b33d3a9 100644 --- a/docs/src/journeys/users/maespa_synthesis.md +++ b/docs/src/journeys/users/maespa_synthesis.md @@ -8,14 +8,21 @@ MAESPA-style stand: two species, five leaves, hourly canopy and soil exchange, daily allocation and LAI, iterative leaf calls, and accepted mutable canopy air. +This is a non-calibrated teaching example inspired by MAESPA's process +structure. It is not a validated MAESPA implementation. Leaf illumination is +uniform with complete absorption, the canopy has one air layer, and the soil +water model uses prescribed withdrawals and bounds rather than a complete +soil hydraulic balance. The example tests coupling and carbon accounting; +its trajectories should not be used as empirical predictions. + If any individual mechanism is unfamiliar, follow its focused link in [How the pieces compose](@ref) before reading the implementation. ## Run the reference case The complete, tested source lives in -`examples/maespa_model_example.jl`. Run 25 hours so both hourly and daily -applications cross a day boundary: +`examples/maespa_model_example.jl`. Supply 25 hourly forcing rows so the daily +scheduler runs at its initial boundary and again 24 base steps later: ```@example journey_maespa_synthesis using PlantSimEngine, DataFrames @@ -151,7 +158,23 @@ changing the model-facing environment contract; the two-cell proof is in ## Check the scientific handoffs -The final snapshots expose canonical state independently of retained history: +The example keeps the conversions visible: + +| Quantity | Meaning and units | +|---|---| +| `Ri_SW_f`, `Ri_PAR_f` | Incoming radiation in W m⁻²; PAR is `PlantMeteo.Constants().PAR_fraction` of shortwave energy. | +| Leaf `aPPFD` | Absorbed photon flux in µmol photons m[leaf]⁻² s⁻¹: `Ri_PAR_f * constants.J_to_umol`, assuming uniform illumination and complete absorption. | +| Leaf `A` | Net CO₂ assimilation in µmol CO₂ m[leaf]⁻² s⁻¹. | +| Leaf `leaf_carbon` | Cumulative net assimilation in g elemental C: sum of `A * leaf_area * duration_seconds * 12e-6`. | +| Plant `daily_growth` | Net C since this plant's previous allocation, in g C per allocation interval; the initial scheduler call is a startup interval. | +| Plant carbon pools | Allocated g elemental C, not g dry matter; the unassigned allocation fraction remains in `reserve_pool`. | +| `scene_transpiration` | Accepted water loss in mm over the current hourly forcing interval. | + +Every leaf receives the same above-canopy irradiance here. There is no +shading, scattering, or leaf-angle calculation; a radiation model would need +to replace that assumption for a realistic stand. + +The final snapshots expose state independently of retained history: ```@example journey_maespa_synthesis scene = final_state(simulation, :model) @@ -160,16 +183,41 @@ plants = final_state(simulation, Many(scale=:Plant)) ( lai=scene.lai, - canopy_temperature=scene.canopy_tair, - transpiration=scene.scene_transpiration, - soil_water_potential=soil.psi_soil, - daily_growth=Dict( + canopy_temperature_C=scene.canopy_tair, + hourly_transpiration_mm=scene.scene_transpiration, + soil_water_potential_MPa=soil.psi_soil, + allocation_interval_g_C=Dict( id => state.daily_growth for (id, state) in plants ), ) ``` +Allocation reads cumulative leaf C without resetting it. Each plant stores +the cumulative amount it has already accounted for and allocates only the +difference at the next daily call. This prevents the same carbon being +allocated again on later days. Between allocation calls, a new signed carbon +increment can remain pending: + +```@example journey_maespa_synthesis +DataFrame([ + ( + plant=id, + cumulative_net_C_g=sum(state.leaf_carbon), + accounted_C_g=state.accounted_carbon, + pools_C_g=state.leaf_pool + state.wood_pool + state.reserve_pool, + pending_C_g=sum(state.leaf_carbon) - state.accounted_carbon, + ) + for (id, state) in plants +]) +``` + +At each allocation, the three pools sum to `accounted_carbon`. Adding pending +C recovers cumulative net assimilation. These are signed carbon accounts: +negative net assimilation reduces them. The example does not model initial +biomass, construction respiration, dry-matter conversion, or limits on +withdrawing reserves, so the pools are not predictions of organ mass. + Retained output counts confirm the cadence boundary: hourly scene and leaf variables have 25 samples, while daily LAI and allocation variables have two: @@ -227,7 +275,11 @@ floating-point trajectories: - hourly and daily output counts match their cadences; - accepted canopy air is committed separately from above-canopy forcing; - leaf fluxes are finite and aggregate consistently at scene scale; -- both species grow, while their parameterized allocations remain distinct. +- PAR energy is bounded by shortwave energy and converted to photon units; +- 73 accepted hourly samples span three daily intervals and an initial + scheduler call, with no carbon added by rejected leaf iterations; +- each plant allocates every C increment once, preserves cumulative leaf C, + and conserves the sum of its leaf, wood, reserve, and pending C accounts. ## Page recap diff --git a/docs/src/journeys/users/one_object.md b/docs/src/journeys/users/one_object.md index e94b20761..991c7f640 100644 --- a/docs/src/journeys/users/one_object.md +++ b/docs/src/journeys/users/one_object.md @@ -12,6 +12,9 @@ below demonstrate coupling and are not a calibrated crop model: 2. `ToyLAIModel` reads cumulative thermal time and computes LAI. 3. `Beer` reads LAI and radiation and computes absorbed PAR. +Start with the [tutorial installation](../../prerequisites/installing_plantsimengine.md) +if these packages are not yet available in your Julia project. + The weather file is supplied forcing data for now. Environments get their own journey later. Its radiation columns contain daily totals in MJ m⁻² d⁻¹; we convert them to mean fluxes in W m⁻², as required by `Beer`. @@ -39,19 +42,11 @@ model = CompositeModel( No `ModelSpec` or selector is needed when all models run on the one object made by the concise constructor. -!!! tip "Choose a status type for the whole scenario" - Add `type_promotion=Dict(Float64 => Float32)` to the constructor to - materialize every matching status scalar, model input default, and model - output default as `Float32`. Ordinary numeric arrays are converted element - by element. Model parameters such as `Beer(0.6)` and values supplied by - `weather` keep their own types. - - Use `status_transform=(variable, value) -> ...` when only selected - variables need another representation. The precise transform runs before - the general type mapping. See [Numerical Reliability](@ref) for complete - `Float32` and uncertainty-propagation examples. - -Run thirty daily steps and retain the model outputs: +Run the first thirty daily steps and retain the model outputs. This short +winter window is useful for learning how to run and continue a simulation; +thermal time accumulates slowly and LAI stays small. The +[homepage](../../index.md) and [plotting guide](../../guides/data/outputs_plotting.md) +show longer or more varied runs. ```@example journey_one_object simulation = run!(model; steps=30, outputs=:all) @@ -67,7 +62,8 @@ evolution = DataFrame( vcat(first(evolution, 3), last(evolution, 3)) ``` -The table is retained history. The latest values are also available directly, +`TT_cu` is cumulative thermal time in °C d; LAI is leaf area per ground area +in m² m⁻². The table is retained history. The latest values are also available directly, whether or not history was requested: ```@example journey_one_object @@ -81,6 +77,9 @@ state_at_day_30 = final_state(simulation) ) ``` +`aPPFD` is absorbed PAR in μmol m⁻² of ground s⁻¹, averaged over the daily +forcing interval. It is not a flux per unit leaf area. + PlantSimEngine inferred both status connections because each has one unambiguous producer on the same object. This focused diagnostic shows the resolved sources and the live reference carriers: @@ -104,6 +103,15 @@ state_at_day_31 = final_state(simulation) (current_step=current_step(simulation), TT_cu=state_at_day_31.TT_cu) ``` +You have now extended the same history to day 31. Continue with +[several independent objects](several_objects.md), or +[plot the results](../../guides/data/outputs_plotting.md). + +!!! tip "Optional numerical choices" + If your study needs `Float32` or uncertainty values, see + [Numerical Reliability](@ref). Those choices are independent of the + coupling and output steps introduced here. + ## Page recap - **You added:** three models, supplied weather, a 30-step run, and retained @@ -115,5 +123,3 @@ state_at_day_31 = final_state(simulation) - **New API names:** `CompositeModel`, `run!`, `Simulation`, `final_state`, `collect_outputs`, `outputs`, `current_step`, `step!`, and `Diagnostics.explain_bindings`. -- **Optional status policies:** `type_promotion` for a general type mapping and - `status_transform` for a variable-specific conversion. diff --git a/docs/src/journeys/users/one_plant.md b/docs/src/journeys/users/one_plant.md index cbb13eae8..046cdb70b 100644 --- a/docs/src/journeys/users/one_plant.md +++ b/docs/src/journeys/users/one_plant.md @@ -2,8 +2,10 @@ ## New concept: topology and cross-object values -The previous journey used independent objects at one scale. A multiscale plant -adds parent/child topology: one plant object owns two leaf objects. +Share an absorbed-light supply between two leaves, then compute how their +surfaces contribute to the plant total. This teaching example adds a +parent/child structure: one plant object owns two leaf objects. The leaf +surfaces and radiation values are illustrative. The scope picture for this page is: @@ -20,6 +22,13 @@ the plant application. Start with leaf surfaces and total plant surface supplied as status. The only new value connection sends the plant-level absorbed light to each leaf. +We use one **common reference ground area** for the whole plant. Its supplied +`aPPFD` is 120 μmol m⁻² of reference ground s⁻¹. Each leaf receives a share in +proportion to its area: `120 × 1/3 = 40` and `120 × 2/3 = 80`, on that same +ground-area basis. These contributions can be added to recover 120. They are +not photon flux densities per unit leaf area. This deliberately simple share +does not calculate shading or 3D light interception. + ```@example journey_one_plant using PlantSimEngine, DataFrames using PlantSimEngine.Examples @@ -75,6 +84,11 @@ scalar_states = final_state(scalar_simulation, Many(scale=:Leaf)) Dict(id => state.aPPFD for (id, state) in scalar_states) ``` +The surfaces are in m² of leaves. To obtain a mean leaf-area photon flux from +one contribution, multiply it by the plant's reference ground area and divide +by that leaf's area. Such a change of basis belongs in an explicit model when +coupling to a leaf photosynthesis model; see [Coupling models](../../guides/coupling.md). + The leaf model reads its own `surface` directly from each leaf status. `SelfPlant()` makes the other two scalar sources plant-local: @@ -95,6 +109,10 @@ Now replace the supplied surfaces with two existing models: - `ToyLeafSurfaceModel` computes each leaf surface from its carbon biomass; - `ToyPlantLeafSurfaceModel` sums those leaf surfaces on the plant. +Here the leaf carbon biomasses are 50 and 100 g C, and the specific leaf area +is 0.02 m² g C⁻¹. Their calculated areas are therefore 1 and 2 m², preserving +the light shares from the first pass. + This is the first vector-like cross-object input. It comes after the scalar connection above, and differs only in the new `:leaf_surfaces` binding. @@ -174,7 +192,12 @@ leaf_states = final_state(computed_simulation, Many(scale=:Leaf)) ) ``` -The plant aggregation uses a live `RefVector`; the scalar connections remain +The resulting plant surface should be 3 m² and the light contributions should +still be 40 and 80 μmol m⁻² of reference ground s⁻¹. You can now +[reuse this configuration on several plants](several_plants.md). + +The following optional diagnostic shows that the plant reads its own leaves. +Its `RefVector` is a collection of live values; individual scalar inputs use single references: ```@example journey_one_plant diff --git a/docs/src/journeys/users/several_objects.md b/docs/src/journeys/users/several_objects.md index 19fba3432..456084b26 100644 --- a/docs/src/journeys/users/several_objects.md +++ b/docs/src/journeys/users/several_objects.md @@ -2,76 +2,74 @@ ## New concept: stable object identity and `Many` -The previous page ran one model chain on one automatically created object. The -smallest extension is to create two same-scale objects and target both with one -reusable `Many` selector. +Run the same teaching models on two independent canopies with different +initial development stages. Each canopy is one object; `Many(scale=:Canopy)` +selects both. This lets you compare the two canopies without writing a loop +inside any process model. The selected initial thermal times make their +different leaf area indices visible even over this short, five-day run. ```@example journey_several_objects using PlantSimEngine, PlantMeteo, Dates, DataFrames using PlantSimEngine.Examples weather = read_weather( - joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"); + joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), + :Ri_SW_f => (x -> x .* 1e6 ./ 86_400) => :Ri_SW_f, + :Ri_PAR_f => (x -> x .* 1e6 ./ 86_400) => :Ri_PAR_f, + :Ri_NIR_f => (x -> x .* 1e6 ./ 86_400) => :Ri_NIR_f; duration=Day, ) -plants = ( +canopies = ( Object( - :plant_a; - scale=:Plant, - kind=:plant, - status=Status(TT_cu=0.0), + :canopy_a; + scale=:Canopy, + kind=:canopy, + status=Status(TT_cu=600.0), ), Object( - :plant_b; - scale=:Plant, - kind=:plant, - status=Status(TT_cu=200.0), + :canopy_b; + scale=:Canopy, + kind=:canopy, + status=Status(TT_cu=900.0), ), ) -plant_targets = Many(scale=:Plant) +canopy_targets = Many(scale=:Canopy) model = CompositeModel( - plants...; + canopies...; applications=( ModelSpec( ToyDegreeDaysCumulModel(); name=:degree_days, - on=plant_targets, + on=canopy_targets, ), - ModelSpec(ToyLAIModel(); name=:lai, on=plant_targets), - ModelSpec(Beer(0.6); name=:light, on=plant_targets), + ModelSpec(ToyLAIModel(); name=:lai, on=canopy_targets), + ModelSpec(Beer(0.6); name=:light, on=canopy_targets), ), environment=weather, ) ``` -`:plant_a` and `:plant_b` are stable object identities. Their initial +`:canopy_a` and `:canopy_b` are stable object identities. Their initial cumulative thermal times differ, but the same three model kernels execute for -both. The model implementations contain no loop over plants. - -The application diagnostic confirms that each application compiled to both -objects: - -```@example journey_several_objects -select( - DataFrame(Diagnostics.explain_applications(model)), - :application_id, - :target_ids, -) -``` +both. The model implementations contain no loop over canopies. Run five steps and inspect each independent final status: ```@example journey_several_objects simulation = run!(model; steps=5, outputs=:all) -states = final_state(simulation, Many(scale=:Plant)) +states = final_state(simulation, Many(scale=:Canopy)) Dict( id => (TT_cu=state.TT_cu, LAI=state.LAI, aPPFD=state.aPPFD) for (id, state) in states ) ``` +LAI is in m² of leaves per m² of ground; `aPPFD` is in μmol of absorbed PAR +per m² of ground per second. The CSV radiation totals were converted to mean +fluxes just as in the one-object example. + Retained streams are keyed by application, object, and variable, so the two objects do not overwrite one another: @@ -86,16 +84,27 @@ lai_rows = rows[rows.variable .== :LAI, [ first(lai_rows, 6) ``` +The application diagnostic confirms that each application compiled to both +objects: + +```@example journey_several_objects +select( + DataFrame(Diagnostics.explain_applications(model)), + :application_id, + :target_ids, +) +``` + This remains a same-scale simulation. Parent/child topology and cross-object -value selection are introduced on the next journey, after independent object -execution is established. +value selection are introduced in [one multiscale plant](one_plant.md). To compare the time +series visually, follow [Collecting And Plotting Outputs](../../guides/data/outputs_plotting.md). ## Page recap - **You added:** two explicit `Object`s, stable ids, one shared `Many` selector, and named `ModelSpec` applications. - **PlantSimEngine inferred:** two targets per application plus independent - same-object `TT_cu` and `LAI` connections for each plant. + same-object `TT_cu` and `LAI` connections for each canopy. - **You keep explicit:** which objects exist, their initial status, application names, and the selector describing the target set. - **New API names:** `Object`, `Status`, `ModelSpec`, `Many`, and diff --git a/docs/src/journeys/users/several_plants.md b/docs/src/journeys/users/several_plants.md index a468532c1..5e76706d9 100644 --- a/docs/src/journeys/users/several_plants.md +++ b/docs/src/journeys/users/several_plants.md @@ -2,9 +2,16 @@ ## New concept: templates, instances, and overrides -The previous journey configured one plant explicitly. Its three applications -can become a `CompositeModelTemplate`, then be mounted on several independent -object topologies without duplicating that model configuration. +Apply the configuration from [one multiscale plant](one_plant.md) to two plants, +then change the specific leaf area of a third. A `CompositeModelTemplate` +stores the reusable model configuration; each `ObjectInstance` supplies the +actual plant and its leaves. + +As in that teaching example, light values are contributions per m² of a +plant's reference ground area per second, in μmol of absorbed PAR. A plant's +leaf contributions share that basis and can be added within the plant. +They are not fluxes per unit leaf area. Combining plants with different +reference areas would require an explicit area conversion first. ```@example journey_several_plants using PlantSimEngine, DataFrames @@ -115,10 +122,10 @@ plant_states = final_state(simulation, Many(scale=:Plant)) Dict(id => (surface=state.surface, aPPFD=state.aPPFD) for (id, state) in plant_states) ``` -Plant A aggregates surfaces `1 + 2 = 3`; plant B aggregates `1 + 1 = 2`. +Plant A aggregates surfaces `1 + 2 = 3 m²`; plant B aggregates `1 + 1 = 2 m²`. Those totals prove that `Subtree()` did not mix leaves between instances. -Likewise, each pair of leaf-level light outputs sums to its own plant's -supplied light: +Likewise, each pair of leaf light contributions sums to its own plant's +supplied flux on that plant's common ground-area basis: ```@example journey_several_plants leaf_states = final_state(simulation, Many(scale=:Leaf)) @@ -188,6 +195,10 @@ override_state = final_state(override_simulation, One(scale=:Plant)) override_state.surface ``` +The third plant has 6 m² of leaves: twice the area at the original specific +leaf area, for the same supplied carbon biomass. This is a parameter comparison +within the teaching model, not a calibrated species comparison. + There is no `SceneScope()` in this example because nothing is deliberately shared between plants. Introduce scene-wide scope only when adding a real shared source, such as a soil object or scene-level forcing controller. diff --git a/docs/src/prerequisites/installing_plantsimengine.md b/docs/src/prerequisites/installing_plantsimengine.md index 67c135dbf..d553d0373 100644 --- a/docs/src/prerequisites/installing_plantsimengine.md +++ b/docs/src/prerequisites/installing_plantsimengine.md @@ -1,23 +1,41 @@ # Installing PlantSimEngine +!!! compat "Use the package version described by this manual" + These development pages use the `CompositeModel` API. Registered releases + through 0.14.1 use the previous mapping API. Follow the commands below to + use the development version, or use the documentation matching your + installed release. For a pull-request preview, replace `"main"` with the + branch or commit shown by that pull request so its examples and package + sources match. + Install Julia from the [official download page](https://julialang.org/downloads/), create a project -environment, and add PlantSimEngine: +environment, and install the packages used in the tutorials: ```julia using Pkg Pkg.activate("my_simulation") -Pkg.add("PlantSimEngine") +Pkg.add(["PlantMeteo", "DataFrames", "CairoMakie"]) +Pkg.add(url="https://github.com/VirtualPlantLab/PlantSimEngine.jl", rev="main") ``` -Most simulations also use PlantMeteo: +PlantMeteo supplies weather data, DataFrames organizes the results, and +CairoMakie draws the tutorial figures. Run these commands in the Julia REPL; +`my_simulation` is the project directory created relative to your current +working directory. In a later session, run `Pkg.activate("my_simulation")` +from the same location before using the project again. -```julia -Pkg.add("PlantMeteo") -``` +For reproducible work, record the package revision and keep the generated +`Project.toml` and `Manifest.toml` with your experiment. The development branch +can change; a commit revision selects one exact version. ## First Simulation +This small example calculates how much light a canopy absorbs. Its leaf area +index is 2 m² of leaves per m² of ground, and incoming photosynthetically +active radiation (PAR) is 500 W m⁻² of ground. The canopy is one simulated +object; individual leaves are not represented. + ```@example install using PlantSimEngine, PlantMeteo, Dates using PlantSimEngine.Examples @@ -33,17 +51,22 @@ meteo = Atmosphere( model = CompositeModel( Beer(0.5); status=(LAI=2.0,), - id=:leaf, - scale=:Leaf, + id=:canopy, + scale=:Canopy, environment=meteo, ) simulation = run!(model) -final_state(simulation, One(scale=:Leaf)).aPPFD +(absorbed_PAR_umol_m2_ground_s=final_state(simulation).aPPFD,) ``` Example models are provided by the `PlantSimEngine.Examples` submodule. They are useful for learning and tests but are not part of the core modeling API. +The result is absorbed photon flux in μmol m⁻² of ground s⁻¹. The inputs are +illustrative, not a calibrated crop scenario. Next, +[couple three models over time](../journeys/users/one_object.md), then +[collect and plot their outputs](../guides/data/outputs_plotting.md). + For local package development, use `Pkg.develop(path="...")`. Run the package tests with `Pkg.test("PlantSimEngine")`. diff --git a/docs/src/prerequisites/key_concepts.md b/docs/src/prerequisites/key_concepts.md deleted file mode 100644 index 2f254e75c..000000000 --- a/docs/src/prerequisites/key_concepts.md +++ /dev/null @@ -1,108 +0,0 @@ -# Key Concepts - -## Processes And Models - -A process identifies a biological or physical phenomenon, such as -photosynthesis, growth, water balance, or energy balance. A model is one -implementation of a process. - -Models subtype `AbstractModel` and declare: - -- `inputs_`: values read from object status, each declared as `Required(T)` or - `Default(value)`; -- `outputs_`: values written to object status; -- `environment_inputs_`: values sampled from the environment; -- `environment_outputs_`: environment variables a controller may commit; -- `commit_environment!`: accepted meteorological state committed to a mutable - environment by controller models; -- `dep`: processes called manually by the model, when required. - -The numerical kernel is implemented with: - -```julia -PlantSimEngine.run!(model, status, environment, constants, context) -``` - -`Required(T)` describes an input that must be supplied by object state or -another application. `Default(value)` is a true model fallback that -PlantSimEngine can initialize automatically. Output literals are initial -output-state values. - -## Composite Models And Objects - -A `CompositeModel` contains objects and model applications. An `Object` can represent a -model, plant, soil volume, axis, internode, leaf, sensor, or any other simulated -entity. PlantSimEngine does not impose one plant architecture. - -Objects can carry: - -- a stable identifier; -- scale, kind, species, and name metadata; -- parent-child relationships; -- geometry and position; -- mutable `Status`; -- object-local model applications. - -`CompositeModelTemplate` packages reusable applications for a species or object type. -`ObjectInstance` mounts the template in a model. Several instances can share -models and parameters while declaring targeted overrides for exceptional -objects. - -## Model Applications - -`ModelSpec` configures one use of a model: - -- `ModelSpec(...; on=...)` selects target objects; -- `ModelSpec(...; inputs=...)` selects producers for value dependencies; -- `ModelSpec(...; calls=...)` binds manually controlled model calls; -- `ModelSpec(...; every=...)` selects the execution cadence; -- `Environment(...)` configures environment sampling; -- `Updates(...; after=:application_id)` orders intentional additional writers; -- `ModelSpec(...; output_routing=...)` controls output publication. - -This keeps model implementations generic. Models do not need to know which -model, object, timestep, or coupling scenario will use them. - -## Soft And Manual Dependencies - -Ordinary dependencies are inferred by matching model inputs with outputs and -are compiled into an acyclic execution order. `ModelSpec(...; inputs=...)` is used when the -source is cross-object, renamed, temporal, or otherwise ambiguous. - -Some algorithms need direct call-stack control. For example, a model energy -balance may repeatedly call leaf energy-balance models until canopy -microclimate converges. Such dependencies are bound with `ModelSpec(...; calls=...)`; the -parent invokes them with `run_call!`. - -## Status And References - -`Status` stores variables in references. Same-rate coupling normally shares -those references instead of copying values. A many-object input uses a -reference vector, so aggregation models read current source values directly. - -Temporal coupling uses published streams when producer and consumer clocks -differ. Policies include `HoldLast`, `Interpolate`, `Integrate`, `Aggregate`, -and `PreviousTimeStep`. - -## Environment - -The active environment backend may be: - -- one constant atmosphere shared by all objects; -- a time-indexed weather table; -- a mutable layer, voxel, grid, or octree microclimate. - -Object-to-environment support is compiled and cached. Geometry changes mark the -binding dirty so it can be refreshed without recomputing spatial lookup at -every timestep. - -## Multiscale Plant Structure - -PlantSimEngine treats scale and object hierarchy as scenario data. A plant may -use leaves directly under a plant, or axes, segments, internodes, roots, and -other intermediate levels. Selectors express relationships such as one source, -many descendants, the current plant, an ancestor, or all matching objects in -the model. - -MultiScaleTreeGraph objects can be imported with `objects_from_mtg`, but the -runtime operates on the same composite-model/object representation afterward. diff --git a/docs/src/step_by_step/detailed_first_example.md b/docs/src/step_by_step/detailed_first_example.md deleted file mode 100644 index 5a762b2dd..000000000 --- a/docs/src/step_by_step/detailed_first_example.md +++ /dev/null @@ -1,225 +0,0 @@ -# [Detailed Walkthrough Of A Simple Simulation](@id detailed-walkthrough-of-a-simple-simulation) - -This page walks through a small composite-model/object simulation. It is written for -readers who are still getting comfortable with Julia and PlantSimEngine. - -If you only want examples to copy and modify, see [Quick examples](quick_and_dirty_examples.md). For -multi-object and multi-plant simulations, the same API scales up: add objects, -select them with `ModelSpec(...; on=...)`, connect values with `ModelSpec(...; inputs=...)`, and use -`ModelSpec(...; calls=...)` when a parent model must manually run child models. - -```@setup detailed_scene -using PlantSimEngine, PlantMeteo, Dates, DataFrames -using PlantSimEngine.Examples - -meteo_day = read_weather( - joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"); - duration=Dates.Day, -) -``` - -```@contents -Pages = ["detailed_first_example.md"] -Depth = 3 -``` - -## Setting Up Your Environment - -Every script needs a Julia environment with PlantSimEngine installed. Most -examples also use companion packages such as PlantMeteo for weather data and -DataFrames for tabular outputs. Installation details are in -[Installing PlantSimEngine](../prerequisites/installing_plantsimengine.md). - -## The Simulation Pieces - -### Processes And Models - -A process is something you want to simulate, such as light interception, -photosynthesis, water flux, growth, yield, or energy balance. - -A model is one implementation of a process. In this page we use the example -`Beer` model, which implements a Beer-Lambert light-interception equation. -Its only parameter is the extinction coefficient `k`. - -```@example detailed_scene -fieldnames(Beer) -``` - -The model implementation declares the status variables it reads and writes: - -```@example detailed_scene -inputs(Beer(0.5)) -``` - -```@example detailed_scene -outputs(Beer(0.5)) -``` - -These declarations are the modeler's contract. The composite-model/object layer decides -where the model runs and where those values come from. - -### CompositeModel Objects - -A `CompositeModel` contains simulated `Object`s. An object can represent a model, plant, -axis, leaf, soil layer, sensor, voxel, or any other simulated entity. - -For a first example, we use one object representing the whole model. The `Beer` -model reads `LAI`, so we initialize that variable on the object status. - -```@example detailed_scene -model = CompositeModel( - Beer(0.5); - status=(LAI=2.0,), - environment=meteo_day, - timestep=Day(1), -); -nothing -``` - -The concise constructor creates one ordinary model object and one application -for each supplied model. `status` initializes that object, `timestep` applies a -common daily cadence, and `environment` supplies weather values such as -radiation. Use explicit `ModelSpec` and selectors when applications need -different policies or targets. - -## Inspecting The Compiled CompositeModel - -Before runtime, PlantSimEngine resolves selectors and builds a compiled model. -This avoids resolving object selections inside the timestep loop. - -```@example detailed_scene -select( - DataFrame(Diagnostics.explain_applications(model)), - :application_id, - :process, - :target_ids, -) -``` - -`Beer` has no model-to-model value input in this first model because `LAI` was -initialized directly on the object status: - -```@example detailed_scene -Diagnostics.explain_bindings(model) -``` - -The schedule tells us when each application runs: - -```@example detailed_scene -select( - DataFrame(Diagnostics.explain_schedule(model)), - :application_id, - :dt_seconds, - :root_scheduled, - :manual_call_only, -) -``` - -## Running The Simulation - -Run the model with [`run!`](@ref): - -```@example detailed_scene -sim = run!(model; steps=3, outputs=:all) -nothing -``` - -Final state is available independently of retained output history: - -```@example detailed_scene -scene_status = final_state(sim) -(LAI=scene_status.LAI, aPPFD=scene_status.aPPFD) -``` - -The returned `Simulation` stores retained output streams: - -```@example detailed_scene -first(collect_outputs(sim; sink=nothing), 3) -``` - -For a table, use the default `DataFrame` sink: - -```@example detailed_scene -first(collect_outputs(sim), 3) -``` - -## Adding A Model Coupling - -Now let a daily LAI model compute `LAI` before the light-interception model -runs. `ToyLAIModel` reads cumulative thermal time `TT_cu` and writes `LAI`. -Because `Beer` reads `LAI`, the compiler can infer the same-object binding. - -```@example detailed_scene -coupled_scene = CompositeModel( - ToyDegreeDaysCumulModel(), - ToyLAIModel(), - Beer(0.5); - status=(TT_cu=0.0,), - environment=meteo_day, - timestep=Day(1), -) - -select( - DataFrame(Diagnostics.explain_bindings(coupled_scene)), - :application_id, - :input, - :source_application_ids, - :carrier_kind, - :copy_semantics, -) -``` - -The `LAI` binding uses a live reference carrier, so the light-interception -model sees the value written by the LAI model without copying it. - -Run the coupled model: - -```@example detailed_scene -coupled_sim = run!(coupled_scene; steps=5, outputs=:all) -first(collect_outputs(coupled_sim), 8) -``` - -The final object status contains the latest values from the coupled models: - -```@example detailed_scene -coupled_status = final_state(coupled_sim) -(TT_cu=coupled_status.TT_cu, LAI=coupled_status.LAI, aPPFD=coupled_status.aPPFD) -``` - -## What Needs Initialization? - -Model `inputs_(...)` explicitly distinguishes `Required(T)` from -`Default(value)`. A required input needs user state or a producer binding; a -defaulted input needs neither. In a coupled model, an upstream application can -satisfy a required input. - -Use the compiler explanations to distinguish the two cases: - -- `:supplied` means the object `Status` already provides the value; -- `:producer_bound` means another application supplies it; -- `:defaulted` means `Default(value)` initialized it; -- `:required` means it still has no source and compilation will fail. - -For example, if we remove `TT_cu` from the model status, compilation fails -because no model in this model computes it before `ToyLAIModel` reads it: - -```@example detailed_scene -bad_scene = CompositeModel( - ToyLAIModel(); - environment=meteo_day, -) - -try - Diagnostics.explain_bindings(bad_scene) -catch err - first(sprint(showerror, err), 300) -end -``` - -## Next Steps - -- [Standard model coupling](@ref) shows more coupling patterns. -- [CompositeModel/Object Quickstart](../composite_model/quickstart.md) is the shortest - copy-pasteable path for the new API. -- [Model execution](../model_execution.md) explains scheduling, temporal inputs, hard calls, - output retention, and lifecycle refreshes. diff --git a/docs/src/step_by_step/implement_a_model.md b/docs/src/step_by_step/implement_a_model.md deleted file mode 100644 index f2a63d5d3..000000000 --- a/docs/src/step_by_step/implement_a_model.md +++ /dev/null @@ -1,31 +0,0 @@ -# [Implementing a model](@id model_implementation_page) - -The canonical, executable model-authoring tutorial is now -[Implement a basic model](@ref). Start there for the complete five-argument -kernel contract, direct testing, same-object coupling, and reuse over several -objects. - -Before declaring a new abstract process type, use -[New process or new model?](@ref) to determine whether the implementation is a -new scientific process or another hypothesis for an existing process. - -Then continue according to the model's needs: - -- [Port an existing model](@ref) explains how to keep a scientific kernel - readable and generic; -- [Model repository layout and tests](@ref) places the process, model, - documentation, and test levels in a package; -- [Implement Cross-Object Values](@ref) covers `One`, `OptionalOne`, - `Many`, and `Subtree()`; -- [Coupling models](@ref) distinguishes value coupling, hard calls, and - explicit adapters; -- [Model compatibility and replacement](@ref) distinguishes process identity - from drop-in substitutability; -- [Model Traits](@ref) documents environment, timing, output, and scientific - variable contracts. - -!!! compat - Older versions of this page showed a hard dependency as an abstract model - type returned directly by `dep(model)`. The current API declares a - `Call(selector)`, for example - `(stomata=Call(One(process=:stomatal_conductance)),)`. diff --git a/docs/src/step_by_step/implement_a_model_additional.md b/docs/src/step_by_step/implement_a_model_additional.md deleted file mode 100644 index 80822b10f..000000000 --- a/docs/src/step_by_step/implement_a_model_additional.md +++ /dev/null @@ -1,113 +0,0 @@ -# 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. - -```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`. - -## Promoting model constructor arguments - -When implementing a new model, you can do a little optional extra work to help future users. - -You can add a constructor that uses Julia's `promote` function. This is -promotion of model parameters while constructing the model; it is distinct -from the `CompositeModel(...; type_promotion=...)` policy that converts status -values for a scenario. See [Numerical Reliability](@ref) for that status -policy. - -Constructor promotion would not make any difference for the previous `Beer` -example because it has only one parameter. Consider instead a `Beer2` model -with 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 -``` - -!!! note - `promote` returns a tuple, which is splatted into the constructor above. - See the [Julia documentation](https://docs.julialang.org/en/v1/manual/conversion-and-promotion/#Promotion) - for a more in-depth explanation. - -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. - -In the example above, `0.6` for `k` is a `Float64`, while `2` for `x` is an -`Int`. Constructor promotion converts both arguments to a common type when -possible. In this case it converts `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: - -```julia -Beer(k = 0.7) -``` - -This helps readability when there are a lot of parameters and some have default values. - -### eltype - -The last optional utility function to implement is a method for the `eltype` function: - -```julia -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. diff --git a/docs/src/step_by_step/implement_a_process.md b/docs/src/step_by_step/implement_a_process.md index 71b65c416..8894ba07d 100644 --- a/docs/src/step_by_step/implement_a_process.md +++ b/docs/src/step_by_step/implement_a_process.md @@ -1,63 +1,64 @@ # New process or new model? -A process names a biological or physical question. A concrete model is one -hypothesis, formulation, or scale at which that process is computed. Make this -choice before writing the model type: it determines whether users can discover -your implementation beside existing alternatives. +A **process** names a biological or physical question. A **model** is one +hypothesis or formulation used to answer it. For example, two photosynthesis +equations can belong to the same process even if they need different inputs. -## Prefer an existing process +Before creating a model, check whether the process already exists. That lets +users find your implementation beside the alternatives they may want to compare. -Start by loading the packages that may own the process and inspect what they -declare: +## Find the existing family + +Load the package that owns the models. This example uses the teaching models +distributed with PlantSimEngine: ```@example choose_process using PlantSimEngine +using PlantSimEngine.Examples -Authoring.available_processes() +growth_models = Authoring.available_models(AbstractGrowthModel) +growth_models ``` -`Authoring.available_processes()` can only see loaded Julia modules. Search the source of -the target package as well before deciding that a process is missing. The -[Loaded model catalog](@ref) shows the same discovery result grouped into a -generated table with provenance and completeness. - -Reuse an existing abstract process type when the new implementation answers -the same scientific question. Examples include two photosynthesis -formulations, a simple and a water-stress-aware radiation-use-efficiency model, -or the same process represented at different scales. Give each hypothesis its -own concrete model instead of adding a `method=:a_or_b` switch to one large -kernel. - -Models in the same process form a scientific family, but they are not -necessarily interchangeable. They may require different inputs, produce -different outputs, use different clocks, or declare different -`VariableContract`s. See [Model compatibility and replacement](@ref) before -using one as an `Override` or replacing it without revisiting scenario -bindings. - -## Declare a process only for a new meaning +For a scientific application, load its model package first and inspect its +documentation. `Authoring.available_processes()` lists the process types +visible in the loaded modules. `Authoring.available_models(process_type)` +then lists that family's concrete model types. -Create a process when no existing process has the same biological or physical -meaning: +Choose a candidate and inspect an actual parameterized instance: ```@example choose_process -PlantSimEngine.@process "docs_root_exudation" verbose=false - -abstract = AbstractDocs_Root_ExudationModel -abstract <: AbstractModel +candidate = ToyRUEGrowthModel(0.2) +( + process=process(candidate), + inputs=inputs(candidate), + outputs=outputs(candidate), +) ``` -The generated abstract type is formed by prefixing `Abstract`, preserving word -boundaries from the process name, and appending `Model`. For example: +[Loaded model catalog](@ref) explains discovery and inspection in more detail. +Discovery only sees packages loaded into Julia; also check the target +package's source and documentation before concluding that a process is absent. -| Declaration | Generated abstract type | +## Decide what your new equation changes + +| Your change | What to create | |---|---| -| `@process "growth"` | `AbstractGrowthModel` | -| `@process "light_interception"` | `AbstractLight_InterceptionModel` | +| Another equation, parameterization, assumption, or resolution for the same question | A concrete model under the existing process | +| A distinct biological or physical question | A new process and its model | +| A conversion of units, basis, or aggregation between models | An explicit adapter model | + +Give alternative hypotheses separate model types so users can select and test +them. Models in one process may have different inputs, outputs, or cadences. +Use [Model compatibility and replacement](@ref) before substituting one. -Concrete implementations subtype the generated abstract type: +## Declare a genuinely new process + +This declaration creates a process for a teaching example of root exudation: ```@example choose_process +PlantSimEngine.@process "docs_root_exudation" verbose=false + struct DocsLinearExudation{T} <: AbstractDocs_Root_ExudationModel fraction::T end @@ -65,43 +66,16 @@ end process(DocsLinearExudation(0.1)) ``` -The macro generates process identity and the abstract type; it does not choose -ports, units, parameters, defaults, equations, or scientific validation for -you. Continue with [Implement a basic model](@ref), which is the canonical -model-authoring path. +`@process` creates the abstract type that concrete models inherit from: -## A practical decision test - -Ask these questions in order: - -1. Is the quantity being simulated and its scientific meaning already - represented by a loaded process? -2. Would users reasonably compare this implementation with the existing - implementations as alternative hypotheses? -3. Is the difference only an equation, assumption, parameterization, scale, or - resolution of that same question? - -If the answers point to the same question, add a model to the existing -process. Create a new process only when the meaning itself changes. If the new -model changes units or basis while connecting two existing meanings, implement -an explicit adapter model instead; see [Coupling models](@ref). - -## Without the macro - -`@process` is a small convenience. The equivalent manual declaration is: - -```@example choose_process -abstract type AbstractDocsManualRootExudationModel <: - PlantSimEngine.AbstractModel end -PlantSimEngine.process_( - ::Type{AbstractDocsManualRootExudationModel}, -) = :docs_manual_root_exudation - -struct DocsManualRootExudation <: AbstractDocsManualRootExudationModel end +| Declaration | Generated abstract type | +|---|---| +| `@process "growth"` | `AbstractGrowthModel` | +| `@process "light_interception"` | `AbstractLight_InterceptionModel` | -manual_process = process(DocsManualRootExudation()) -@assert manual_process == :docs_manual_root_exudation -manual_process -``` +When another package already declares the process, import its abstract type +and subtype it. Avoid declaring a second identity with the same meaning. -Prefer the macro for ordinary package code so process naming stays consistent. +The type above is only the beginning. It still needs input and output +declarations, scientific contracts, an equation, and tests. Continue with +[Implement a basic model](@ref) to complete those steps. diff --git a/docs/src/step_by_step/model_switching.md b/docs/src/step_by_step/model_switching.md index c593a8031..9fb91131e 100644 --- a/docs/src/step_by_step/model_switching.md +++ b/docs/src/step_by_step/model_switching.md @@ -1,188 +1,135 @@ # Model compatibility and replacement -```@setup scene_model_switching -using PlantSimEngine, PlantMeteo, Dates, DataFrames -using PlantSimEngine.Examples +Suppose daily carbon gain increases with intercepted light. We want to compare +a linear response with a saturating response, then add a soil-water limitation. +All three models answer the same scientific question, but the last needs an +extra input. -meteo_day = read_weather( - joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"); - duration=Dates.Day, -) -``` - -One main objective of PlantSimEngine is to compare and switch model -implementations for a process without changing the engine or unrelated model -kernels. Process identity and substitutability are nevertheless different -claims: - -1. **Same process:** two models answer the same scientific question. -2. **Scenario-compatible replacement:** the new model supplies every value - required by the current consumers and its own inputs can be bound. -3. **Drop-in replacement:** process, status and environment ports, scientific - contracts, dependencies, and relevant traits are compatible without - changing scenario wiring. - -Only the third level is suitable for an `Override`, whose applications share -one logical interface. Models may belong to the same process while using -different inputs or producing additional outputs; this is useful scientific -variation, not an error. - -At the model-application layer, replace the model inside a `ModelSpec`, keep -the same `ModelSpec(...; on=...)` selector, then revalidate every binding and -consumer affected by the changed interface. +This example uses teaching models with arbitrary coefficients. They demonstrate +replacement and input checks; their outputs are not predictions for a crop. -## A first simulation - -This model computes degree-days, LAI, absorbed PAR, and growth on one model -object: +## Load one family of alternatives ```@example scene_model_switching -function plant_model_with_growth(growth_model; growth_name=:growth) - CompositeModel( - Object(:scene; scale=:Scene, kind=:scene); - applications=( - ModelSpec(ToyDegreeDaysCumulModel(); name=:degree_days, on=One(scale=:Scene), every=Day(1)), - - ModelSpec(ToyLAIModel(); name=:lai, on=One(scale=:Scene), every=Day(1)), - - ModelSpec(Beer(0.5); name=:light_interception, on=One(scale=:Scene), every=Day(1)), +using Dates, Test, PlantSimEngine - ModelSpec(growth_model; name=growth_name, on=One(scale=:Scene), every=Day(1)), - ), - environment=meteo_day, - ) -end +include(joinpath( + pkgdir(PlantSimEngine), "skills", "plantsimengine", + "assets", "alternative-model.jl", +)) +using .AlternativeModelExample -rue_scene = plant_model_with_growth(ToyRUEGrowthModel(0.2)) -rue_sim = run!(rue_scene; steps=10) -rue_status = final_state(rue_sim) -(growth_model=:ToyRUEGrowthModel, biomass=rue_status.biomass) +linear = LinearCarbonGain(0.2) +saturating = SaturatingCarbonGain(10.0, 5.0) +water_limited = WaterLimitedCarbonGain(0.2) ``` -The compiler infers the same-object bindings from the model declarations. The -growth model reads `aPPFD`, which is produced by the light interception model: +| Model | Equation | Required inputs | +|---|---|---| +| Linear | efficiency × absorbed PAR | Absorbed PAR | +| Saturating | maximum × absorbed PAR / (half-saturation + absorbed PAR) | Absorbed PAR | +| Water-limited | efficiency × absorbed PAR × bounded soil-water fraction | Absorbed PAR and soil-water fraction | -```@example scene_model_switching -select( - DataFrame(Diagnostics.explain_bindings(rue_scene)), - :application_id, - :input, - :source_application_ids, - :origin, - :carrier_kind, -) -``` +Absorbed PAR is a daily total in mol photons per plant; carbon gain is a daily +total in g carbon per plant. The soil-water fraction is dimensionless. The +models declare these meanings in their variable contracts. -## Switching the growth model +## Check before changing a scenario -`ToyAssimGrowthModel` implements the same `:growth` process and reads the same -`aPPFD` input, but computes additional outputs such as carbon assimilation and -respiration. It is compatible with this small scenario because no downstream -model requires an output that disappeared. Its larger output interface means -that same process alone did not prove a strict drop-in replacement: +Compare the concrete model instances: ```@example scene_model_switching -assim_scene = plant_model_with_growth(ToyAssimGrowthModel()) -assim_sim = run!(assim_scene; steps=10) -assim_status = final_state(assim_sim) +compatible = Authoring.compare_models(linear, saturating) +needs_water = Authoring.compare_models(linear, water_limited) + +@test compatible.override_compatible +@test needs_water.requires_binding_changes ( - growth_model=:ToyAssimGrowthModel, - carbon_assimilation=assim_status.carbon_assimilation, - Rm=assim_status.Rm, - biomass=assim_status.biomass, + saturating_can_replace_directly=compatible.override_compatible, + water_limited_needs_new_inputs=needs_water.requires_binding_changes, ) ``` -The dependency graph and execution plan are rebuilt from the new application -set: +The first pair has a compatible complete interface. The water-limited model +belongs to the same process, but its extra `ftsw` input needs a value. -```@example scene_model_switching -select( - DataFrame(Diagnostics.explain_execution_plan(assim_sim)), - :application_id, - :object_ids, - :batch_size, - :inner_loop_dispatch, -) -``` +Three distinctions matter: -## Check before replacing +- **Same process:** the models answer the same scientific question. +- **Usable in this scenario:** the candidate supplies what consumers need and + can obtain all its own inputs. +- **Direct override:** the complete process, inputs, outputs, contracts, + dependencies, and relevant traits are compatible. -Use the public authoring report on the concrete instances: +Use `Override` only for the third case. For broader changes, replace the +model in a `ModelSpec` and update the affected inputs or other configuration. -```@example scene_model_switching -alternative_asset = joinpath( - pkgdir(PlantSimEngine), - "skills", - "plantsimengine", - "assets", - "alternative-model.jl", -) -include(alternative_asset) -using .AlternativeModelExample +## Compare the two light responses -drop_in = Authoring.compare_models( - LinearCarbonGain(0.2), - SaturatingCarbonGain(10.0, 5.0), -) -needs_binding = Authoring.compare_models( - LinearCarbonGain(0.2), - WaterLimitedCarbonGain(0.2), -) -( - drop_in=( - same_process=drop_in.same_process, - override_compatible=drop_in.override_compatible, - requires_reconfiguration=drop_in.requires_reconfiguration, - compatibility=drop_in.compatibility, - ), - water_limited=( - same_process=needs_binding.same_process, - override_compatible=needs_binding.override_compatible, - requires_binding_changes=needs_binding.requires_binding_changes, - requires_reconfiguration=needs_binding.requires_reconfiguration, - compatibility=needs_binding.compatibility, - ), -) -``` +Keep the objects, forcing, and timing identical: -`requires_binding_changes` is specific to ports, contracts, dependencies, -output policies, or model-level environment hints. `requires_reconfiguration` -is broader: it is true for any interface difference that prevents a direct -override, including a schedule-only trait change. +```@example scene_model_switching +function carbon_scenario(gain_model; values=(absorbed_par=10.0,)) + CompositeModel( + Object(:plant; scale=:Plant, status=Status(; values...)); + applications=( + ModelSpec(gain_model; name=:carbon_gain, on=One(scale=:Plant)), + ), + environment=(duration=Day(1),), + ) +end -The report compares: +linear_scene = carbon_scenario(linear) +saturating_scene = carbon_scenario(saturating) + +linear_gain = final_state(run!(linear_scene)).carbon_gain +saturating_gain = final_state(run!(saturating_scene)).carbon_gain +@test linear_gain == 2.0 +@test saturating_gain ≈ 100 / 15 +(linear=linear_gain, saturating=saturating_gain) +``` -- `process(model)`; -- required and defaulted status inputs, including declared types; -- local `outputs_` schemas, including initial values; -- environment inputs and outputs; -- complete `VariableContract`s; -- model-authored `Input`, `Call`, and `Initializer` dependencies; -- cadence and temporal output policies. +Only the selected hypothesis changed. At 10 mol of absorbed photons the +linear equation produces 2 g carbon and the saturating equation about +6.67 g carbon. These different outcomes reflect the chosen teaching +coefficients; interface compatibility does not imply equal results. -A variable with the same name but a different unit, basis, temporal meaning, -aggregation, or extent is incompatible. Add an explicit adapter rather than -weakening or omitting the contract. +## Supply the additional water input -After replacement, compile the candidate scenario and inspect: +For a controlled comparison, supply a soil-water fraction of 0.5 alongside +the same light input: ```@example scene_model_switching -candidate_validation = Authoring.validate_scenario(assim_scene) -( - scenario_valid=candidate_validation.valid, - initialization=Diagnostics.explain_initialization(assim_scene), - bindings=Diagnostics.explain_bindings(assim_scene), - schedule=Diagnostics.explain_schedule(assim_scene), +water_scene = carbon_scenario( + water_limited; + values=(absorbed_par=10.0, ftsw=0.5), ) +validation = Authoring.validate_scenario(water_scene) +@test validation.valid + +water_gain = final_state(run!(water_scene)).carbon_gain +@test water_gain == 1.0 +(linear=linear_gain, water_limited=water_gain) ``` -The compiler diagnostics prove that this concrete scenario can initialize and -route the replacement. They do not prove that two equations are scientifically -equivalent or valid over the same domain; that remains model documentation and -validation evidence. Distributed `outputs_to` destinations belong to the -`ModelSpec`, not `ModelInterface`, so revalidate them at this scenario level. +This is an explicitly supplied value. In a dynamic simulation, bind `ftsw` +to a soil model or forcing data using [Coupling models](@ref). Inspect +`Diagnostics.explain_initialization(water_scene)` to see how each required +input was supplied, then `Diagnostics.explain_bindings(water_scene)` for +connections to other model applications. + +## Keep physical meaning and scientific validation visible + +Two variables with the same name can still differ in units, area or mass +basis, temporal meaning, or aggregation. Add a named conversion model when +those meanings differ; see [explicit adapters](../guides/coupling.md). + +`Authoring.compare_models` reports differences in declarations. +`Authoring.validate_scenario` checks the proposed composition. Neither proves +that an equation or its parameterization is valid for your study. Compare +assumptions, domain of validity, reference data, and outputs as well. -Use `ObjectInstance(...; overrides=...)` only for a model that satisfies the -logical application's exact replacement contract. Otherwise create or replace -a complete `ModelSpec` and update the affected bindings explicitly. +For detailed reports, inspect `requires_binding_changes` for connection +changes and `requires_reconfiguration` for all differences that prevent a +direct override, including cadence changes. The [Public API](@ref) describes +the complete report. diff --git a/docs/src/step_by_step/quick_and_dirty_examples.md b/docs/src/step_by_step/quick_and_dirty_examples.md deleted file mode 100644 index be48327f0..000000000 --- a/docs/src/step_by_step/quick_and_dirty_examples.md +++ /dev/null @@ -1,121 +0,0 @@ -# Quick Examples - -This page is for copy-paste experimentation with the native composite-model/object API. -If you want a slower explanation of the same ideas, see -[Detailed Walkthrough Of A Simple Simulation](@ref detailed-walkthrough-of-a-simple-simulation). - -The examples use one model object, but the same pattern scales to plants, -organs, soil objects, and microclimate grids by adding more `Object`s and -selecting them with `ModelSpec(...; on=...)` and `ModelSpec(...; inputs=...)`. - -```@setup quick_model_examples -using PlantSimEngine, PlantMeteo, Dates, DataFrames -using PlantSimEngine.Examples - -meteo_day = read_weather( - joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"); - duration=Dates.Day, -) -``` - -```@contents -Pages = ["quick_and_dirty_examples.md"] -Depth = 2 -``` - -## One Light Interception Model - -```@example quick_model_examples -model = CompositeModel( - Beer(0.5); - status=(LAI=2.0,), - environment=meteo_day, -) - -sim = run!(model; steps=3, outputs=:all) -first(collect_outputs(sim), 3) -``` - -## LAI And Light Interception - -Here, `ToyDegreeDaysCumulModel` computes cumulative thermal time, `ToyLAIModel` -computes `LAI`, and `Beer` consumes `LAI`. The compiler infers the same-object -value bindings from model inputs and outputs. - -```@example quick_model_examples -lai_scene = CompositeModel( - ToyDegreeDaysCumulModel(), - ToyLAIModel(), - Beer(0.5); - environment=meteo_day, -) - -lai_sim = run!(lai_scene; steps=5, outputs=:all) -first(collect_outputs(lai_sim), 8) -``` - -Inspect the inferred coupling: - -```@example quick_model_examples -select( - DataFrame(Diagnostics.explain_bindings(lai_scene)), - :application_id, - :input, - :source_application_ids, - :carrier_kind, -) -``` - -## Add Biomass Growth - -`ToyRUEGrowthModel` consumes absorbed light and accumulates biomass. No extra -input binding is needed because `Beer` is the unique producer of `aPPFD` on the -same object. - -```@example quick_model_examples -growth_scene = CompositeModel( - ToyDegreeDaysCumulModel(), - ToyLAIModel(), - Beer(0.5), - ToyRUEGrowthModel(0.2); - environment=meteo_day, -) - -growth_sim = run!(growth_scene; steps=5) -growth_status = final_state(growth_sim) -(LAI=growth_status.LAI, aPPFD=growth_status.aPPFD, biomass=growth_status.biomass) -``` - -## Keep Only One Requested Output - -For larger simulations, request only the streams you want to keep: - -```@example quick_model_examples -request = OutputRequest( - :Scene, - :biomass; - name=:biomass_daily, - application=:growth, - policy=HoldLast(), - clock=Day(1), -) - -requested_sim = run!( - growth_scene; - steps=5, - outputs=request, -) - -first(collect_outputs(requested_sim, :biomass_daily), 5) -``` - -## PlantBiophysics - -The same composite-model/object API can host models from companion packages such as -PlantBiophysics. A typical PlantBiophysics energy-balance setup uses -`ModelSpec(...; calls=...)` so an iterative parent model can manually run photosynthesis and -stomatal-conductance models, then call `run_call!(target; publish=true)` once -for the accepted solution. - -See [MAESPA-style model example handoff](../dev/maespa_model_handoff.md) for -the current multi-plant energy-balance acceptance example. diff --git a/docs/src/step_by_step/simple_model_coupling.md b/docs/src/step_by_step/simple_model_coupling.md deleted file mode 100644 index 56882d6b2..000000000 --- a/docs/src/step_by_step/simple_model_coupling.md +++ /dev/null @@ -1,97 +0,0 @@ -# Standard model coupling - -```@setup scene_coupling -using PlantSimEngine -using PlantSimEngine.Examples -using PlantMeteo, Dates, DataFrames - -meteo_day = read_weather( - joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"); - duration=Dates.Day, -) -``` - -This page shows the standard coupling case: one model computes a variable that -another model reads. In the composite-model/object API, the user describes model -applications on objects, and the compiler wires the value dependencies. - -## Setting up your environment - -Make sure you have a working Julia environment with PlantSimEngine and the -recommended companion packages. Details are provided on the -[Installing PlantSimEngine](../prerequisites/installing_plantsimengine.md) -page. - -## One object and one model - -A model contains objects. A model application says where a model runs. Here a -light interception model runs on the model object, uses the environment's -daily cadence, and reads `LAI` from that object's status: - -```@example scene_coupling -light_scene = CompositeModel( - Beer(0.5); - status=(LAI=2.0,), - environment=meteo_day, -) - -light_sim = run!(light_scene; steps=3, outputs=:all) -first(collect_outputs(light_sim; sink=DataFrame), 3) -``` - -## Coupling two models - -Suppose we want `ToyLAIModel` to compute `LAI` for `Beer`. Both models can run -on the same object. `ToyLAIModel` produces `LAI`, and `Beer` declares `LAI` as -an input, so the model compiler infers the binding: - -```@example scene_coupling -coupled_scene = CompositeModel( - ToyDegreeDaysCumulModel(), - ToyLAIModel(), - Beer(0.5); - environment=meteo_day, -) - -select( - DataFrame(Diagnostics.explain_bindings(coupled_scene)), - :application_id, - :input, - :source_application_ids, - :origin, - :carrier_kind, - :copy_semantics, -) -``` - -The `:inferred_same_object` rows are soft dependencies: the consumer input is -provided by another model output. Same-rate local links use live references, so -the timestep loop does not copy values between models. - -Run the coupled model: - -```@example scene_coupling -coupled_sim = run!(coupled_scene; steps=5) -coupled_status = final_state(coupled_sim) -(TT_cu=coupled_status.TT_cu, LAI=coupled_status.LAI, aPPFD=coupled_status.aPPFD) -``` - -## Adding another model - -Additional models are just additional applications. `ToyRUEGrowthModel` -consumes `aPPFD`, which is produced by `Beer`, so the compiler infers another -same-object binding: - -```@example scene_coupling -growth_scene = CompositeModel( - ToyDegreeDaysCumulModel(), - ToyLAIModel(), - Beer(0.5), - ToyRUEGrowthModel(0.2); - environment=meteo_day, -) - -growth_sim = run!(growth_scene; steps=5) -growth_status = final_state(growth_sim) -(LAI=growth_status.LAI, aPPFD=growth_status.aPPFD, biomass=growth_status.biomass) -``` diff --git a/docs/src/troubleshooting/common_errors.md b/docs/src/troubleshooting/common_errors.md index a8df3b70d..5caa284f8 100644 --- a/docs/src/troubleshooting/common_errors.md +++ b/docs/src/troubleshooting/common_errors.md @@ -1,13 +1,55 @@ # Common Errors -A missing-input error means a `Required(T)` input has neither supplied state -nor a producer binding; start with `Diagnostics.explain_initialization`. A -plain-input-declaration error means `inputs_` must replace each literal with -`Required(T)` or `Default(value)`. A cardinality error lists selector matches; -correct the scope or choose the intended `OptionalOne`/`Many` multiplicity. An -ambiguity requires an explicit application/object selector. - -Duplicate-writer errors require either distinct output routing or an explicit -`Updates` order. Cadence errors require fixed `Dates` periods compatible with -the environment base step. Extend package functions as -`PlantSimEngine.run!(...)`, including the package qualification. +Start with the application, object, and variable named in the error. They tell +you which part of the configuration needs attention. Many connection errors +are detected before any equation runs. + +| Symptom | What it means | First action | +|:--|:--|:--| +| A required input is missing | Neither initial status nor another application supplies it | Supply a measured/initial value or connect a producer | +| A selector finds too few or too many objects | The matches do not satisfy `One`, `OptionalOne`, or `Many` | Check object labels and the search scope | +| More than one source matches | The source application is ambiguous | Name the intended `application` and, if needed, `var` | +| Several models write the same variable | Canonical status has competing writers | Decide which model owns the value; use `Updates` only for intentional ordered updates | +| Variable contracts differ | Units, physical basis, or another declared meaning differ | Check the equations and add an explicit conversion model if appropriate | +| A cadence is rejected | The period does not fit the base step, or an implicit cadence violates a model's hint | [Choose compatible time steps](../guides/time/advanced_time_environment.md) | +| There is a dependency cycle | No valid same-step execution order exists | Decide whether the science requires a lag or an iterative solution | + +## Example: the light model needs LAI + +`Beer` reads LAI from status. With no producer, you must supply it: + +```@example missing_lai +using PlantSimEngine +using PlantSimEngine.Examples + +model = CompositeModel(Beer(0.6); + status=(LAI=2.0,), environment=(Ri_PAR_f=100.0,)) +simulation = run!(model) +final_state(simulation).aPPFD +``` + +Alternatively, add a model that calculates LAI and supply that model's inputs. +An arbitrary zero may remove a missing-input error while changing the +scientific question. Use a value with a clear meaning. + +## Check the objects a selector actually sees + +Inspect `Diagnostics.explain_objects(model)` to see labels and parent links. +For a plant application, `Many(scale=:Leaf, within=Subtree())` searches that +plant and its descendants. For a leaf application, the same selector starts +at that leaf. Use `SelfPlant()` when the search should cover its whole plant. + +Choose `Many` when the receiving model accepts a collection. Choosing it only +to silence an error can pass a vector to an equation that expects one value. + +## Errors while writing a model + +Declare each status input with `Required(T)` or `Default(value)`, for example +`inputs_(::MyModel) = (LAI=Required(Real),)` with the function qualified as +`PlantSimEngine.inputs_`. A plain literal in `inputs_` is not a declaration of +required state. Define the equation as `PlantSimEngine.run!(...)` so Julia +extends the package function. + +For the complete sequence, see [Write and test a first model](../journeys/modelers/basic_model.md). +For further investigation, see [Inspect a simulation](runtime_contracts.md) +and [Dependency cycles](dependency_cycles.md). diff --git a/docs/src/troubleshooting/runtime_contracts.md b/docs/src/troubleshooting/runtime_contracts.md index 7e5fda2bd..d08f5727e 100644 --- a/docs/src/troubleshooting/runtime_contracts.md +++ b/docs/src/troubleshooting/runtime_contracts.md @@ -1,12 +1,57 @@ -# Runtime Contracts And Diagnostics - -Use `Diagnostics.explain_initialization`, `Diagnostics.explain_bindings`, `Diagnostics.explain_calls`, -`Diagnostics.explain_schedule`, `Diagnostics.explain_environment_bindings`, and -`Diagnostics.explain_output_retention` as the supported inspection surface. Do not inspect -compiled internal fields. - -Targets, carriers, calls, writer checks, and schedules refresh after the -application that made a structural change. New objects join applications still -remaining in that timestep; they do not retroactively run earlier applications. -Movement and geometry changes invalidate affected spatial environment bindings. -Accepted streams are append-only. +# Inspect A Simulation + +When a result is unexpected, first check what ran, where it ran, and which +values it read. The public `Diagnostics` functions return structured rows that +can be displayed as a table. They explain the configured computation; they +do not establish that the scientific equations or parameter values are valid. + +| Question | Diagnostic | +|:--|:--| +| Which objects and labels exist? | `Diagnostics.explain_objects(model)` | +| Which models run on them? | `Diagnostics.explain_applications(model)` | +| Where does each input come from? | `Diagnostics.explain_bindings(model)` | +| Which initial values are supplied or missing? | `Diagnostics.explain_initialization(model)` | +| When does each application run? | `Diagnostics.explain_schedule(model)` | +| Which weather or spatial source is sampled? | `Diagnostics.explain_environment_bindings(model)` | +| Which models are called by a controller? | `Diagnostics.explain_calls(model)` | +| Why was an output stream kept? | `Diagnostics.explain_output_retention(simulation)` | + +## Inspect a small working example + +Here leaf surface depends on supplied carbon biomass. Check its input source +before running a larger structure: + +```@example inspect_simulation +using PlantSimEngine, DataFrames +using PlantSimEngine.Examples + +model = CompositeModel(ToyLeafSurfaceModel(0.02); + status=(carbon_biomass=50.0,), id=:leaf, scale=:Leaf) +DataFrame([ + (variable=row.variable, role=row.role, disposition=row.disposition) + for row in Diagnostics.explain_initialization(model) +]) +``` + +```@example inspect_simulation +simulation = run!(model; outputs=:all) +@assert final_state(simulation).surface ≈ 1.0 # hide +DataFrame(Diagnostics.explain_output_retention(simulation)) +``` + +If model construction fails, use the error's object and application names to +correct the configuration first. A diagnostic cannot inspect a model that was +never constructed. + +## After growth or movement + +Structural changes refresh application targets and their connections **after +the application that changed the structure**. New objects may run applications +still remaining in that time step. They do not retroactively run earlier ones. +Changes made between simulation steps are processed before the next step. + +Movement or a geometry update invalidates the affected spatial environment +bindings. Use the public lifecycle functions so the runtime knows that a +refresh is needed. Removing an organ stops its future execution but preserves +its already retained output history. See [Modify plant structure](../journeys/users/structure_changes.md) +for an example that checks both target changes and conservation. diff --git a/docs/src/troubleshooting_and_testing/downstream_tests.md b/docs/src/troubleshooting_and_testing/downstream_tests.md deleted file mode 100644 index f78ec40ab..000000000 --- a/docs/src/troubleshooting_and_testing/downstream_tests.md +++ /dev/null @@ -1,9 +0,0 @@ -# Automated tests : downstream dependency checking - -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 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 uses PlantSimEngine, so the integration workflow checks that PlantBioPhysics's tests do not break unexpectedly after changes to PlantSimEngine. The repository also keeps a separate benchmark workflow and benchmark scripts in the `benchmark/` directory for performance tracking. - -If you maintain a package that depends on PlantSimEngine, you can propose adding it to the downstream integration workflow through a pull request. diff --git a/docs/src/tutorials/growing_plant/part1_growth.md b/docs/src/tutorials/growing_plant/part1_growth.md index aa692f125..00168ce41 100644 --- a/docs/src/tutorials/growing_plant/part1_growth.md +++ b/docs/src/tutorials/growing_plant/part1_growth.md @@ -1,35 +1,48 @@ # Growing A Plant CompositeModel -Begin with a plant object and leaf objects whose carbon production is gathered -by a plant application through `Many(scale=:Leaf, within=Subtree())`. A growth -model calls `register_object!` after its carbon or thermal threshold is met. - -Structural changes refresh compiled targets after the application that made -the change. A new leaf may run applications that remain later in the same -timestep. It runs an application that already completed only when its creator -declares that application as an `Initializer` and explicitly initializes the -newborn object. -When callers mutate structure between `step!` calls, refresh occurs before the -next step. - -Build the initial registry explicitly so ownership remains visible: - -```julia -model = CompositeModel( - Object(:plant; scale=:Plant, status=Status(carbon=0.0)), - Object(:leaf_1; scale=:Leaf, parent=:plant, status=Status(area=1.0)); - applications=(leaf_application, plant_balance, growth_application), - environment=weather, -) -``` - -The plant balance gathers leaf production with -`Many(scale=:Leaf, within=Subtree())`. The growth kernel obtains the live model -with `runtime_model(context)`, checks its carbon and thermal thresholds, creates -a fully initialized `Object`, and calls `register_object!`. It should deduct -the construction cost exactly once before registration. - -After each step, assert both biology and structure: remaining plant carbon, -the number of leaf objects, each new leaf's parent, and accepted historical -outputs. `Diagnostics.explain_applications` should show that the new leaf is absent -during its creation step and present after the between-step refresh. +Start with [Modify Plant Structure](@ref) for a runnable example of adding, +reparenting, and removing leaves. This page explains the extra decisions needed +when a biological growth model owns those operations. + +## Connect organ creation to a resource budget + +Keep the plant carbon stock on the plant object and leaf production on the +leaves. The plant balance gathers only descendant production with +`Many(scale=:Leaf, within=Subtree())`. State whether production is a rate, +an interval amount, or a cumulative quantity before connecting it to a stock. +An accumulated source must be differenced or accounted for once; adding its +whole value repeatedly creates carbon. + +A growth kernel then follows this sequence: + +1. Read the plant's available carbon and the developmental condition. +2. If growth is permitted, construct the new organ's status with explicit + initial area, mass, and other required values. +3. Charge the construction cost once and register the organ with its stable + identity and correct parent. +4. Check that remaining reserves plus constructed material reproduce the + pre-growth budget, including any explicitly modelled respiration cost. + +Use `register_object!` when the caller has constructed a fully initialized +`Object`. For an MTG-backed model, use `add_organ!` so the MTG node, status, +and runtime object are created together. Neither operation chooses an +organogenesis hypothesis, construction cost, or carbon-to-dry-matter conversion +for you. + +## Know when a newborn can run + +When a kernel changes topology, PlantSimEngine refreshes targets and bindings +**after that application**. A new leaf can run applications that remain later +in the same timestep. An application that already completed is not rerun. +If a newborn needs that application's initial calculation immediately, its +creator must declare an `Initializer` and call `run_initializer!` explicitly. + +When the caller changes topology between `step!` calls, the refresh happens +before the next step. These are two different entry points to the same +lifecycle mechanism; neither implies a rollback of biological state. + +Check the organ's parent, required initial values, first retained sample, and +plant carbon budget after creation. Use the working example in +[Modify Plant Structure](@ref) to inspect the registry, then continue with +[Adding Roots And Water](@ref) for a small resource-accounting example and +[Debugging Growth And Resource Ordering](@ref) when execution order is unclear. diff --git a/docs/src/tutorials/growing_plant/part2_roots_water.md b/docs/src/tutorials/growing_plant/part2_roots_water.md index c15bd5bd5..51a957b3e 100644 --- a/docs/src/tutorials/growing_plant/part2_roots_water.md +++ b/docs/src/tutorials/growing_plant/part2_roots_water.md @@ -1,38 +1,86 @@ # Adding Roots And Water -Add root objects and gather absorption through a plant-local `Many` selector. -Keep shared carbon and water stocks on the plant, while leaf and root state -remains object-local. Environment precipitation is an environment input; root -creation is an explicit `register_object!` operation with initialized status. - -When several plants share one soil object, select it explicitly with a -model-wide `One` selector rather than relying on traversal order. - -Keep stocks at the scale that owns conservation. A root model may publish an -absorption rate per root, while the plant model integrates all root rates and -updates one plant water stock. The example below assumes uptake per second -and weights rates by their durations in seconds. A soil model owns soil water; plants read it -through an explicit model-wide selector. This avoids copying one stock into -every organ and makes duplicate writers visible. - -```julia -ModelSpec( - PlantWaterModel(); - inputs=( - :root_uptake => Many( - scale=:Root, within=Subtree(), application=:root_absorption, - var=:uptake, policy=Integrate((values, durations_seconds) -> sum(values .* durations_seconds)), window=Day(1), - ), - :soil_water => One( - scale=:Soil, within=SceneScope(), application=:soil_water, - var=:water, +This example gathers two roots' **already accepted** uptake rates into one +plant water stock. It teaches the time and ownership boundary; the constant +rates are illustrative, not a root-uptake equation. It assumes an external +water supply has granted those rates and does not simulate soil competition, +transpiration, or a complete plant water balance. + +## Integrate each accepted interval once + +Rates are in g water s⁻¹ per root. The plant adds a day's uptake in g water +once per day. Both `on=One(scale=:Plant)` and `every=Day(1)` are explicit: +updating this stock hourly with a rolling one-day total would count overlapping +intervals repeatedly. + +```@example root-water +using Dates, PlantSimEngine + +PlantSimEngine.@process "docs_root_uptake" verbose=false +PlantSimEngine.@process "docs_plant_water" verbose=false +struct DocsRootUptake <: AbstractDocs_Root_UptakeModel end +struct DocsPlantWater <: AbstractDocs_Plant_WaterModel end + +PlantSimEngine.inputs_(::DocsRootUptake) = (accepted_rate=Required(Real),) +PlantSimEngine.outputs_(::DocsRootUptake) = (uptake=0.0,) +function PlantSimEngine.run!(::DocsRootUptake, status, environment, constants, context) + status.uptake = status.accepted_rate + return nothing +end + +PlantSimEngine.inputs_(::DocsPlantWater) = (root_uptake=Required(AbstractVector{<:Real}),) +PlantSimEngine.outputs_(::DocsPlantWater) = (stored_water=0.0,) +function PlantSimEngine.run!(::DocsPlantWater, status, environment, constants, context) + status.stored_water += sum(status.root_uptake) + return nothing +end + +model = CompositeModel( + Object(:plant; scale=:Plant), + Object(:root_1; scale=:Root, parent=:plant, status=Status(accepted_rate=1e-4)), + Object(:root_2; scale=:Root, parent=:plant, status=Status(accepted_rate=2e-4)); + applications=( + ModelSpec(DocsRootUptake(); name=:uptake, on=Many(scale=:Root), every=Hour(1)), + ModelSpec( + DocsPlantWater(); name=:water, on=One(scale=:Plant), every=Day(1), + inputs=(root_uptake=Many( + scale=:Root, within=Subtree(), application=:uptake, var=:uptake, + policy=Integrate((values, seconds) -> sum(values .* seconds)), + window=Day(1), + ),), ), ), + environment=(duration=Hour(1),), ) +simulation = run!(model; steps=49, outputs=:all) +water_history = [ + (base_step=row.time, stored_water_g=row.value) + for row in collect_outputs(simulation; sink=nothing) + if row.object_id == :plant && row.variable == :stored_water +] +@assert isapprox(final_state(simulation, :plant).stored_water, 49 * (1e-4 + 2e-4) * 3600) +water_history ``` -Precipitation, temperature, and radiation remain environment variables, not -ordinary object outputs. Use `Environment(sources=...)` when provider column -names differ from model-facing names. When growth creates a root, initialize -all required root status values before `register_object!`; verify the next -timestep's carrier with `Diagnostics.input_value` or `Diagnostics.explain_bindings`. +The first daily call has only one hourly sample available: its partial window +adds `1.08` g. Each following daily call adds 24 new hourly intervals, or +`25.92` g. The stock is therefore `1.08`, `27.0`, and `52.92` g at base steps +1, 25, and 49. All 49 supplied hourly amounts are counted once. A daily cadence +does not by itself suppress this partial startup window. + +These totals describe uptake added to storage, not tissue hydration or growth. + +## Extend the boundary to a shared soil + +When several plants share finite soil water, give the soil stock one owner. +A collective soil/root controller must gather all demands, limit their sum to +the available water, subtract the accepted withdrawals once, and return the +accepted rates or amounts to each plant. Several roots independently reading +one soil stock do not provide that arbitration. `Updates` can order writers, +but it does not implement a resource-allocation rule. + +Keep rainfall as environmental forcing and state its units before converting +it to a soil-water amount. Add losses and exchanges explicitly when extending +the plant balance. When growth adds a root, initialize its state and register +it through the lifecycle API; the plant-local `Many` binding then refreshes +after the creating application. See [Growing A Plant CompositeModel](@ref). diff --git a/docs/src/tutorials/growing_plant/part3_debugging.md b/docs/src/tutorials/growing_plant/part3_debugging.md index eff0e2282..c6aff4d74 100644 --- a/docs/src/tutorials/growing_plant/part3_debugging.md +++ b/docs/src/tutorials/growing_plant/part3_debugging.md @@ -1,33 +1,50 @@ # Debugging Growth And Resource Ordering -If an organ appears to spend resources before it exists, inspect activation -timing and the compiled schedule. If two models intentionally update one stock, -declare `Updates(:stock; after=:producer)`. If a parent must test several child -states before accepting one, use `calls` and publish only the accepted call. +Use this page when a new organ receives the wrong inputs, spends a resource +twice, or appears in outputs at an unexpected time. Begin with the runnable +[Modify Plant Structure](@ref) example and change one mechanism at a time. -For cycles, choose a scientific meaning: lag one edge with +## Follow one timestep + +First establish whether the organ was created inside a kernel or between +`step!` calls. Inside a kernel, targets and bindings refresh after the creating +application. The newborn can run applications that remain later in that step; +applications that already completed do not run again. Between steps, the +refresh happens before the next step. + +An `Initializer` allows a creator to calculate one newborn's initial state +explicitly. It does not create an extra retained publication during the step. +Registration changes the live registry immediately; the refresh barrier is +not a general transaction or an automatic rollback of model state. + +## Inspect the boundary that failed + +| Symptom | First check | What to verify | +|---|---|---| +| Missing or invalid newborn state | `Diagnostics.explain_initialization(model)` | Every required input has a meaningful initial value. | +| One plant consumes another plant's leaves | `Diagnostics.explain_bindings(model)` | The source uses the intended `Subtree()` or other explicit scope. | +| A stock has two producers | `Diagnostics.explain_writers(model)` | One owner updates it, or intentional writers declare `Updates(:stock; after=:producer)`. | +| Child models run unexpectedly | `Diagnostics.explain_calls(model)` | The correct applications are call targets and the parent invokes them once for each intended trial or acceptance. | +| Daily uptake is added hourly | `Diagnostics.explain_schedule(model)` | Stock-update cadence matches the non-overlapping integration intervals. | +| Newborn output begins too early or too late | `Diagnostics.explain_outputs(simulation)` and `collect_outputs(simulation)` | First publication agrees with the creation point and remaining schedule. | + +For carbon or water, also write a balance independent of the execution graph: +initial stock plus accepted inputs equals final stock plus explicit losses +and transfers. Passing writer checks cannot establish this conservation law. +[Adding Roots And Water](@ref) shows a two-day accounting check. + +## Keep trial and accepted state distinct + +For a numerical cycle, choose the scientific meaning: lag one input with `PreviousTimeStep`, put convergence under a parent-owned hard call, or -reformulate the equations. Do not resolve a cycle by incidental application -ordering. - -Use this debugging order: - -1. `Diagnostics.explain_initialization(model)` for missing state or environment values. -2. `Diagnostics.explain_bindings(model)` for source scope and multiplicity. -3. `Diagnostics.explain_writers(model)` for competing canonical outputs. -4. `Diagnostics.explain_calls(model)` for call-only targets and target cardinality. -5. `Diagnostics.explain_schedule(model)` for cadence and root ordering. -6. `Diagnostics.explain_outputs(simulation)` after execution for publication history. - -A trial call must not mutate accepted output history or scatter mutable -environment outputs. Nested trials inherit the outer publication decision. -Convergence and failure policy belongs to the parent model: it decides the -iteration limit, tolerance, fallback, and whether any state is accepted. - -Structural mutation is also transactional at the timestep boundary. A new -organ is registered immediately in the model registry but does not recursively -run merely because it was created. A creator may explicitly initialize one -newborn object through a declared `Initializer`; otherwise, compilation refreshes -targets, carriers, calls, writer validation, schedules, and requested outputs at -the safe barrier. Geometry-only movement refreshes only affected spatial -bindings where possible. +reformulate the equations. Incidental application order is not a solver. + +Use `run_call!(context, name; publish=false)` for trial evaluations and publish +only the accepted result. This suppresses trial output publication; it does +not undo assignments to live status. The parent must prepare each trial's +state and keep irreversible stock updates out of rejected iterations. Commit +mutable environment changes only after accepting a solution. + +The parent owns the iteration limit, tolerance, and failure policy. The +[MAESPA-Style Synthesis](@ref) demonstrates accepted canopy-air commits and +leaf calls; its carbon accumulation happens after the solver accepts a state. diff --git a/docs/test/runtests.jl b/docs/test/runtests.jl index 73b4f265c..9ff8d3d65 100644 --- a/docs/test/runtests.jl +++ b/docs/test/runtests.jl @@ -1,42 +1,20 @@ -using Test - -@testset "Progressive journey structure" begin - journey_root = joinpath(@__DIR__, "..", "src", "journeys") - user_pages = sort([ - joinpath(journey_root, "users", file) - for file in readdir(joinpath(journey_root, "users")) - if endswith(file, ".md") - ]) - modeler_pages = sort([ - joinpath(journey_root, "modelers", file) - for file in readdir(joinpath(journey_root, "modelers")) - if endswith(file, ".md") - ]) - - for page in user_pages - source = read(page, String) - @test occursin("New concept", source) - @test occursin("## Page recap", source) - @test occursin("**You added:**", source) - @test occursin("**PlantSimEngine infer", source) - @test occursin("**You keep explicit:**", source) - @test occursin("**New API names:**", source) - @test !occursin("```julia", source) - end - - for page in modeler_pages - source = read(page, String) - @test occursin("**New concept:**", source) - @test occursin("## Model-author recap", source) - @test occursin("**You implemented:**", source) - @test occursin("**PlantSimEngine inferred:**", source) - @test occursin("**The scenario author keeps explicit:**", source) - @test occursin("**New API names:**", source) - @test occursin("tested", lowercase(source)) - end -end +using Test, Markdown +# Documenter executes the @example blocks, their numerical assertions, and +# doctests. Avoid tests that prescribe headings or repeated editorial labels. @testset "PlantSimEngine documentation" begin ENV["PLANTSIMENGINE_DOCS_BUILD_ONLY"] = "true" @test include(joinpath(@__DIR__, "..", "make.jl")) === nothing end + +@testset "Generated Markdown keeps code and table structure" begin + writer = Base.get_extension(Bonito, :BonitoDocumenterExt) + markdown = Markdown.parse("```julia\nx = 1\ny = 2\n```\n\n| Variable | Value |\n| --- | --- |\n| LAI | 2 |") + root = convert(writer.MA.Node, markdown) + context = writer.DCtx(nothing, nothing, nothing, joinpath(@__DIR__, "..", "build")) + rendered = writer.domify(context, root) + html = repr(MIME"text/html"(), Bonito.DOM.div(rendered...)) + @test occursin("
     0 <= row.Ri_PAR_f <= row.Ri_SW_f, weather)
    +    @test all(row -> row.Ri_PAR_f ≈ row.Ri_SW_f * constants.PAR_fraction, weather)
    +
    +    status = Status(leaf_Ra_SW_f=zeros(2), leaf_aPPFD=zeros(2), Ψₗ=zeros(2))
    +    noon = weather[12]
    +    _prepare_model_leaf_inputs!(status, noon, -0.2, constants)
    +    @test status.leaf_Ra_SW_f == fill(noon.Ri_SW_f, 2)
    +    @test status.leaf_aPPFD ≈ fill(noon.Ri_PAR_f * constants.J_to_umol, 2)
    +    @test status.Ψₗ == fill(-0.2, 2)
    +
    +    # The supplied run constants, rather than a hidden default, own the conversion.
    +    _prepare_model_leaf_inputs!(status, noon, -0.2, (J_to_umol=2.0,))
    +    @test status.leaf_aPPFD == fill(2 * noon.Ri_PAR_f, 2)
    +end
    +
    +@testset "MAESPA allocation accounts each carbon increment once" begin
    +    status = _maespa_plant_status()
    +    model = AllocA(0.35, 0.55)
    +    for (cumulative, increment) in ((3.0, 3.0), (8.0, 5.0), (15.0, 7.0), (15.0, 0.0), (14.0, -1.0))
    +        status.leaf_carbon[1] = cumulative
    +        PlantSimEngine.run!(model, status, nothing, nothing, nothing)
    +        @test status.leaf_carbon == [cumulative]
    +        @test status.daily_growth ≈ increment
    +        @test status.accounted_carbon ≈ cumulative
    +        @test status.leaf_pool + status.wood_pool + status.reserve_pool ≈ cumulative
    +    end
    +end
    +
    +@testset "MAESPA three-day elemental carbon balance" begin
    +    result = run_maespa_example(; nhours=73, check=true)
    +    rows = collect_outputs(result.simulation; sink=nothing)
    +    for (plant_id, leaf_ids) in (
    +        (:plant_A, (:plant_A_leaf_1, :plant_A_leaf_2)),
    +        (:plant_B, (:plant_B_leaf_1, :plant_B_leaf_2, :plant_B_leaf_3)),
    +    )
    +        state = model_status(result.model, plant_id)
    +        # Independently integrate the 73 accepted assimilation samples for each
    +        # leaf. Trial iterations must neither publish nor accumulate extra C.
    +        accepted_carbon = sum(leaf_ids) do leaf_id
    +            leaf = model_status(result.model, leaf_id)
    +            assimilation = [row.value for row in rows if row.object_id == leaf_id && row.variable == :A]
    +            @test length(assimilation) == 73
    +            carbon = sum(assimilation) * leaf.leaf_area * 3600 * 12e-6
    +            @test leaf.leaf_carbon ≈ carbon
    +            carbon
    +        end
    +        @test sum(state.leaf_carbon) ≈ accepted_carbon
    +        pool_carbon = state.leaf_pool + state.wood_pool + state.reserve_pool
    +        @test pool_carbon ≈ state.accounted_carbon
    +        pending_carbon = accepted_carbon - state.accounted_carbon
    +        @test pool_carbon + pending_carbon ≈ accepted_carbon
    +
    +        history(variable) = [row.value for row in rows if row.object_id == plant_id && row.variable == variable]
    +        accounted = history(:accounted_carbon)
    +        growth = history(:daily_growth)
    +        @test length(growth) == length(accounted) == 4
    +        @test growth ≈ diff([0.0; accounted])
    +        @test sum(growth) ≈ last(accounted)
    +        @test history(:leaf_pool) .+ history(:wood_pool) .+ history(:reserve_pool) ≈ accounted
    +    end
    +end
    
    From 532b7b2da102d9f565b4b93098b904ec25dc2f50 Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?R=C3=A9mi=20Vezy?= 
    Date: Sun, 6 Sep 2026 15:02:10 +0200
    Subject: [PATCH 5/6] docs: clarify guides and summarize composite models
    
    Use plain English throughout the manual, simplify the first results table with collect_outputs and sink, and replace verbose CompositeModel dumps with a bounded summary.
    
    Validation: 41 display checks, full executable documentation build, and 1212 internal fragment links passed.
    ---
     docs/make.jl                                  |  18 +-
     docs/src/API/API_examples.md                  |   6 +-
     docs/src/API/API_private.md                   |   8 +-
     docs/src/API/API_public.md                    | 412 ++++++++++--------
     docs/src/API/model_catalog.md                 |  31 +-
     docs/src/API/public_symbols.md                | 147 ++++---
     docs/src/agent_skill.md                       |  25 +-
     docs/src/developers.md                        |  18 +-
     docs/src/guides/coupling.md                   | 179 ++++----
     docs/src/guides/data/forcing_observations.md  |  29 +-
     docs/src/guides/data/numerical_reliability.md | 135 +++---
     docs/src/guides/data/outputs_plotting.md      |  49 ++-
     .../guides/extensions/environment_backends.md | 134 +++---
     docs/src/guides/graph_visualizer_editor.md    | 150 ++++---
     .../guides/modelers/port_existing_model.md    |  44 +-
     .../guides/modelers/repository_and_tests.md   |  40 +-
     docs/src/guides/modelers/stateful_models.md   |  37 +-
     docs/src/guides/multiscale/concepts.md        |  43 +-
     docs/src/guides/multiscale/import_mtg.md      |  24 +-
     docs/src/guides/multiscale/manual_calls.md    | 220 ++++++----
     docs/src/guides/multiscale/value_coupling.md  | 150 ++++---
     .../multiscale/visualizing_structure.md       |  14 +-
     .../guides/time/advanced_time_environment.md  |  61 +--
     docs/src/guides/time/hourly_daily_weekly.md   |  75 ++--
     docs/src/index.md                             |  13 +-
     docs/src/introduction/why_julia.md            |  11 +-
     docs/src/introduction/why_plantsimengine.md   |  26 +-
     docs/src/journeys/modelers/basic_model.md     |  34 +-
     .../journeys/modelers/cross_object_values.md  |  25 +-
     .../modelers/environment_and_cadence.md       |  37 +-
     .../journeys/modelers/hard_dependencies.md    |  55 +--
     .../journeys/modelers/mutable_environment.md  |  58 +--
     docs/src/journeys/users/advanced_execution.md | 118 ++---
     docs/src/journeys/users/cadences.md           | 129 +++---
     docs/src/journeys/users/environments.md       |  72 ++-
     docs/src/journeys/users/maespa_synthesis.md   | 173 ++++----
     docs/src/journeys/users/mental_model.md       | 114 ++---
     .../journeys/users/mutable_environments.md    |  73 ++--
     docs/src/journeys/users/one_object.md         | 126 +++---
     docs/src/journeys/users/one_plant.md          |  73 ++--
     docs/src/journeys/users/several_objects.md    |  41 +-
     docs/src/journeys/users/several_plants.md     |  51 +--
     docs/src/journeys/users/structure_changes.md  |  60 ++-
     docs/src/migration_composite_model.md         |  95 ++--
     docs/src/model_execution.md                   | 252 ++++++-----
     docs/src/model_traits.md                      | 119 +++--
     docs/src/planned_features.md                  |  26 +-
     .../installing_plantsimengine.md              |  13 +-
     docs/src/prerequisites/julia_basics.md        |  89 ++--
     docs/src/step_by_step/advanced_coupling.md    | 103 +++--
     docs/src/step_by_step/implement_a_process.md  |  28 +-
     docs/src/step_by_step/model_switching.md      |  46 +-
     docs/src/troubleshooting/common_errors.md     |  34 +-
     docs/src/troubleshooting/dependency_cycles.md |  38 +-
     docs/src/troubleshooting/runtime_contracts.md |  31 +-
     .../tutorials/growing_plant/part1_growth.md   |  75 ++--
     .../growing_plant/part2_roots_water.md        |  45 +-
     .../growing_plant/part3_debugging.md          |  74 ++--
     docs/src/working_with_data/fitting.md         |  22 +-
     src/PlantSimEngine.jl                         |   1 +
     src/composite_model/display.jl                | 103 +++++
     test/runtests.jl                              |   4 +
     test/test-model-display.jl                    | 109 +++++
     63 files changed, 2601 insertions(+), 2044 deletions(-)
     create mode 100644 src/composite_model/display.jl
     create mode 100644 test/test-model-display.jl
    
    diff --git a/docs/make.jl b/docs/make.jl
    index 9b8a40de5..499f23698 100644
    --- a/docs/make.jl
    +++ b/docs/make.jl
    @@ -95,19 +95,19 @@ makedocs(;
             "Write models" => [
                 "New process or new hypothesis?" => "step_by_step/implement_a_process.md",
                 "Write and test a first model" => "journeys/modelers/basic_model.md",
    -            "Keep scientific kernels readable" => "guides/modelers/port_existing_model.md",
    -            "Repository layout and test pyramid" => "guides/modelers/repository_and_tests.md",
    +            "Keep equations readable" => "guides/modelers/port_existing_model.md",
    +            "Organize and test your models" => "guides/modelers/repository_and_tests.md",
                 "Cross-object values" => "journeys/modelers/cross_object_values.md",
                 "Choose a coupling mechanism" => "guides/coupling.md",
                 "Model compatibility and replacement" => "step_by_step/model_switching.md",
    -            "Environment and cadence traits" => "journeys/modelers/environment_and_cadence.md",
    -            "Hard dependencies" => "journeys/modelers/hard_dependencies.md",
    -            "Mutable environment controllers" => "journeys/modelers/mutable_environment.md",
    -            "Stateful models" => "guides/modelers/stateful_models.md",
    +            "Read weather and set time steps" => "journeys/modelers/environment_and_cadence.md",
    +            "Call another model" => "journeys/modelers/hard_dependencies.md",
    +            "Update growing conditions" => "journeys/modelers/mutable_environment.md",
    +            "Models that remember values" => "guides/modelers/stateful_models.md",
             ],
             "Environment and time" => [
                 "Read an environment" => "journeys/users/environments.md",
    -            "Different model cadences" => "journeys/users/cadences.md",
    +            "Different model time steps" => "journeys/users/cadences.md",
                 "Hourly, daily, and weekly" => "guides/time/hourly_daily_weekly.md",
                 "Choose compatible time steps" => "guides/time/advanced_time_environment.md",
             ],
    @@ -119,8 +119,8 @@ makedocs(;
                 "Modify the environment" => "journeys/users/mutable_environments.md",
                 "Control advanced execution" => "journeys/users/advanced_execution.md",
                 "MAESPA-style synthesis" => "journeys/users/maespa_synthesis.md",
    -            "Manual calls" => "guides/multiscale/manual_calls.md",
    -            "Advanced coupling and hard dependencies" => "step_by_step/advanced_coupling.md",
    +            "Control when other models run" => "guides/multiscale/manual_calls.md",
    +            "More about calls between models" => "step_by_step/advanced_coupling.md",
             ],
             "Check and troubleshoot" => [
                 "Common errors" => "troubleshooting/common_errors.md",
    diff --git a/docs/src/API/API_examples.md b/docs/src/API/API_examples.md
    index e9e989266..f9620d22f 100644
    --- a/docs/src/API/API_examples.md
    +++ b/docs/src/API/API_examples.md
    @@ -1,11 +1,15 @@
     # 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 in a working environment with PlantSimEngine, you can simply use this sub-module:
    +PlantSimEngine includes models for learning and testing. Load them with:
     
     ```julia
     using PlantSimEngine.Examples
     ```
     
    +These examples show how to write equations and connect models. Check their
    +assumptions before using them in a scientific study. Start with
    +[your first simulation](../journeys/users/one_object.md) for a guided example.
    +
     ## List
     
     ```@index
    diff --git a/docs/src/API/API_private.md b/docs/src/API/API_private.md
    index 40b584bd8..73dd17933 100644
    --- a/docs/src/API/API_private.md
    +++ b/docs/src/API/API_private.md
    @@ -1,7 +1,13 @@
     # 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).
    +This reference is for people working on PlantSimEngine itself. The names
    +below are internal functions, types, and constants. They may change as the
    +implementation develops.
    +
    +For building a simulation, use the [public API](API_public.md). To inspect
    +its connections and results, use the
    +[simulation reports](../troubleshooting/runtime_contracts.md).
     
     ## Index
     
    diff --git a/docs/src/API/API_public.md b/docs/src/API/API_public.md
    index 87980685f..8221b9e1f 100644
    --- a/docs/src/API/API_public.md
    +++ b/docs/src/API/API_public.md
    @@ -1,129 +1,154 @@
     # Public API
     
    +This reference lists the functions for building, running, and inspecting a
    +simulation. For a first example, start with
    +[Couple Models On One Object](@ref).
    +
    +An **object** is one part of your simulated system, such as a plant or leaf.
    +Its **status** holds its changing values. A **model application** is a model
    +configured with a name, selected objects, and input or timing settings.
    +An input **binding** connects a model to the value it reads. The **carrier**
    +holds that connection, usually as a shared reference to the source value;
    +time-based inputs can instead read saved results.
    +
     ## Unified CompositeModel/Object API
     
     ### Scenario and model applications
     
    -- `CompositeModel` stores objects, model applications, instances, and environment.
    +- `CompositeModel` stores the objects, their model applications, reusable plant
    +  instances, and environmental data or providers.
     - `CompositeModel(model, models...; status=..., timestep=...,
       type_promotion=..., status_transform=...)` is the concise one-object form and
    -  lowers to the same object/application representation.
    -- `Object` represents one runtime entity with stable identity and status.
    -- `object_id(model, source)` resolves an `ObjectId`, registered `Object` or
    -  `Status`, MTG node, or raw identifier against the live registry. MTG nodes
    -  retain the exact identity assigned by the model's `id=` accessor during
    -  adaptation or organogenesis; `object_id` does not reevaluate that accessor,
    -  and copied or foreign nodes are rejected. The same methods accept a
    -  `RunContext` or `Simulation`.
    -- `CompositeModelTemplate` and `ObjectInstance` reuse a model across instances.
    +  creates one object and its model applications.
    +- `Object` represents one entity, such as a plant or organ, with an identifier
    +  that stays the same and its own status.
    +- `object_id(model, source)` finds an object's identifier in the model's
    +  registry, the collection of registered objects. `source` can be an
    +  `ObjectId`, registered `Object` or `Status`, MTG node, or raw identifier.
    +  MTG nodes keep the identifier assigned by the model's `id=` function when
    +  the MTG was imported or the organ was created. `object_id` does not call
    +  that function again, and rejects copied nodes or nodes from another model.
    +  The same methods accept a `RunContext` or `Simulation`.
    +- `CompositeModelTemplate` and `ObjectInstance` reuse a set of model
    +  definitions for several plants or other repeated objects.
     - `ModelSpec(model; name=..., on=..., inputs=..., calls=..., outputs_to=...,
    -  every=..., environment=..., output_routing=..., updates=...)` is the one
    -  application construction form.
    +  every=..., environment=..., output_routing=..., updates=...)` configures a
    +  model application.
     
     ### Coupling
     
    -- `ModelSpec(...; inputs=...)` declares value dependencies.
    -- `bound_input(context, :name)` opts a model kernel into an identity-aware
    -  `BoundMany` view for one of its declared `Many` inputs; `object_ids(view)`
    -  returns the aligned object identities without copying them.
    -- `ModelSpec(...; calls=...)` declares manually executable child models.
    +- `ModelSpec(...; inputs=...)` describes where a model's inputs come from.
    +- `bound_input(context, :name)` gives a model access to both the values and
    +  source objects of a declared `Many` input through a `BoundMany` view.
    +  `object_ids(view)` returns the source identifiers in the same order as the
    +  values, without copying them.
    +- `ModelSpec(...; calls=...)` declares models that this model can run from
    +  inside its own calculation.
     - `ModelSpec(...; outputs_to=(name=OutputTo(selector; vars=...),))`
    -  declares status variables owned by the application but stored on selected
    -  destination objects. Each variable uses `Required(T)` or `Default(value)`;
    -  the compiler resolves identities and rejects ambiguous writers before
    -  initializing statuses.
    -- `output_targets(context, :name)` returns the compiled [`OutputTargets`](@ref)
    -  view for one named `outputs_to` group. Destination columns are exposed
    -  explicitly as `targets.columns.`, and `object_ids(targets)` returns
    -  their aligned, read-only identities.
    +  declares variables that this application writes into other selected objects'
    +  statuses. Each variable uses `Required(T)` or `Default(value)`. Before
    +  initializing the statuses, PlantSimEngine finds the destination objects and
    +  checks that competing applications do not write the same value.
    +- `output_targets(context, :name)` returns an [`OutputTargets`](@ref) view
    +  for one named `outputs_to` group. Read or write a variable's destination
    +  values through `targets.columns.`. `object_ids(targets)` returns
    +  their identifiers in the same order; those identifiers are read-only.
     - `assign_outputs!(targets, table; id=:object_id)` assigns a
    -  Tables.jl-compatible result by identity. The lower-level
    +  Tables.jl-compatible result to objects using their identifiers. The
       `assign_outputs!(targets, ids, columns)` overload accepts an ID vector and a
       `NamedTuple` of columns directly.
    -- `Updates(:variable; after=:application_id)` orders intentional duplicate writers.
    -- `Input(...)` and `Call(...)` express model defaults through `dep(model)`.
    +- `Updates(:variable; after=:application_id)` sets the order when several
    +  applications deliberately update the same variable.
    +- `Input(...)` and `Call(...)` describe a model's default input connections
    +  and calls through `dep(model)`.
     - `Initializer(One(application=:name, ...))` declares one normally scheduled
       application that may initialize a newly registered object during its
       creation event.
    -- `run_call!(context, :name; publish=false)` executes every resolved hard-call
    -  target and always returns a vector-like `CallTargets` collection.
    +- `run_call!(context, :name; publish=false)` runs every model and object
    +  selected by that named call. Each such pair is a **call target**. The result
    +  is always a vector-like `CallTargets` collection.
     - `run_call!(context, :name; sampled_environment=value)` forwards one already
    -  sampled model-facing environment through cached typed execution batches.
    -- `call_model(context, :name)` returns the concrete model when a call resolves
    -  to exactly one target.
    -- `call_targets(context, :name)` returns the same non-executing collection for
    -  fine-grained execution with `run_call!(target; ...)`.
    -- `run_initializer!(context, :name, object)` runs an `Initializer` binding once
    -  on that newborn object, initializes canonical local status without an extra
    -  mid-step output sample, and returns its canonical `Status`. It is not a
    -  trial-call or existing-object API.
    -
    -Distributed assignment requires exact destination coverage. Every selected
    -object ID must occur exactly once and every declared output column must be
    -present; additional table or `NamedTuple` columns are treated as metadata and
    -ignored. Result columns may alias destination storage only for direct
    -self-assignment of the same column in exact destination order.
    -
    -Obtain `OutputTargets` inside each model invocation and do not retain it across
    -a lifecycle barrier. Reusing the same ID-column object lets PlantSimEngine
    -reuse its compiled row permutation and promises that the IDs and their order
    -have not been mutated. Replace the ID-column object when either changes.
    +  prepared set of environmental values to the selected models. It uses the
    +  execution groups already prepared by PlantSimEngine.
    +- `call_model(context, :name)` returns the model when a call selects exactly
    +  one target.
    +- `call_targets(context, :name)` returns those targets without running them,
    +  so you can choose individual targets to run with `run_call!(target; ...)`.
    +- `run_initializer!(context, :name, object)` runs a declared `Initializer`
    +  once on a newly created object. It sets values in that object's main stored
    +  status and returns that `Status`, without adding an output-history sample
    +  partway through the step. Use it only for initialization during object
    +  creation, not for trial calls or existing objects.
    +
    +When assigning outputs to several objects, include every selected object ID
    +exactly once and provide every declared output column. Additional table or
    +`NamedTuple` columns are treated as metadata and ignored. A result column
    +may share storage with a destination column only when assigning that column
    +to itself in exactly the destination order.
    +
    +Obtain `OutputTargets` each time the model runs. Do not keep it for later
    +calls after changes to the objects have been processed. If you reuse the
    +same ID-column object, PlantSimEngine reuses the correspondence it calculated
    +between result rows and destination objects. This requires the IDs and their
    +order to remain unchanged. Supply a new ID-column object if either changes.
     
     ### Model input schema
     
    -- `Required(T)` declares an input that object state or another application must
    -  supply. `T` is an expected type and may be generic.
    -- `Default(value)` declares a true model fallback that needs no user
    -  initialization.
    +- `Required(T)` declares an input that you must supply in the object's status
    +  or connect to another model. `T` is the expected type and may be generic.
    +- `Default(value)` supplies a model's fallback value when no input is provided.
     - `inputs_(model)` uses only these explicit declarations; plain literals are
       rejected.
    -- `outputs_(model)` literals remain initial output-state values.
    -- `init_variables(model)` returns only genuine input defaults and initial
    +- `outputs_(model)` gives the initial values of outputs.
    +- `init_variables(model)` returns only input defaults and initial
       output values.
    -- `VariableContract` records a variable's unit, spatial or object basis,
    -  temporal basis, aggregation meaning, and intensive/extensive character
    -  without wrapping its runtime value.
    -- `variable_contracts(model)` returns validated declarations from the
    -  package-extension trait `PlantSimEngine.variable_contracts_`. A compiled
    -  producer-consumer binding must have identical contracts once either side
    -  declares one.
    +- `VariableContract` describes units and physical meaning: for example,
    +  whether a variable is per plant or per square metre, a rate or a daily
    +  total, and whether amounts from several objects can be added. The description
    +  is stored separately from the numerical value.
    +- `variable_contracts(model)` returns checked declarations from
    +  `PlantSimEngine.variable_contracts_`, which model packages implement.
    +  When an input is connected to another model's output, their contracts must
    +  be identical if either model declares one.
     
     ### Status representation
     
     - `CompositeModel(...; type_promotion=Dict(Float64 => Float32))` converts every
    -  matching status value with `convert` when its storage is materialized.
    +  matching status value with `convert` when the value's storage is created.
     - `CompositeModel(...; status_transform=(variable, value) -> ...)` applies a
       precise transformation based on the status variable name and value. The
    -  returned value becomes the candidate for the general `type_promotion`
    -  mapping, so the transform always runs first.
    +  returned value is then checked against the `type_promotion` mapping, so
    +  `status_transform` always runs first.
     - Ordinary numeric arrays are converted element by element when their elements
       match a mapping rule. Their shape is preserved.
     - The policy covers supplied object statuses, model input and output defaults,
    -  and statuses of objects registered later through the lifecycle API.
    +  and statuses of objects added later through the object-creation functions.
     - The policy is limited to status values. Model parameters, environment values,
       constants, object labels, and topology are not converted.
    -- Conversion occurs during status materialization or object registration, not
    -  on every call to a model kernel.
    +- Conversion occurs when status storage is created or an object is registered,
    +  not every time the model's equation runs.
     - `Diagnostics.explain_initialization(model)` reports `declared_type`,
       `original_type`, `transformed_type`, and `effective_type`, plus flags and the
       selected mapping rule for each initialized value.
     
    -The effective status type must be supported by the model kernel. Generic
    -`Required` declarations and generic computations allow the same model to use
    -`Float32`, uncertainty-carrying numbers, or another compatible numeric type.
    +The model's calculation must support the resulting numerical type. Keeping
    +`Required` declarations and equations open to different types allows the same
    +model to use `Float32`, numbers with uncertainty estimates, or another
    +compatible numerical type.
     See [Numerical Reliability](../guides/data/numerical_reliability.md) for
     complete examples.
     
     ### Selectors
     
    -- Multiplicity: `One(...)`, `OptionalOne(...)`, and `Many(...)`.
    -- Scope: `SceneScope()`, `Self()`, `Subtree()`, `SelfPlant()`,
    +- Number of matches: `One(...)`, `OptionalOne(...)`, and `Many(...)`.
    +- Where to look: `SceneScope()`, `Self()`, `Subtree()`, `SelfPlant()`,
       `Ancestor(...)`, and `Scope(name)`.
     - Label criteria: `kind=...`, `species=...`, `scale=...`, and `name=...`.
    -- Topology relations: `Relation(...)`.
    +- Connections between objects: `Relation(...)`.
     
    -`Self()` always means the current object: the object on which the consuming
    -application runs. It means a plant only when that object is itself the plant.
    +`Self()` always means the current object: the object on which the model
    +reading the input runs. It means a plant only when that object is itself
    +the plant.
     
     Selector fields are checked where the selector is used:
     
    @@ -136,62 +161,76 @@ Selector fields are checked where the selector is used:
     | object queries and `OutputRequest` selectors | object criteria only |
     
     Unsupported or misspelled fields fail when the selector is constructed.
    -Object-relative scopes and relations require a current object, so they belong
    -in inputs, calls, or contextual object/output queries rather than application
    -targets.
    +Selections such as descendants or ancestors need a current object to start
    +from. Use them in inputs, calls, or object/output queries with that context.
    +They cannot select where an application runs through `ModelSpec(...; on=...)`.
     
     ### Time and environment
     
    -- `ModelSpec(...; every=period)` sets an application cadence.
    -- `HoldLast`, `Interpolate`, `Integrate`, and `Aggregate` define temporal
    -  input policies.
    -- `Environment(...)` configures environment providers and source remapping.
    -- Models declare sampled environment variables with `environment_inputs_`.
    -- Mutable environment controllers pass trial state with
    -  `run_call!(context, name; environment=trial_state)` and commit accepted state
    -  with `commit_environment!`.
    -- `OutputRequest(selector, variable; ...)` selects retained and optionally
    -  resampled streams using the same object selector grammar.
    +- `ModelSpec(...; every=period)` sets how often an application runs.
    +- `HoldLast`, `Interpolate`, `Integrate`, and `Aggregate` describe how an input
    +  uses results over time: keep the last value, interpolate, integrate, or
    +  combine values over an interval.
    +- `Environment(...)` chooses where environmental data comes from and can
    +  map source variable names to the names a model expects.
    +- Models list the environmental variables they read in `environment_inputs_`.
    +- A model that tries changes to its environment passes trial values with
    +  `run_call!(context, name; environment=trial_state)`. It stores the accepted
    +  values with `commit_environment!`.
    +- `OutputRequest(selector, variable; ...)` selects results to save and can
    +  resample them at a chosen interval. It uses the same selectors as object
    +  queries.
     
     ### Lifecycle
     
    -- `objects_from_mtg` and `CompositeModel(mtg; ...)` adapt an MTG into the object
    -  registry.
    -- `add_organ!` creates and initializes a new organ in an MTG-backed model.
    -- `runtime_model(context)` gives lifecycle-capable kernels sanctioned access to
    -  the live model from their `RunContext`.
    +- `objects_from_mtg` and `CompositeModel(mtg; ...)` create registered objects
    +  from a MultiScaleTreeGraph (MTG).
    +- `add_organ!` creates and initializes a new organ when the model uses an MTG.
    +- `runtime_model(context)` gives a model's `run!` function access to the
    +  running `CompositeModel`, for example to add or remove objects.
     - `object_id(context)`, `model_object(context)`, `model_status(context)`, and
    -  `source_node(context)` resolve the current execution target. The status
    -  accessor returns canonical registry state rather than the application-local
    -  status view passed to a kernel.
    +  `source_node(context)` return the current object's identifier, object,
    +  status, and source MTG node, respectively. `model_status` returns the main
    +  status stored for that object. The `status` argument passed to `run!` is
    +  instead the view of variables used by that particular application.
     - `register_object!`, `remove_object!`, and `reparent_object!` change
    -  topology.
    -- `move_object!` and `update_geometry!` change spatial state.
    -- Supported lifecycle operations invalidate affected bindings. Structural
    -  connections refresh after the application that changed topology, so new
    +  the objects and their parent relationships.
    +- `move_object!` and `update_geometry!` change position or geometry.
    +- These functions mark affected connections for updating. Changes to object
    +  relationships are processed after the application that made them, so new
       objects can run applications still remaining in the same time step.
       Changes made between steps are processed before the next step.
    -- A creator that must run an application which already completed on existing
    -  objects declares an `Initializer` call. The compiler orders the scheduled
    -  target before the creator and the creator before direct non-temporal
    -  same-step consumers;
    -  `run_initializer!` admits exactly one target from the current pure-addition
    -  event and rejects repeat, existing, reparented, manual-call, and
    -  refresh-fallback execution. Each initialized output must have one potential
    -  canonical writer across local and distributed destinations. Because
    -  `run_initializer!` emits no mid-step stream sample,
    -  downstream temporal consumers of a possible newborn output are rejected at
    -  compilation; a `PreviousTimeStep` input used by the initializer itself
    -  remains supported.
    -- `run!(model; steps=..., outputs=:none)` starts a fresh result timeline and
    +- `Initializer` lets a model initialize an object it has just created, using
    +  an application that already ran on existing objects. Its restrictions are
    +  detailed below.
    +- `run!(model; steps=..., outputs=:none)` starts a fresh simulation history and
       returns a `Simulation`.
     - `continue!(simulation; steps=...)` and `step!(simulation)` advance an
    -  existing timeline without resetting temporal state.
    -- `current_step(simulation)` reports the accepted timeline position.
    -- `final_state(simulation)` returns a latest-state snapshot without
    -  requiring output retention; pass an object id or selector for multi-object
    -  simulations.
    -- `collect_outputs(sim)` materializes retained output streams.
    +  existing simulation, preserving the values needed for time-based inputs.
    +- `current_step(simulation)` reports the latest completed time step.
    +- `final_state(simulation)` returns a snapshot of the latest values even
    +  when output history was not saved. Pass an object id or selector for
    +  multi-object simulations.
    +- `collect_outputs(sim)` gathers saved results into rows for analysis.
    +
    +**Initializing objects during growth.** An `Initializer` application runs
    +before the model that creates new objects. Models that directly read the new
    +values run after the creator in the same step. The following restrictions
    +keep those new values consistent with the rest of the simulation:
    +
    +- `run_initializer!` accepts exactly one target from the current creation
    +  event, and that event must only add objects. It rejects repeated
    +  initialization, existing or reparented objects, and ordinary manual-call
    +  bindings. It also rejects cases that require a full connection rebuild
    +  instead of the update limited to the new objects.
    +- Each initialized output must have exactly one possible application writing
    +  its main stored value. This check counts both local outputs and outputs
    +  written to other objects.
    +- Initialization adds no output sample partway through a step. Models that
    +  read the new values directly can use them in that step, but PlantSimEngine
    +  rejects downstream time-based inputs that could read a newly created
    +  object's output. The initializer itself can still use a `PreviousTimeStep`
    +  input.
     
     ### Explanations
     
    @@ -220,88 +259,103 @@ translations from removed APIs.
     
     ### Model authoring and scenario validation
     
    -Use the neutral `Authoring` namespace for model discovery and machine-readable
    -validation:
    +Use `Authoring` to find models and inspect their definitions. Its reports
    +can also be read by tools such as an AI coding agent:
     
     - `Authoring.available_processes()` and `Authoring.available_models(...)`
    -  discover models from loaded Julia modules;
    -- `Authoring.describe_model(instance)` records explicit process identity plus
    -  the parameter values, ports, contracts, dependencies, traits, metadata, and
    -  source location of a concrete instance. Its nested `field_provenance`
    -  separately marks exact or declared model information, inferred source
    -  information, and the independent provenance of constructor fields, defaults,
    -  and methods;
    -- `Authoring.describe_model(ModelType)` is explicitly best effort and returns
    -  an incomplete report when no real zero-argument constructor exists. It never
    -  fabricates parameter values or constructs a dummy instance;
    +  find models in loaded Julia modules;
    +- `Authoring.describe_model(instance)` reports the model's process, current
    +  parameter values, inputs and outputs, variable contracts, input connections
    +  and calls, execution settings, descriptive information, and source location.
    +  Its nested `field_provenance` explains where each fact came from: the actual
    +  model, an explicit declaration, or information inferred from the source.
    +  The origins of constructor fields, defaults, and methods are reported
    +  separately;
    +- `Authoring.describe_model(ModelType)` reports whatever can be determined
    +  from the type. If it has no constructor that can be called without
    +  arguments, the report is incomplete. It never invents parameter values
    +  or creates a dummy instance;
     - current values from `Authoring.describe_model(instance)` stay in
       `parameters`; they are never relabeled as constructor defaults. Defaults are
    -  inspected only from a real zero-argument type description;
    -- `Authoring.model_interface(instance)` returns the exact interface enforced
    -  for object and instance overrides;
    -- `Authoring.model_interface(ModelType)` is best effort only when that type has
    -  a real zero-argument constructor; otherwise it raises `ArgumentError` rather
    -  than inventing parameter values;
    -- `Authoring.compare_models(a, b)` distinguishes common process identity from
    -  direct override compatibility, binding changes, and other scenario
    -  reconfiguration. Inspect `requires_binding_changes` for interface differences
    -  that affect binding configuration and `requires_reconfiguration` for any
    -  difference that prevents a direct override; each structured difference
    +  inspected from a type description only when a constructor can actually be
    +  called without arguments;
    +- `Authoring.model_interface(instance)` returns the declarations checked
    +  when replacing a model with `Override` for an object or plant instance;
    +- `Authoring.model_interface(ModelType)` attempts the same inspection using
    +  a constructor with no arguments. If no such constructor exists, it raises
    +  `ArgumentError` rather than inventing parameter values;
    +- `Authoring.compare_models(a, b)` reports whether two models share a process,
    +  whether one can directly replace the other, and what settings would need to
    +  change. `requires_binding_changes` reports differences in inputs, outputs,
    +  variable contracts, dependencies, output policies, or environmental
    +  connections. `requires_reconfiguration` covers every
    +  difference preventing a direct override. Each reported difference
       records its `path`, `kind`, values, `affects_override`, and
       `affects_bindings`;
    -- `Authoring.validate_model(model; strict=false)` validates declarations
    -  without executing the scientific kernel. Strict mode requires a complete
    -  `VariableContract` for every declared port;
    -- `Authoring.validate_scenario(model; strict=false)` preserves a partial
    -  compilation report and structured diagnostics for incomplete scenarios;
    -- `Authoring.to_dict(report)` and `Authoring.to_json(report)` expose the
    -  versioned report schema without serializing compiler internals;
    +- `Authoring.validate_model(model; strict=false)` checks declarations
    +  without running the equation. Strict mode requires a complete
    +  `VariableContract` for every declared input and output, including
    +  environmental variables;
    +- `Authoring.validate_scenario(model; strict=false)` checks the simulation
    +  setup. If it is incomplete, the result still includes a partial report
    +  and diagnostic information;
    +- `Authoring.to_dict(report)` and `Authoring.to_json(report)` convert reports
    +  to dictionaries or JSON using a versioned format. They do not include
    +  internal compiler objects;
     - `Authoring.scenario_source(model; environments=...)` reconstructs readable,
    -  editable Julia scenario code. Pass named runtime environment values through
    +  editable Julia simulation setup code. Pass named environment values through
       `environments` so the generated code can refer to them explicitly;
    -- `Authoring.compiled_model_source(model_or_simulation)` produces an executable,
    -  readable view of the resolved application order, targets, input provenance,
    -  calls, and invoked kernel bodies;
    +- `Authoring.compiled_model_source(model_or_simulation)` produces readable,
    +  executable Julia code showing the chosen application order, selected
    +  objects, input sources, model calls, and calculation functions;
     - `Authoring.write_compiled_model_source(path, value)` writes that view
       explicitly.
     
    -These functions validate declared structure and coupling. They do not infer
    -units, assumptions, references, domains of validity, or scientific
    -equivalence. The compiled source is an explanatory execution view over the
    -normal compiled runtime, not a second scheduler. Use `scenario_source` when the
    -goal is to edit or version the scenario declaration; use
    -`compiled_model_source` when the goal is to inspect what compilation resolved.
    -A package may extend `Authoring.model_metadata(model)` with explicit metadata
    -such as summary, hypothesis, references, and maturity. It may extend
    -`Authoring.parameter_metadata(model)` with descriptions, units, domains,
    -defaults, references, or constraints for the fields of the model type.
    +These functions check the declared models and their connections. They do not
    +guess units, assumptions, references, valid use conditions, or whether two
    +equations are scientifically equivalent. The generated execution code uses
    +the normal runtime; it does not create a separate way to schedule the models.
    +
    +Use `scenario_source` to edit or save the simulation setup in version
    +control. Use `compiled_model_source` to inspect the calculations and
    +connections PlantSimEngine prepared from that setup.
    +
    +A model package can implement `Authoring.model_metadata(model)` to supply a
    +summary, hypothesis, references, and development or validation status. It can
    +implement `Authoring.parameter_metadata(model)` to describe each parameter's
    +meaning, units, valid range, defaults, references, or other constraints.
     
     ### CompositeModel graph visualization and editing
     
    -- `GraphEditor.model_graph_view(model; level=:applications)` returns the typed graph view.
    -- `GraphEditor.model_graph_view_json(model)` serializes the same DTO used by the browser.
    +- `GraphEditor.model_graph_view(model; level=:applications)` returns graph
    +  data describing the models and their connections.
    +- `GraphEditor.model_graph_view_json(model)` converts the same graph data
    +  used by the browser to JSON.
     - `GraphEditor.write_model_graph_view(path, model)` writes a self-contained static viewer.
    -- `GraphEditor.edit_graph(model; templates=..., environments=...)` starts the optional HTTP editor after `using HTTP` and keeps catalog values authoritative in Julia.
    +- `GraphEditor.edit_graph(model; templates=..., environments=...)` starts the
    +  optional HTTP editor after `using HTTP`. The editor uses the templates
    +  and environment values supplied from Julia.
     - `GraphEditor.current_model(session)`, `GraphEditor.undo!(session)`,
       `GraphEditor.redo!(session)`, and `close(session)` control an interactive
       session from Julia.
     
     See [Visualize And Edit A CompositeModel](../guides/graph_visualizer_editor.md) for the
    -runnable workflow, model discovery, selector previews, cycle breaking, and
    -Documenter embedding.
    +runnable workflow, finding models, previewing selected objects, handling
    +dependency cycles, and embedding a graph in Documenter pages.
     
     ### Environment backend extensions
     
    -Backend packages extend the protocol under `EnvironmentAPI`, including
    +Packages that provide environmental data implement functions under
    +`EnvironmentAPI`, including
     `EnvironmentAPI.AbstractEnvironmentBackend`,
     `EnvironmentAPI.bind_environment`, `EnvironmentAPI.sample`,
     `EnvironmentAPI.commit_environment!`, and `EnvironmentAPI.update_index!`.
    -The root-level `commit_environment!` remains part of the ordinary model-kernel
    -workflow for committing an accepted controller state.
    +The root-level `commit_environment!` is the function a model calls to store
    +accepted environmental values after a trial calculation.
     
     ### Fitting and evaluation
     
    -Generic fitting and metrics live under `Evaluation`: `Evaluation.fit`,
    +Parameter fitting and evaluation metrics are available under `Evaluation`: `Evaluation.fit`,
     `Evaluation.RMSE`, `Evaluation.NRMSE`, `Evaluation.EF`, and `Evaluation.dr`.
     PlantMeteo reducers are accessed from `PlantMeteo` directly rather than being
     re-exported by PlantSimEngine.
    @@ -312,11 +366,11 @@ re-exported by PlantSimEngine.
     PlantSimEngine.Advanced
     ```
     
    -Compiler representations, cache refresh operations, and low-level binding
    -compilers live under `PlantSimEngine.Advanced`. They are intended for package
    -integration, diagnostics development, and compiler work rather than ordinary
    -scenario composition. Prefer `Diagnostics.explain_*`, which accepts a
    -`CompositeModel` directly, over manually compiling and inspecting fields.
    +`PlantSimEngine.Advanced` contains the compiler's data structures and functions
    +for preparing connections and refreshing cached information. Use it when
    +integrating a package or developing the compiler and its diagnostics. For
    +ordinary simulations, use `Diagnostics.explain_*` with a `CompositeModel`
    +to inspect the setup.
     
     Examples include `Advanced.compile_composite_model`, `Advanced.refresh_bindings!`, and
     the `Advanced.CompiledCompositeModel` family. These qualified APIs may evolve more
    diff --git a/docs/src/API/model_catalog.md b/docs/src/API/model_catalog.md
    index a578fba81..0b07f8394 100644
    --- a/docs/src/API/model_catalog.md
    +++ b/docs/src/API/model_catalog.md
    @@ -49,19 +49,23 @@ description = Authoring.describe_model(candidate)
     )
     ```
     
    -Read the model's equations, assumptions, units, parameter meanings, domain of
    -validity, and validation evidence in its package documentation. The report can
    -expose declared metadata, but cannot supply missing scientific evidence.
    +Read the package documentation to understand the equations, parameter units,
    +assumptions, and conditions where the model has been tested. The report only
    +shows the information its author has supplied.
     
    -`variable_contracts(candidate)` returns the declared physical meanings of its
    -variables. When a candidate has no contracts, that information remains
    -missing. Before using a model in a contracted connection, its author must
    -provide the matching declarations or an explicit physical adapter.
    +`variable_contracts(candidate)` shows the units and physical meaning recorded
    +for each variable. These declarations are called **variable contracts**. If
    +one side of a connection has a contract, the other side must have the same
    +one. Ask the model author to add missing declarations. If the quantities
    +differ, for example a value per plant and a value per ground area, use a
    +separate model to perform the conversion.
     
     ## Check whether a candidate fits your simulation
     
     1. Confirm that its inputs can come from your data, environment, or other models.
    -2. Compare units, basis, timing, and aggregation at every connection.
    +2. Check that connected values have the same units, refer to the same area
    +   or object, and describe the same time period. Check whether each is a
    +   total, an average, or a rate.
     3. Use `Authoring.compare_models(current, candidate)` when replacing a model.
     4. Validate the assembled scenario with `Authoring.validate_scenario`, then
        check a small run against an expected result.
    @@ -73,9 +77,10 @@ See [Coupling models](@ref) for connection choices and
     
     The following table is generated from the loaded modules. `complete=false`
     means a type could not provide a complete description, for example because
    -it needs constructor arguments. Inspect a concrete instance before making
    -a choice. `provenance` distinguishes declared information from best-effort
    -inspection; detailed reports also record provenance field by field.
    +it needs parameter values before it can be created. Create a model with those
    +parameters and inspect it before choosing it. The `provenance` column records
    +where the description came from: the author's declarations or information
    +found by examining the code. Detailed reports give this source for each field.
     
     ```@setup loaded-model-catalog
     rows = NamedTuple[]
    @@ -102,5 +107,5 @@ catalog
     To reproduce this discovery yourself, loop over
     `Authoring.available_processes()` and call
     `Authoring.available_models(process_type)` for each process. Pass a concrete
    -instance to `Authoring.describe_model` for its parameters, variables,
    -contracts, metadata, and diagnostic messages.
    +model, such as `ToyRUEGrowthModel(0.2)`, to `Authoring.describe_model` for its
    +parameters, variables, physical meanings, and any problems found.
    diff --git a/docs/src/API/public_symbols.md b/docs/src/API/public_symbols.md
    index b4a5abae7..ed9b60022 100644
    --- a/docs/src/API/public_symbols.md
    +++ b/docs/src/API/public_symbols.md
    @@ -1,150 +1,159 @@
     # Public Symbol Inventory
     
    -This page records the supported default namespace and the five focused public
    -submodules. Compiler representations and cache controls are intentionally
    -listed separately under [`PlantSimEngine.Advanced`](@ref "Advanced namespace").
    -
    -`using PlantSimEngine` imports the ordinary model-author and simulation-user
    -workflow plus the `Authoring`, `Diagnostics`, `GraphEditor`, `EnvironmentAPI`,
    -and `Evaluation` module names. Their members remain qualified unless a user
    -explicitly imports one of those submodules.
    +This page lists public names by purpose. Names for compiler data structures
    +and cached information are listed separately under
    +[`PlantSimEngine.Advanced`](@ref "Advanced namespace").
    +
    +`using PlantSimEngine` makes the usual modeling and simulation functions
    +available, along with the `Authoring`, `Diagnostics`, `GraphEditor`,
    +`EnvironmentAPI`, and `Evaluation` module names. Functions in those modules
    +keep their prefix, such as `Diagnostics.explain_bindings`, unless you import
    +the module's functions explicitly.
    +
    +An object's **status** stores its changing values. A **model application**
    +configures a model's name, objects, connections, and timing. An input
    +**binding** connects a model to a source value, and its **carrier** holds
    +that connection, usually through a shared reference. The [Public API](@ref)
    +explains the functions and their requirements in detail.
     
     ## Scenario composition
     
    -- CompositeModel structure: `CompositeModel`, `Object`, `ObjectId`, `CompositeModelTemplate`,
    +- Building a simulation: `CompositeModel`, `Object`, `ObjectId`, `CompositeModelTemplate`,
       `ObjectInstance`, `Override`.
    -- Applications: `ModelSpec`, `OutputTo`, `Environment`, and `Updates`.
    -- Application inspection: `application_name`, `applies_to`, `value_inputs`,
    +- Configuring model applications: `ModelSpec`, `OutputTo`, `Environment`, and `Updates`.
    +- Inspecting application settings: `application_name`, `applies_to`, `value_inputs`,
       `model_calls`, `outputs_to`, `environment_config`, `output_routing`,
       `updates`.
    -- Dependency defaults: `Input`, `Call`, `Initializer`, `PreviousTimeStep`.
    +- Default input connections and calls: `Input`, `Call`, `Initializer`, `PreviousTimeStep`.
     
     ## Object selectors and queries
     
    -- Multiplicity: `One`, `OptionalOne`, `Many`.
    -- Scope and topology: `SceneScope`, `Self`, `Subtree`, `SelfPlant`, `Ancestor`,
    +- Choosing the number of objects: `One`, `OptionalOne`, `Many`.
    +- Choosing where to look and how objects are connected: `SceneScope`, `Self`, `Subtree`, `SelfPlant`, `Ancestor`,
       `Scope`, `Relation`.
     - Label criteria are selector keywords: `kind`, `species`, `scale`, and
       `name`.
    -- Identity and queries: `object_id`, `object_ids`, `model_objects`,
    +- Finding objects and their identifiers: `object_id`, `object_ids`, `model_objects`,
       `resolve_object_ids`, `resolve_objects`.
     - Object data: `geometry`, `position`, `bounds`.
     
     ## Execution, lifecycle, and outputs
     
    -- Execution: `run!`, `continue!`, `step!`, `Simulation`, `current_step`,
    +- Running and advancing a simulation: `run!`, `continue!`, `step!`, `Simulation`, `current_step`,
       `runtime_model`, `final_state`.
     - Output selection and collection: `OutputRequest`, `outputs`,
       `collect_outputs`.
    -- Distributed output assignment: `OutputTargets`, `output_targets`, and
    -  `assign_outputs!`. Access destination carriers through
    -  `targets.columns.` and aligned identities through
    -  `object_ids(targets)`.
    -- Lifecycle: `register_object!`, `add_organ!`, `remove_object!`,
    +- Writing results to several objects: `OutputTargets`, `output_targets`, and
    +  `assign_outputs!`. Access each variable's destination values through
    +  `targets.columns.` and their object identifiers, in the same
    +  order, through `object_ids(targets)`.
    +- Adding, removing, or moving objects: `register_object!`, `add_organ!`, `remove_object!`,
       `reparent_object!`, `move_object!`, `update_geometry!`,
       `mark_environment_binding_dirty!`, `objects_from_mtg`.
    -- Hard calls: `RunContext`, `CallTarget`, `CallTargets`, `call_model`,
    +- Running another model from within a calculation: `RunContext`, `CallTarget`, `CallTargets`, `call_model`,
       `call_targets`, `run_call!`.
    -- Newborn initialization: `Initializer`, `run_initializer!`.
    +- Setting initial values when an object is created: `Initializer`, `run_initializer!`.
     
     ## Diagnostics namespace
     
    -`PlantSimEngine.Diagnostics` owns structured explanations and supported
    -inspection:
    +`PlantSimEngine.Diagnostics` explains how the simulation is set up and what
    +happened during execution:
     
     - Structure: `Diagnostics.explain_objects`, `Diagnostics.explain_instances`, `Diagnostics.explain_scopes`.
    -- Compilation: `Diagnostics.explain_applications`, `Diagnostics.explain_bindings`,
    +- Model connections and execution order: `Diagnostics.explain_applications`, `Diagnostics.explain_bindings`,
       `Diagnostics.explain_calls`, `Diagnostics.explain_output_bindings`,
       `Diagnostics.explain_writers`,
       `Diagnostics.explain_schedule`, `Diagnostics.explain_execution_plan`.
     - Initialization, environment, and outputs: `Diagnostics.explain_initialization`,
       `Diagnostics.explain_environment`, `Diagnostics.explain_environment_bindings`,
       `Diagnostics.explain_output_retention`, `Diagnostics.explain_outputs`.
    -- Supported carrier inspection: `Diagnostics.input_carrier`, `Diagnostics.input_value`,
    +- Inspecting an input's connection and current value: `Diagnostics.input_carrier`, `Diagnostics.input_value`,
       `Diagnostics.has_reference_carrier`.
    -- Normalized selector addresses: `Diagnostics.ObjectAddress`,
    +- Consistent descriptions of selected objects: `Diagnostics.ObjectAddress`,
       `Diagnostics.object_address`.
     
     ## Authoring namespace
     
    -`PlantSimEngine.Authoring` is the stable inspection surface for model authors
    -and agents:
    +`PlantSimEngine.Authoring` helps model authors and coding agents find,
    +inspect, and check models:
     
    -- Discovery: `Authoring.available_processes` and
    +- Finding loaded models: `Authoring.available_processes` and
       `Authoring.available_models`.
    -- Exact instance description: `Authoring.describe_model` and
    +- Describing a model with its chosen parameters: `Authoring.describe_model` and
       `Authoring.model_interface`.
    -- Alternative comparison: `Authoring.compare_models`, including separate
    +- Comparing alternatives: `Authoring.compare_models`, including separate
       `requires_binding_changes` and `requires_reconfiguration` results.
    -- Structural validation: `Authoring.validate_model` and
    +- Checking declarations and connections: `Authoring.validate_model` and
       `Authoring.validate_scenario`.
    -- Explicit scientific metadata: `Authoring.model_metadata` and
    +- Supplying scientific descriptions: `Authoring.model_metadata` and
       `Authoring.parameter_metadata`.
    -- Stable serialization: `Authoring.to_dict`, `Authoring.to_json`, and
    +- Converting reports to versioned dictionaries or JSON: `Authoring.to_dict`, `Authoring.to_json`, and
       `Authoring.SCHEMA_VERSION`.
    -- Editable scenario reconstruction: `Authoring.scenario_source`.
    -- Readable resolved execution: `Authoring.compiled_model_source` and
    +- Generating editable simulation setup code: `Authoring.scenario_source`.
    +- Showing the calculations and connections prepared for execution: `Authoring.compiled_model_source` and
       `Authoring.write_compiled_model_source`.
     - Typed reports: `Authoring.ModelDescription`, `Authoring.ModelComparison`,
       `Authoring.ModelValidationReport`, and
       `Authoring.ScenarioValidationReport`.
     
    -A description built from a concrete instance includes its explicit `process`
    -identity and exact model values. Inspect nested `field_provenance` because
    -declarations, source information, and constructor metadata have independent
    -origins. A type-only description is best effort and never invents constructor
    -arguments or a dummy instance. A type-only interface requires a real
    -zero-argument constructor. Authoring validation is structural; it does not
    -establish scientific validity.
    +A description of a model instance includes its declared `process` and
    +actual parameter values. The nested `field_provenance` records where each
    +piece of information came from; declarations, source information, and
    +constructor information can have different origins.
    +
    +When you supply a type rather than an instance, the description reports only
    +what can be determined. It never invents constructor arguments or creates a
    +dummy model. To obtain the model interface from a type, that type must have
    +a constructor that can be called without arguments. These checks concern
    +model definitions and connections; they do not prove scientific validity.
     
     ## Model-author contract
     
     - Model identity: `AbstractModel`, `@process`, `process`.
    -- State schema and initialization: `Status`, `Required`, `Default`,
    +- Declaring variables and initial values: `Status`, `Required`, `Default`,
       `VariableContract`, `variable_contracts`, `init_variables`, `dep`.
    -- Model IO inspection: `inputs`, `outputs`, `variables`,
    +- Inspecting inputs and outputs: `inputs`, `outputs`, `variables`,
       `environment_inputs`, `environment_outputs`,
       `validate_environment_inputs`.
    -- Identity-aware many-input access: `bound_input`, `BoundMany`, and
    +- Reading several input values together with their source object identifiers: `bound_input`, `BoundMany`, and
       `object_ids`.
    -- Timing and routing traits: `timespec`, `output_policy`, `timestep_hint`,
    +- Timing and environment settings: `timespec`, `output_policy`, `timestep_hint`,
       `environment_hint`, `environment_bindings`, `environment_window`.
     
    -The underscore declarations `inputs_`, `outputs_`, `environment_inputs_`,
    -`environment_outputs_`, and `variable_contracts_` are intentionally
    -unexported extension functions.
    -Model authors implement them with qualified definitions such as
    +Model authors implement `inputs_`, `outputs_`, `environment_inputs_`,
    +`environment_outputs_`, and `variable_contracts_` with the package prefix, such as
     `PlantSimEngine.inputs_(model) = ...`. `inputs_` must return explicit
     `Required(T)` or `Default(value)` declarations; `outputs_` returns initial
    -output-state values; `variable_contracts_` returns `VariableContract` metadata
    -for declared status or environment variables.
    +output values; `variable_contracts_` returns `VariableContract` descriptions
    +of declared status or environment variables. These functions ending in `_`
    +are not imported by `using PlantSimEngine`.
     
     ## Time and reducers
     
    -- Scheduling: `ClockSpec`, `SchedulePolicy`, `HoldLast`, `Interpolate`,
    +- Choosing when models run and how inputs combine values over time: `ClockSpec`, `SchedulePolicy`, `HoldLast`, `Interpolate`,
       `Integrate`, `Aggregate`.
     - Meteorology reducers are not re-exported. Use qualified PlantMeteo names,
       for example `PlantMeteo.MeanReducer` or `PlantMeteo.RadiationEnergy`.
     
     ## EnvironmentAPI namespace
     
    -- Backend contract: `EnvironmentAPI.AbstractEnvironmentBackend`, `EnvironmentAPI.EnvironmentContext`,
    +- Implementing a provider of environmental data: `EnvironmentAPI.AbstractEnvironmentBackend`, `EnvironmentAPI.EnvironmentContext`,
       `EnvironmentAPI.GlobalConstant`, `EnvironmentAPI.environment_backend`, `EnvironmentAPI.environment_variables`,
       `EnvironmentAPI.base_step_seconds`, `EnvironmentAPI.get_nsteps`, and
       `EnvironmentAPI.bind_environment`.
    -- Sampling and mutation: `EnvironmentAPI.sample`,
    +- Reading and updating environmental values: `EnvironmentAPI.sample`,
       `EnvironmentAPI.sample_environment`, `EnvironmentAPI.commit_environment!`,
       and `EnvironmentAPI.update_index!`.
     - PlantMeteo conveniences: `Atmosphere`, `Constants`, `Weather`.
     
     ## GraphEditor namespace
     
    -- Graph DTOs: `GraphEditor.ModelGraphView` and
    +- Graph data: `GraphEditor.ModelGraphView` and
       `GraphEditor.model_graph_view`.
    -- Serialization and static views: `GraphEditor.model_graph_view_json`,
    +- Saving graph data and displaying static graphs: `GraphEditor.model_graph_view_json`,
       `GraphEditor.model_graph_view_html`, and
       `GraphEditor.write_model_graph_view`.
    -- Semantic edits and sessions live under the same namespace, including
    +- Editing models and connections, and controlling editor sessions:
       `GraphEditor.AddModelApplication`, `GraphEditor.apply_model_graph_edit`,
       `GraphEditor.edit_graph`, `GraphEditor.current_model`,
       `GraphEditor.undo!`, and `GraphEditor.redo!`.
    @@ -156,7 +165,8 @@ for declared status or environment variables.
     
     ## Advanced namespace
     
    -`PlantSimEngine.Advanced` contains the qualified compiler and cache API:
    +`PlantSimEngine.Advanced` contains the compiler's data structures and the
    +functions that prepare and update cached information:
     
     - registries and compiled representations: `ObjectRegistry`, `CompiledCompositeModel`,
       `CompiledModelApplication`, `CompiledModelInputBinding`,
    @@ -172,11 +182,10 @@ for declared status or environment variables.
       `model_revision`, `environment_revision`, `compiled_bindings`,
       `compiled_environment_bindings`.
     
    -These names require explicit qualification or `using PlantSimEngine.Advanced`.
    -They are not part of the concise user namespace and may evolve with compiler
    -implementation requirements.
    +Use these names with the `Advanced` prefix, or import them with
    +`using PlantSimEngine.Advanced`. They are not imported by
    +`using PlantSimEngine` and may change as the compiler develops.
     
    -The namespace-boundary test in `test/test-model-api-stabilization.jl` compares
    -the complete default public-name set with an explicit inventory and separately
    -checks every focused submodule. Adding or removing an export therefore requires
    -an intentional inventory update.
    +The test in `test/test-model-api-stabilization.jl` compares the full set of
    +public names with an explicit list and checks each submodule separately.
    +When adding or removing an exported name, update that list as well.
    diff --git a/docs/src/agent_skill.md b/docs/src/agent_skill.md
    index dfa028350..0092d025e 100644
    --- a/docs/src/agent_skill.md
    +++ b/docs/src/agent_skill.md
    @@ -3,7 +3,8 @@
     An **AI coding agent** can read and edit code and run tests with the tools you
     make available. PlantSimEngine provides an optional skill with instructions
     and executable examples for implementing process models and assembling
    -simulations.
    +simulations. A **skill** is a folder of instructions and examples that your
    +agent can read while working.
     
     You choose the scientific question, assumptions, references, and validation
     evidence. The agent can help turn those choices into code and check that the
    @@ -24,7 +25,7 @@ skill_root = joinpath(package_root, "skills", "plantsimengine")
     Copy or link that complete directory into your coding agent's skill directory,
     following its installation instructions. Keep its `SKILL.md`, `references`,
     `assets`, and `scripts` together. Use the copy from your installed package
    -rather than a floating `main` branch: different versions can describe
    +rather than the latest `main` branch: different versions can describe
     different APIs.
     
     The skill uses the Julia tools available in your agent's environment. It does
    @@ -38,12 +39,12 @@ Adapt this request to the packages, data, and scientific question you have:
     > compare two existing models for the same process. First identify available
     > alternatives and explain their inputs, outputs, units, and assumptions.
     > Show what data each needs and flag anything that is missing. Build the
    -> smallest simulation that can compare them with the same forcing. Explain
    +> smallest simulation that can compare them with the same input data. Explain
     > their connections, run the tests, and show the results. Keep any required
     > physical conversion explicit.
     
     Useful inputs to provide are the package names, the process of interest,
    -your forcing data and its units, and the outputs you want to compare. The
    +your input data and its units, and the outputs you want to compare. The
     [Loaded model catalog](@ref) and [Model compatibility and replacement](@ref)
     show the corresponding manual workflow.
     
    @@ -55,8 +56,9 @@ parameter values and units, and at least one expected result:
     > Use the PlantSimEngine skill shipped with my loaded package to implement
     > the equation and variable definitions I provide. Check whether an existing
     > process already represents this question. Keep the equation readable and
    -> declare its inputs, outputs, and complete scientific contracts. Test one
    -> calculation against my reference result, then compose it on one object.
    +> declare each input and output, including its units and physical meaning.
    +> Test one calculation against my reference result, then use the model in
    +> a simulation of one object.
     > If any assumption, physical conversion, parameter, or validation criterion
     > is unspecified, identify it before choosing one. Show the code, test
     > results, and remaining scientific questions.
    @@ -72,19 +74,20 @@ Expect a concrete result you can inspect:
     - the loaded package path and version;
     - the selected process and hypotheses, with the evidence supplied for them;
     - readable model code and scenario configuration;
    -- checks of required inputs, physical contracts, and resolved connections;
    +- checks that the required inputs are available, their units and meanings
    +  match, and the models receive values from the intended sources;
     - tests actually run, their results, and any remaining limitations.
     
     PlantSimEngine's `Authoring` reports describe models and compare interfaces.
    -`Diagnostics` explains inputs, connections, timing, and execution. Reports
    -mark missing or inferred information so it can be distinguished from declared
    -facts. These checks help find implementation and coupling errors; scientific
    +`Diagnostics` explains inputs, connections, timing, and execution. The reports
    +show which information comes from the model author, which was found by
    +examining the code, and which is missing. These checks help find implementation and coupling errors; scientific
     validation still needs suitable observations or reference results.
     
     ## Check the packaged examples
     
     The skill includes a minimal model, alternative hypotheses, a physical
    -adapter, coupling examples, and their tests. You or your agent can check
    +conversion model, coupling examples, and their tests. You or your agent can check
     the copy supplied by the installed package with:
     
     ```julia
    diff --git a/docs/src/developers.md b/docs/src/developers.md
    index 92591f76c..b63e7a9dd 100644
    --- a/docs/src/developers.md
    +++ b/docs/src/developers.md
    @@ -92,12 +92,11 @@ simulation = run!(model; steps=48, outputs=:none, performance=true)
     Diagnostics.explain_runtime_performance(simulation)
     ```
     
    -The returned rows distinguish immutable-plan compilation, object-target
    -instantiation, lifecycle-buffer updates, steady-state execution, output
    -collection, and whole-initialization totals. Use these counters to establish
    -where work occurred, not as a microbenchmark. Timing instrumentation calls
    -`time_ns()` at runtime boundaries, so benchmark ordinary execution separately
    -with `performance=false` after warming the simulation.
    +The report separates time spent preparing the simulation, selecting objects,
    +updating after structural changes, running the models, and collecting outputs.
    +Use it to find which stage takes time. Measuring those stages also adds some
    +work, so measure normal execution separately with `performance=false` after
    +a first run has allowed Julia to compile the code.
     
     ## CI workflows
     
    @@ -210,7 +209,6 @@ were editing.
     
     ### Coverage gaps to keep in mind
     
    -Not every combination of weather structure, status shape, mapping layout, and
    -downstream usage is covered directly in PlantSimEngine. When changing the public
    -API or runtime semantics, treat downstream integration results as part of the
    -validation surface, not as optional extra signal.
    +PlantSimEngine's own tests cannot cover every weather format, object
    +configuration, and use in other packages. When changing public functions or
    +how simulations run, also run the tests of packages that depend on them.
    diff --git a/docs/src/guides/coupling.md b/docs/src/guides/coupling.md
    index 8e31fafc2..869b89953 100644
    --- a/docs/src/guides/coupling.md
    +++ b/docs/src/guides/coupling.md
    @@ -1,78 +1,107 @@
     # Coupling models
     
    -PlantSimEngine has three coupling mechanisms with different ownership:
    +Coupling means letting models work together. For example, a light model can
    +calculate the radiation that a growth model needs. The growth model then reads
    +that result as an input.
     
    -| Mechanism | Use it when | Who owns the operation? |
    +Choose how to connect your models from the calculation you need:
    +
    +| Connection | Example | How to set it up |
     |---|---|---|
    -| Value coupling | A consumer reads a producer's accepted value | The scenario declares `inputs`; the compiler transports references or streams |
    -| Hard call | A parent must choose when or how often a child executes | The parent declares `Call` and invokes `run_call!` |
    -| Adapter model | Unit, basis, temporal meaning, or aggregation changes | An ordinary model owns and tests the explicit conversion |
    -
    -Use `inputs` when a model reads a value produced by another application. A
    -unique same-object producer is inferred; cross-object sources should use an
    -explicit `One`, `OptionalOne`, or `Many` selector. Inspect the resolved
    -references with `Diagnostics.explain_bindings`.
    -
    -Use `calls` only when a parent algorithm owns child execution or iteration.
    -Use `run_call!(context, :name)` to execute every resolved target. Pass
    -`sampled_environment=value` to this bulk path when the caller already has one
    -model-facing environment for all targets. Use `call_model(context, :name)` to
    -inspect a singular dependency model without materializing a public target. For
    -selection, target status access, custom ordering, or distinct environments,
    -retrieve the vector-like collection with `call_targets(context, :name)` and
    -execute individual targets. Trial calls use `publish=false`; accepted state is
    -published once.
    -Nested calls inherit publication suppression, so a descendant cannot publish
    -inside an unpublished ancestor trial. `Diagnostics.explain_calls` and `Diagnostics.explain_schedule`
    -show call-only targets and ordering.
    -
    -`Diagnostics.explain_initialization(model)` classifies values as supplied, generated,
    -producer-bound, defaulted, required, or environment-bound before execution.
    +| Value coupling | Growth reads the radiation calculated by a light model | Connect the output to the input with `inputs` |
    +| Manual call, also called a hard dependency | An energy-balance model runs photosynthesis at several trial leaf temperatures | Declare `Call`, then use `run_call!` inside the energy-balance calculation |
    +| Adapter model | Radiation per square metre must become radiation per plant | Write a small model that performs the conversion |
    +
    +A **model application** is a model configured with `ModelSpec`: it has a name,
    +a choice of objects, and any settings needed for its inputs or timing.
     
     ## Value coupling
     
    -A consumer on the same object needs no scenario syntax when exactly one
    -canonical producer exists. Make cross-object intent explicit:
    +Suppose a growth model needs `absorbed_par`. If exactly one model on the same
    +object writes that variable, PlantSimEngine connects them automatically and
    +runs the light calculation first. When the value comes from another object,
    +describe where to find it in `ModelSpec(...; inputs=...)`.
    +
    +Use these selectors to choose how many objects supply a value:
    +
    +| Selector | Meaning |
    +|---|---|
    +| `One(...)` | Exactly one match is required; zero or several matches are errors |
    +| `OptionalOne(...)` | A match may be absent; use this only if your model can meaningfully handle that absence |
    +| `Many(...)` | Read several values, for example to add the respiration of all leaves |
    +
    +The `within` setting limits where PlantSimEngine looks. `Self()` means the
    +current object. `Subtree()` means that object and its descendants, such as a
    +plant and its organs. `SelfPlant()` means the plant instance to which the
    +current object belongs. `SceneScope()` searches the whole simulation.
    +
    +[Implement Cross-Object Values](@ref) gives a complete example: a plant model
    +reads respiration from its own leaves with `Many(...; within=Subtree())`.
    +The model then adds those values. Choosing `Many` does not perform the sum
    +for you.
    +
    +Normally, PlantSimEngine identifies which model writes an input and runs it
    +before the model that reads it. Sometimes you want to read a value already
    +stored in an object's `Status`, such as an initial soil-water reserve. Use
    +`from_status=true` for this case.
    +
    +That setting reads the current stored value directly. It does not look for a
    +model that should calculate it first. If you need to wait for a particular
    +calculation, name it with `after`, for example
    +`Many(...; var=:reserve, from_status=true, after=:plant_allocation)`.
    +Otherwise, the applications keep their order in the scenario. You cannot
    +combine `from_status=true` with `process`, `application`, `policy`, or `window`.
     
    -[Implement Cross-Object Values](@ref) is the executable example: a plant
    -consumer receives a `Many(...; within=Subtree())` collection from its own
    -leaves while PlantSimEngine preserves each source object's identity.
    +Use `Diagnostics.explain_bindings(model)` to see where each input comes from.
    +`Diagnostics.explain_initialization(model)` shows how starting values are
    +obtained, including values you supplied, defaults, and missing required inputs.
     
    -`One` is a contract: zero or multiple matches are errors. Use `OptionalOne`
    -only when absence has a scientific meaning, and `Many` when aggregation is
    -part of the consumer model. `within=Subtree()` searches descendants of the
    -current target; `within=Self()` selects only the current target;
    -`within=SelfPlant()` anchors repeated plant instances; and `SceneScope()`
    -is deliberately global.
    +## Manual calls
     
    -By default, an input selector also identifies applications that produce the
    -selected variable, and those producers are scheduled before the consumer. Use
    -`from_status=true` only when the input deliberately reads the objects' current
    -`Status` references independently of any producer. Declare that selector as
    -`Many(...; var=:reserve, from_status=true, after=:plant_allocation)`.
    +A model that controls another model's calculation is called a **controller**.
    +For example, an energy-balance controller can try different temperatures,
    +run photosynthesis at each temperature, and keep the result that satisfies
    +its energy-balance equation.
     
    -This is a same-step live-reference binding. It cannot be combined with
    -`process`, `application`, `policy`, or `window`. It does not infer a producer
    -edge; use `after=:application_id` when the state must be read or mutated after
    -a particular application. Otherwise, the scenario's application order is
    -preserved.
    +Declare the models to call with `Call` in the controller's `dep` method, or
    +with `ModelSpec(...; calls=...)` in the scenario. Then use:
     
    -## Manual calls
    +| What the controller needs to do | Function |
    +|---|---|
    +| Run all models selected by a named call | `run_call!(context, :leaf_energy)` |
    +| Choose individual models or objects to run | `call_targets(context, :leaf_energy)`, then `run_call!(target)` |
    +| Read a called model's type or parameters | `call_model(context, :leaf_energy)`; this requires exactly one match |
     
    -[Implement A Hard Dependency](@ref) is the executable parent/child example.
    -Inside a controller, iterate over `call_targets(context, :leaf_energy)`. Run
    -candidate states with `run_call!(target; publish=false)` and publish the
    -accepted state once with `publish=true`. A call-only target is excluded from
    -root scheduling, and an unpublished outer call suppresses publication by every
    -nested descendant.
    +Here a **target** is one selected model application on one object. If the
    +controller has already prepared the environmental values for every target,
    +pass them as `sampled_environment=value` to `run_call!`. Use individual
    +targets when each object needs different values.
    +
    +Trial calls use `publish=false`, the default. Their results are not saved as
    +accepted output samples for time-based connections or output history. Use
    +`publish=true` for the accepted calculation. This setting does not undo
    +changes to model state: the controller must handle any changes that should
    +be discarded after a trial.
    +
    +A model used only through calls runs when its controller calls it; it does
    +not also run independently. If a trial calls further models, those nested
    +calls cannot save accepted samples either. The same rule prevents a nested
    +controller from committing trial values to the environment.
    +
    +[Implement A Hard Dependency](@ref) walks through a complete example.
    +`Diagnostics.explain_calls(model)` lists the selected models and objects;
    +`Diagnostics.explain_schedule(model)` shows their execution order.
     
     ## Explicit adapters
     
    -Variable renaming only changes the local field name. It must never silently
    -convert molar to mass units, ground-area to plant-area basis, rates to totals,
    -or one carbon convention to another. Put that operation in a small, named
    -model with a contract on each side. The executable adapter shipped with the
    -agent skill converts radiation per unit ground area to radiation per plant:
    +Two variables can have the same name and still mean different things. For
    +example, radiation per square metre of ground cannot be used directly where
    +a model expects radiation per plant. Renaming the variable does not convert it.
    +
    +An **adapter** is a small model that performs such a conversion. Its
    +`VariableContract` declarations record the units and physical meaning before
    +and after the conversion. This example multiplies radiation per unit ground
    +area by the ground area assigned to a plant:
     
     ```@example coupling-adapter
     using PlantSimEngine
    @@ -105,21 +134,17 @@ simulation = run!(adapted)
     )
     ```
     
    -The producer-to-adapter binding matches `GROUND_PAR_CONTRACT`; the
    -adapter-to-consumer binding matches `PLANT_PAR_CONTRACT`. The conversion
    -parameter, equation, units, and bases are now visible and testable. Document
    -the scientific domain and validity limits explicitly; the fixture does not
    -invent them. Use the same pattern for temporal or carbon-basis conversions.
    -
    -Temporal policies currently leave contracts unchanged. In particular,
    -`Integrate(reducer)` with a duration-aware reducer computes an integral but
    -does not make a rate contract
    -compatible with a total contract. For a contracted rate-to-amount conversion,
    -perform the calculation in the adapter kernel and declare the different
    -contracts on its input and output. Accumulate varying rates at their producer
    -cadence or supply a correctly averaged rate before multiplying by duration.
    -
    -After compilation, inspect `Diagnostics.explain_bindings(compiled)` for source identity
    -and carrier type, `Diagnostics.explain_calls(compiled)` for call-only targets, and
    -`Diagnostics.explain_schedule(compiled)` for root execution order. These rows are the
    -supported diagnostic surface; compiled fields are internal.
    +The adapter reads radiation per ground area and writes radiation per plant.
    +The models on either side therefore receive the units they expect. You can
    +inspect and test the area parameter and the conversion equation separately.
    +For your own adapter, also explain when that conversion is scientifically
    +appropriate.
    +
    +Use the same approach to convert a rate to an amount. A constant rate can be
    +multiplied by its duration. If it varies, add its contributions at each update
    +or use the correct mean rate over the interval.
    +
    +Time settings such as `Integrate(reducer)` can calculate an integral, but
    +they do not change the variable's declared units or meaning. To connect a
    +declared rate to a model expecting an amount, perform that calculation in an
    +adapter and declare the rate as its input and the amount as its output.
    diff --git a/docs/src/guides/data/forcing_observations.md b/docs/src/guides/data/forcing_observations.md
    index f44784497..630df2949 100644
    --- a/docs/src/guides/data/forcing_observations.md
    +++ b/docs/src/guides/data/forcing_observations.md
    @@ -7,7 +7,8 @@ is an energy flux in W m⁻² ground.
     
     ## One fixed observation
     
    -Supply a constant LAI in status and use only the light model:
    +Supply a constant LAI in `status`, where the canopy stores its values, and
    +use only the light model:
     
     ```@example observed_lai
     using PlantSimEngine, Dates, DataFrames
    @@ -22,14 +23,13 @@ fixed_simulation = run!(fixed_model; outputs=:all)
     final_state(fixed_simulation).aPPFD
     ```
     
    -Do not add a second model that also writes LAI: its output would replace the
    -supplied value. A status value is an initial or fixed input, not a rule that
    -overrides a running producer.
    +If you add a model that calculates LAI, it will replace this supplied value
    +when it runs. To keep the observed LAI fixed, use it without an LAI model.
     
     ## A sequence of observations
     
    -For time-varying observations, use a model that reads each observation and
    -publishes LAI. The small `ObservedLAI` definition below only copies the value;
    +For observations that change over time, use a model that reads each
    +observation and supplies it as LAI. The small `ObservedLAI` model below only copies the value;
     it performs no fitting, interpolation, or unit conversion. Include it from the
     downloadable source to run the example:
     
    @@ -56,7 +56,8 @@ filter(row -> row.variable == :LAI || row.variable == :aPPFD, rows)
     nothing # hide
     ```
     
    -`Beer` receives the published LAI through the usual same-object connection.
    +PlantSimEngine passes LAI from `ObservedLAI` to `Beer` automatically: one model
    +supplies it and the other needs it on the same canopy.
     To predict LAI instead, replace `ObservedLAI()` with `ToyLAIModel()` and supply
     the thermal time that model requires. A change of model can change the inputs
     you must provide; [compare alternatives](../../step_by_step/model_switching.md)
    @@ -70,12 +71,14 @@ The complete [source file](observed_lai.jl) is short:
     Main.DocsSources.section("docs/src/guides/data/observed_lai.jl", "struct ObservedLAI")
     ```
     
    -Before using real measurements, check their units, area basis, time stamps,
    -and missing values. The example has one observation per regular daily row.
    -For sparse measurements, choose and document an interpolation or holding rule
    -before supplying the forcing. This workflow prescribes a measured variable;
    -it is not a data-assimilation method that estimates uncertainty or updates
    -other state variables.
    +Before using real measurements, check their units, whether they refer to
    +leaf or ground area, their time stamps, and any missing values. This example
    +has one observation per day. If your measurements are less frequent, decide
    +how to fill the gaps: for example, interpolate between measurements or keep
    +the last measured value until the next one. Document that choice before
    +using the data. This approach supplies a measured input directly. It does
    +not perform data assimilation, which would use observations to estimate or
    +correct the model state and could account for measurement uncertainty.
     
     See [Collect and plot results](outputs_plotting.md) for displaying predictions
     alongside observations and [Parameter fitting](../../working_with_data/fitting.md)
    diff --git a/docs/src/guides/data/numerical_reliability.md b/docs/src/guides/data/numerical_reliability.md
    index 3874feb49..bfe9af580 100644
    --- a/docs/src/guides/data/numerical_reliability.md
    +++ b/docs/src/guides/data/numerical_reliability.md
    @@ -1,19 +1,24 @@
     # Numerical Reliability
     
    -Use exact assertions for deliberately exact integer/rational scenarios and
    -`isapprox` for floating-point scientific results. Splitting a computation
    -across objects may change reduction order without changing the model.
    -
    -PlantSimEngine preserves compatible numeric types through parameters, status,
    -carriers, meteorology, and streams. Avoid forced `Float64` conversion. For long
    -or ill-conditioned sums, use pairwise or compensated accumulation inside the
    -scientific model and test its error tolerance explicitly.
    -
    -## Choose the status representation
    -
    -A scenario may give its status values a different numeric representation when
    -it constructs a [`CompositeModel`](@ref). A type mapping is the shortest way to
    -convert every matching status value:
    +A small difference in a computed result can come from rounding. For example,
    +adding the same floating-point values in a different order can change the
    +last few digits. This can happen when you split a calculation across objects.
    +Use `isapprox` with a chosen tolerance to compare such results. Use exact
    +equality when the calculation is meant to be exact, such as an integer count.
    +
    +PlantSimEngine lets models use different numeric types for parameters, stored
    +values, weather data, and results. Avoid converting values to `Float64` unless
    +your calculation requires that precision. For sums that are sensitive to
    +rounding, consider pairwise summation (adding smaller groups first) or
    +compensated summation (tracking rounding losses). Implement the chosen method
    +in the scientific model and test its error against an appropriate tolerance.
    +
    +## Choose numeric types for stored values
    +
    +You can choose the numeric types used to store object values when you create
    +a [`CompositeModel`](@ref). For example, `Float32` uses less memory than
    +`Float64`, with less precision. A `type_promotion` rule converts all matching
    +status values:
     
     ```@example status_numeric_types
     using PlantSimEngine
    @@ -39,18 +44,18 @@ status = only(model_objects(model)).status
     )
     ```
     
    -Here the scalar `Float64` and the elements of the ordinary numeric array become
    -`Float32`. The integer is unchanged. Array conversion is element by element and
    -preserves the array shape. PlantSimEngine does not recursively inspect arbitrary
    -user structs or custom containers; map the complete container type or handle it
    -explicitly in `status_transform` when that is required.
    +Here `biomass` and each value in `cohort_masses` become `Float32`.
    +`cohort_count` remains an integer. Arrays keep their shape. PlantSimEngine
    +converts the elements of ordinary numeric arrays, but it does not look inside
    +every user-defined struct or custom container. To convert one of those, add a
    +rule for the whole container type or use `status_transform`, shown below.
     
    -Use `AbstractFloat => Float32` when all floating-point status values should be
    -converted, including types other than `Float64`. Avoid a broad
    -`Real => Float32` rule unless integer counts should also become floating-point
    -values. An exact source-type rule takes priority over an abstract rule. Rules
    -whose source types overlap without one being more specific are rejected during
    -model construction, so the result never depends on `Dict` iteration order.
    +Use `AbstractFloat => Float32` to convert all floating-point status values,
    +including types other than `Float64`. A broader `Real => Float32` rule also
    +converts integer counts, so use it only if that is intended. A rule for an
    +exact type takes priority over a rule for a broader type. If two rules match
    +and neither is more specific, PlantSimEngine rejects them when you create
    +the model. Their order in the dictionary does not decide which one wins.
     
     Use `status_transform` when the choice also depends on the variable name:
     
    @@ -66,43 +71,45 @@ model = CompositeModel(
     )
     ```
     
    -The transform receives `(variable, value)` and returns the value to store. It
    -runs before the general type mapping, so a value changed to another type by the
    -transform no longer matches the `Float64` rule above. It must be callable for
    -every status value that is materialized. If it throws, or if a mapped value
    -cannot be converted with `convert(Target, value)`, model construction stops
    -with the variable, original type, object when known, and initialization origin
    -in the error message.
    -
    -This policy applies only to values stored in object statuses: supplied values,
    -model input and output defaults, and status values of objects registered later
    -in the simulation. It does not convert model parameters or environment values.
    -The conversion happens when status storage is materialized or an object is
    -registered, not inside every model invocation.
    -
    -For a supplied `Status`, PlantSimEngine creates a new status reference only for
    -values that actually change. The supplied `Status` itself is not mutated, and
    -unchanged references are preserved where possible. Each `Object` must still own
    -its own `Status`; sharing one `Status` between objects is rejected. The stored
    -policy is also applied once to values introduced later by `register_object!` or
    -`add_organ!`.
    -
    -`Diagnostics.explain_initialization(model)` reports the declared, original,
    -transformed, and effective types, together with whether the precise transform
    -or the type mapping changed each materialized value.
    +`status_transform` receives the variable name and its value, then returns the
    +value to store. It runs before `type_promotion`, so a value it changes to
    +`MyNumericType` no longer matches the `Float64` rule above. The function must
    +handle every status value it receives, including values it should leave alone.
    +If the function fails, or a conversion with `convert(Target, value)` fails,
    +model construction stops. The error identifies the variable, its original
    +type, where its initial value came from, and the object when known.
    +
    +These settings apply to values stored on objects: supplied initial values,
    +model input and output defaults, and values on objects added later during
    +the simulation. Model parameters and environment values keep their types.
    +Conversion happens when status storage is created or a new object is added;
    +it is not repeated every time a model runs.
    +
    +If you supply a `Status`, PlantSimEngine leaves that original object unchanged.
    +It creates new storage for converted values and reuses existing references
    +where possible for unchanged values. Each `Object` must have its own `Status`;
    +sharing one `Status` between objects is rejected. The same conversion rules
    +apply once to values added later through `register_object!` or `add_organ!`.
    +
    +`Diagnostics.explain_initialization(model)` shows the type declared by the
    +model, the type of the supplied value, the type after `status_transform`, and
    +the final stored type. It also shows which conversion changed each value.
     
     !!! note
    -    Model kernels still need to support the effective types. Keep status
    -    computations generic and avoid constructing intermediate values as
    -    `Float64` unless that precision is part of the scientific contract.
    +    Your model's `run!` function must support the types you choose. Let its
    +    calculations work with the input types, and avoid forcing intermediate
    +    values to `Float64` unless the equations require that precision.
     
     ## Propagate uncertainty with particles
     
     [MonteCarloMeasurements.jl](https://github.com/baggepinnen/MonteCarloMeasurements.jl)
    -is an optional package and is not a PlantSimEngine dependency. A precise
    -`status_transform` can initialize selected inputs and outputs as particles,
    -while the general mapping converts the remaining floating-point status values.
    -For example:
    +is an optional package that you install separately. It represents an uncertain
    +number as a collection of possible values, called **particles**. Calculations
    +on those particles show how uncertainty in an input affects an output.
    +
    +Use `status_transform` to give selected inputs and outputs this representation.
    +The `type_promotion` rule can still convert the other floating-point values.
    +This small example squares two possible values of `x`, 0.9 and 1.1:
     
     ```julia
     using PlantSimEngine
    @@ -144,14 +151,14 @@ uncertain_result = final_state(simulation).y
     (mean=pmean(uncertain_result), standard_deviation=pstd(uncertain_result))
     ```
     
    -Both `x` and the initial storage for `y` are particles, so assigning the
    -particle result preserves its uncertainty. The unrelated `ordinary` status is
    -still converted to `Float32`. Model parameters would need their own generic
    -types if they also have to carry uncertainty.
    +Both `x` and the initial value of `y` use `Particles`, so `y` can store the
    +uncertain result. The separate `ordinary` value becomes `Float32`. If you
    +also want uncertain model parameters, define the model so its parameter
    +fields accept those types.
     
     ## Test the expected tolerance
     
    -Changing the floating-point representation can change rounding and reduction
    -order. Compare scientific results with an explicit tolerance appropriate for
    -the chosen representation. Test exact values only when exactness is an intended
    -part of the model contract.
    +Changing numeric types can change rounding and the order in which values are
    +added. Choose a comparison tolerance that makes sense for both the precision
    +and the scientific calculation. Test exact equality only when the model is
    +intended to produce an exact result.
    diff --git a/docs/src/guides/data/outputs_plotting.md b/docs/src/guides/data/outputs_plotting.md
    index 93f5b90d6..3bceb1135 100644
    --- a/docs/src/guides/data/outputs_plotting.md
    +++ b/docs/src/guides/data/outputs_plotting.md
    @@ -30,11 +30,14 @@ light = rows[rows.variable .== :aPPFD, :]
     first(light, 3)
     ```
     
    -Each row identifies the model application, object, variable, time and value.
    +`sink=DataFrame` asks for a DataFrame; this is already the default if you
    +leave out `sink`. Each row records one variable for one object at one time,
    +and identifies which model application produced it.
     `aPPFD` is absorbed PAR in μmol m⁻² of ground s⁻¹. The `time` column uses
    -model base-step coordinates, with the first record at 1. In this fixed hourly
    -example, `6 + (time - 1)` gives the hour of day. Use the actual forcing dates
    -and durations when your data use another time grid.
    +simulation steps rather than clock hours, with the first record at 1. In
    +this example each step lasts one hour and the first record is at 06:00, so
    +`6 + (time - 1)` gives the hour of day. Use your weather data's actual dates
    +and durations when they follow a different timetable.
     
     ```@example collect-output
     light.hour_of_day = 6 .+ (light.time .- 1)
    @@ -52,9 +55,10 @@ figure_one
     ```
     
     With fixed leaf area, absorption follows the supplied radiation. This curve
    -shows a mean flux for each hourly record, not a cumulative
    -amount. To obtain an amount over time, include the represented duration in
    -the calculation; see [different model cadences](../../journeys/users/cadences.md).
    +shows a mean flux for each hourly record. To calculate the total absorbed
    +over an interval, multiply each flux by its duration in seconds and add the
    +amounts. See [different model cadences](../../journeys/users/cadences.md) for
    +models that run at different intervals.
     
     ## Keep two canopies separate
     
    @@ -98,7 +102,7 @@ join different objects.
     ## Retain only the outputs you need
     
     `outputs=:all` is convenient for these small examples. Larger simulations
    -can retain selected variables with `OutputRequest`. Here, the new run keeps
    +can save selected variables with `OutputRequest`. Here, the new run keeps
     only the light result requested from the two canopies:
     
     ```@example collect-output
    @@ -120,20 +124,21 @@ to extend an existing simulation instead. Runs default to `outputs=:none`;
     `final_state(simulation)` remains available when you only need the latest
     values.
     
    -Requested output history and internal dependency history have different
    -purposes. PlantSimEngine may retain an internal stream because another model
    -needs it, even if you did not request it for analysis. Inspect
    -`Diagnostics.explain_output_retention(simulation)` when memory use matters.
    +PlantSimEngine may also keep past values because another model needs them,
    +even if you did not request those values for analysis. When memory use
    +matters, `Diagnostics.explain_output_retention(simulation)` explains why
    +each time series is being stored.
     
     ## Reading the result tables
     
    -Raw rows have the columns `timestep`, `time`, `application_id`, `object_id`,
    -`variable` and `value`. Requested or resampled rows also identify `scale` and
    -`process`. A temporal request can return `missing` when the available history
    -cannot supply the requested value. Check the model schedule and the start of
    -the run before interpreting these as gaps in observations.
    -
    -Published values, including arrays, are snapshots: subsequent state changes
    -do not rewrite earlier rows. Removed organs retain their historical outputs.
    -Values also retain compatible numerical types, including unit-bearing values
    -when the model produces them.
    +The basic result table has the columns `timestep`, `time`, `application_id`,
    +`object_id`, `variable` and `value`. Tables produced by an `OutputRequest`
    +also identify `scale` and `process`. Requests that combine values over time
    +or sample them at new times can return `missing` if there is not enough
    +saved history. Check when the model ran and when the simulation started
    +before interpreting a missing result as a gap in the weather observations.
    +
    +Saved values, including arrays, record the result at that time. Later changes
    +do not rewrite earlier rows, and removing an organ does not delete its past
    +results. Compatible numerical types are preserved, including values with
    +attached physical units when your model uses them.
    diff --git a/docs/src/guides/extensions/environment_backends.md b/docs/src/guides/extensions/environment_backends.md
    index 4c157dc09..8859197a0 100644
    --- a/docs/src/guides/extensions/environment_backends.md
    +++ b/docs/src/guides/extensions/environment_backends.md
    @@ -1,94 +1,104 @@
     # Environment Backend Extensions
     
    -This page is for package authors who connect a meteorology, canopy-layer,
    -voxel, grid, or other spatial provider to PlantSimEngine. Simulation users
    -normally configure an existing backend with `Environment(...)` and do not need
    -this protocol.
    -
    -## The boundary
    -
    -An extension backend subtypes
    -`PlantSimEngine.EnvironmentAPI.AbstractEnvironmentBackend` and extends
    -functions in `PlantSimEngine.EnvironmentAPI`. Keep the concrete backend and
    -handle types in the extension package. They do not belong in PlantSimEngine's
    -root namespace.
    +An **environment backend** connects a source of weather or spatial data to
    +PlantSimEngine. It may provide weather records, canopy layers, a grid, or
    +voxels (three-dimensional cells). This page explains how to write one. To use
    +an existing backend in a simulation, start with
    +[Understand Environments](../../journeys/users/environments.md).
    +
    +## Define a backend
    +
    +Define your backend as a subtype of
    +`PlantSimEngine.EnvironmentAPI.AbstractEnvironmentBackend` and extend
    +functions in `PlantSimEngine.EnvironmentAPI`. Define its types in your
    +extension package, including the **handle** type. A handle stores the location
    +or data needed to read the environment for one model on one object. For
    +example, it might contain the canopy layer used by a leaf. PlantSimEngine
    +keeps this handle so it does not need to find that layer on every step.
     
     The required methods are:
     
     - `base_step_seconds(backend)`: duration of one base step;
     - `get_nsteps(backend)`: available number of steps;
    -- `bind_environment(backend, object, context, config)`: compile one opaque
    -  handle for an application/object pair;
    -- `sample(backend, handle, variable, time)`: read committed state.
    -
    -Implement `environment_variables(backend)` when the variable names can be
    -enumerated cheaply. PlantSimEngine then validates model environment inputs
    -while compiling.
    -
    -Mutable or structurally indexed backends add only the capabilities they need:
    -
    -- `sample(backend, handle, trial_state, variable, time)` for transient trial
    -  states supplied to `run_call!`;
    -- `commit_environment!(backend, handle, accepted_state, time)` for accepted
    -  state;
    -- `update_index!(backend, changed_entities, removed_object_ids)` when topology
    -  or geometry changes require a spatial-index refresh. Initial compilation
    -  supplies all entities; later refreshes supply only the structural delta.
    +- `bind_environment(backend, object, context, config)`: create the handle for
    +  one model application on one object;
    +- `sample(backend, handle, variable, time)`: read a value from the stored,
    +  accepted environment state.
    +
    +Implement `environment_variables(backend)` if you can list the available
    +variable names without an expensive calculation. PlantSimEngine can then
    +check that the required names exist when it prepares the simulation.
    +
    +Add these methods if your backend needs to support trials, saved changes,
    +or changes to the plant structure:
    +
    +- `sample(backend, handle, trial_state, variable, time)`: read from a trial
    +  environment supplied to `run_call!`, without changing the stored environment;
    +- `commit_environment!(backend, handle, accepted_state, time)`: save accepted
    +  environment values;
    +- `update_index!(backend, changed_entities, removed_object_ids)`: update how
    +  objects are found in space after a structure or geometry change. The first
    +  call supplies all objects; later calls supply only the changes.
     
     [`ToySpatialEnvironment`](@ref) is the small, tested implementation used by
     the user journeys.
     
    -## Compile routing into the handle
    +## Find each object's data once
     
    -`bind_environment` receives the target `Object`, an
    -`EnvironmentAPI.EnvironmentContext`, and the payload configured with
    -`Environment(...)`. Resolve geometry, layer, voxel, provider, and commit sink
    -there. Return a concrete handle containing everything later sampling and
    -committing need.
    +`bind_environment` receives the `Object` that will use the environment, an
    +`EnvironmentAPI.EnvironmentContext`, and the settings from `Environment(...)`.
    +Use them to find where this object reads its values and, if needed, where it
    +saves changes. For example, find the leaf's voxel or canopy layer here. Return
    +a concrete handle containing everything needed for later reads and writes.
     
    -PlantSimEngine caches that handle. The hot `sample` methods receive it directly,
    -so they should not search the object registry or repeat spatial routing.
    +PlantSimEngine stores that handle and passes it directly to `sample` each time.
    +To keep repeated reads fast, avoid searching all objects or recalculating
    +their locations in `sample`.
     `EnvironmentContext` contains application id, object id, scale, and process;
    -runtime status is deliberately absent.
    +it does not include the object's current status values.
     
    -A controller that reads one provider and writes another can encode both routes
    -in its handle. For example, the scenario may configure
    +A controller that reads one data source and writes to another can store both
    +locations in its handle. For example, the scenario may configure
     `Environment(provider=:forcing, sink=:canopy)`, while the backend decides what
     those names mean.
     
    -## Trial and commit semantics
    +## Try values, then save accepted changes
     
    -Transient sampling and committing are separate backend operations:
    +Trying a value and saving it are separate operations:
     
    -1. a controller passes its typed trial state to
    +1. A controller passes a trial environment to
        `run_call!(context, name; environment=trial_state, publish=false)`;
    -2. PlantSimEngine invokes the transient `sample` overload through each target's
    -   already-compiled handle;
    -3. the controller accepts a state and calls
    +2. PlantSimEngine calls the trial version of `sample` using each target's
    +   stored handle, so each target reads the trial at its own location.
    +3. The controller accepts a solution and calls
        `commit_environment!(context, accepted_state)`;
    -4. PlantSimEngine validates the caller's `environment_outputs_` declaration,
    -   then invokes the backend commit through the caller's handle;
    -5. the controller publishes accepted called-model outputs explicitly with
    -   `publish=true`.
    +4. PlantSimEngine checks that `environment_outputs_` allows the controller to
    +   change those variables, then calls the backend's commit method using the
    +   controller's handle.
    +5. The controller runs the called models with `publish=true` to record their
    +   accepted outputs.
     
    -The model-facing `commit_environment!(context, state)` is root API. The
    -backend-facing method belongs to `PlantSimEngine.EnvironmentAPI`; extension
    -packages should qualify it when adding methods.
    +Model code calls `PlantSimEngine.commit_environment!(context, state)`. Your
    +backend extends the separate function
    +`PlantSimEngine.EnvironmentAPI.commit_environment!`; use that full name when
    +defining its method in an extension package.
     
    -## Refresh and diagnostics
    +## Update locations and check the results
     
    -PlantSimEngine calls `update_index!` once per distinct spatial backend before
    -binding or rebinding affected targets. Geometry changes invalidate the affected
    -handles; structural changes can also change the set of bound targets.
    +Before creating or updating affected handles, PlantSimEngine calls
    +`update_index!` once for each spatial backend. Moving an object or changing
    +its geometry requires an updated handle. Adding, removing, or reparenting
    +objects can also change which objects a model runs on.
     
    -Use these public checks instead of reading compiler fields:
    +Use these functions to inspect your backend:
     
    -- `validate_environment_inputs(model)` checks declared variables;
    +- `validate_environment_inputs(model)` checks that required variables exist;
     - `Diagnostics.explain_environment_bindings(model)` reports each
       application/object handle and geometry source;
     - `Diagnostics.explain_environment(simulation)` reports the active backend,
       variables, step count, and base-step duration.
     
    -Backend tests should cover at least two objects with distinct handles,
    -committed and transient sampling, rejected undeclared commits, and handle
    -refresh after movement or geometry changes.
    +Test at least two objects that need different local values. Check that your
    +backend reads both stored and trial values correctly, rejects writes to
    +undeclared variables, and updates handles when objects move or their geometry
    +changes.
    diff --git a/docs/src/guides/graph_visualizer_editor.md b/docs/src/guides/graph_visualizer_editor.md
    index 3fc4f6bbc..b36b04800 100644
    --- a/docs/src/guides/graph_visualizer_editor.md
    +++ b/docs/src/guides/graph_visualizer_editor.md
    @@ -4,16 +4,17 @@ CurrentModule = PlantSimEngine
     
     # Visualize And Edit A CompositeModel
     
    -The CompositeModel graph shows how model applications, objects, and compiled value
    -bindings fit together before a simulation runs. Use the static visualizer when
    -you want an inspectable HTML artifact, and the interactive editor when you want
    -browser actions to update a Julia [`CompositeModel`](@ref).
    +The graph shows which models run on which objects and how values pass between
    +them. For example, you can follow LAI from the model that calculates it to the
    +light model that uses it. Use the viewer to explore and share a graph, or the
    +editor to change your [`CompositeModel`](@ref) from the browser.
     
     ## A Small CompositeModel
     
    -This example applies three toy models to one plant object. The compiler infers
    -the same-object `TT_cu` and `LAI` bindings from the declared input and output
    -names.
    +This example applies three teaching models to one plant object. The
    +thermal-time model supplies `TT_cu` to the LAI model, which supplies `LAI` to
    +the light model. PlantSimEngine connects them using their matching input and
    +output names.
     
     ```@example graph_viewer
     using PlantSimEngine
    @@ -46,13 +47,17 @@ embeds it below.
     
     ```
     
    -The default **Applications** projection groups all concrete executions of one
    -application into one card. Use **Objects** to inspect topology and **Executions**
    -to inspect concrete `(application, object)` pairs. Search, diagnostics,
    -initialization, selectors, parameters, and resolved edge details remain
    -available in the static viewer. The topology projection includes model,
    -template, and instance containers; selecting an instance or object subtree scopes the
    -application and execution projections until the filter is cleared.
    +Use the three views to explore the simulation:
    +
    +- **Applications** shows one card for each configured use of a model, even
    +  when it runs on many leaves or plants.
    +- **Objects** shows the plant structure and groups of objects.
    +- **Executions** shows each model application on each object separately.
    +
    +Select a card or connection to inspect its parameters, inputs, starting
    +values, and any problems found. Selecting a plant or a branch of the structure
    +filters the other views to those objects. Clear the filter to see the whole
    +simulation again. These controls also work in a saved, read-only viewer.
     
     ## Write A Static Viewer
     
    @@ -63,7 +68,7 @@ server:
     path = GraphEditor.write_model_graph_view("model-graph.html", model)
     ```
     
    -The output bundles the graph payload, JavaScript, and CSS in one HTML file. It
    +The output includes the graph data and viewer code in one HTML file. It
     can be opened locally or embedded in Documenter documentation. A downstream
     package can generate the file from `docs/make.jl` and place it under
     `docs/src/assets`:
    @@ -77,13 +82,13 @@ GraphEditor.write_model_graph_view(
     ```
     
     Then embed it from a Markdown page with an HTML `iframe`. The graph is
    -read-only, but its projection controls, search, inspector, and diagnostics are
    +read-only, but its views, search, details panel, and error reports are
     interactive in the browser.
     
     ## Start The Editor
     
    -The mutable editor is an optional package extension activated by HTTP.jl. Add
    -HTTP once to the environment that will launch the editor:
    +The editor needs the optional HTTP.jl package to communicate with Julia. Add
    +HTTP once to the project where you will use the editor:
     
     ```julia
     using Pkg
    @@ -100,9 +105,9 @@ session = GraphEditor.edit_graph(model)
     ```
     
     The default browser opens automatically. The returned session also prints its
    -URL and shutdown command. Julia remains authoritative: browser edits are sent
    -as semantic commands, applied transactionally to a candidate CompositeModel, compiled,
    -and returned as a fresh graph state.
    +URL and shutdown command. When you make an edit, the browser sends it to
    +Julia. Julia checks the proposed change before accepting it and sends the
    +updated graph back to the browser.
     
     Inspect the current result or stop the server with:
     
    @@ -116,10 +121,10 @@ Call `GraphEditor.edit_graph()` without a CompositeModel to start from an empty
     
     ## Templates And Several Plants
     
    -A template is a reusable set of already coupled applications. Mounting the same
    -template twice creates two instance-local application sets. Unqualified selectors
    -remain inside their own plant, so a model in `plant_a` does not accidentally read
    -values from `plant_b`.
    +A template is a reusable set of connected models. Applying the same template
    +to two plants gives each plant its own set of calculations. By default, the
    +models look for inputs within their own plant, so a model in `plant_a` does
    +not accidentally read values from `plant_b`.
     
     ```julia
     using Dates
    @@ -156,15 +161,16 @@ session = GraphEditor.edit_graph(
     )
     ```
     
    -The **Add instance** wizard can mount a catalog template on an existing unclaimed
    -root and its descendants, or create a minimal root and mount the template in one
    -transaction. Preview the claimed subtree and resolved application targets before
    -committing. Unmounting removes the applications but keeps the object subtree.
    +Use **Add instance** to apply a template to an existing root object and its
    +descendants, provided they are not already part of another instance. You can
    +also create a new root and apply the template in one operation. Preview the
    +objects and model applications before accepting. Removing the template from
    +an instance removes its applications but keeps the objects.
     
     Catalog templates are presets. The first edit to a mounted preset creates a
    -model-local replacement shared by all instances that currently use it. The original
    +replacement within this simulation, shared by all instances that currently use it. The original
     preset remains available when adding another instance. Template application names
    -are fixed because they are part of the template contract.
    +cannot be changed; they identify the applications within the template.
     
     ## Overrides
     
    @@ -182,13 +188,14 @@ plant_b = ObjectInstance(
     )
     ```
     
    -The editor offers the same operation at instance or object scope. Julia checks that
    -the replacement implements the same process and variable contract.
    +The editor can replace a model for a whole plant instance or for an individual
    +object. Julia checks that the replacement describes the same process and has
    +compatible inputs, outputs, and other requirements.
     
     ## Environment Catalogs And Routing
     
    -Environment values remain in Julia. Give the editor a named catalog rather than
    -serializing backends to the browser:
    +Give the editor names for the environment sources it can use. The browser
    +shows these names; the weather data and other environment objects stay in Julia:
     
     ```julia
     session = GraphEditor.edit_graph(
    @@ -201,14 +208,15 @@ session = GraphEditor.edit_graph(
     )
     ```
     
    -The scene environment can be replaced from this catalog. Each application can use
    -the scene backend or a catalog backend and can configure `provider`, model-facing
    -input-to-source mappings, `sink`, and backend-specific typed options. The editor
    -shows `environment_hint(model)` and the effective compiled bindings read-only, then
    -asks Julia to validate the candidate routing before it is committed.
    +You can choose a new environment for the whole simulation or for one model
    +application. The settings include the source to read (`provider`), any
    +variable-name translations, and the destination for accepted updates (`sink`).
    +Other options depend on the environment source. The editor shows the model's
    +defaults from `environment_hint(model)` and its current environment
    +connections. Julia checks a proposed change before accepting it.
     
    -Application cadence and temporal windows use `Dates.Second`, `Dates.Minute`,
    -`Dates.Hour`, or `Dates.Day`. Whole-scene targeting is also explicit:
    +Model time steps and time windows use `Dates.Second`, `Dates.Minute`,
    +`Dates.Hour`, or `Dates.Day`. To select objects across the whole scene,
     `SceneScope()` must be selected deliberately. Omitting the scope keeps a template
     application local to each mounted instance.
     
    @@ -216,17 +224,17 @@ application local to each mounted instance.
     
     The editor supports:
     
    -- model objects, metadata, status initialization, and parent topology;
    -- model applications, constructor parameters, target selectors, and cadence;
    -- explicit value bindings, hard calls, output routing, and update ordering;
    -- template catalogs, transactional instance mounting, shared template edits, and overrides;
    -- named scene and application environment backends, providers, sources, and sinks;
    -- dependency cycles through an explicit `PreviousTimeStep` break action;
    -- undo, redo, temporary recovery autosaves, and readable Julia CompositeModel scripts.
    +- objects, their labels and starting values, and their place in the structure;
    +- models, their parameters, which objects they run on, and their time steps;
    +- input connections, calls between models, saved outputs, and update order;
    +- templates, plant instances, and model replacements;
    +- environment sources and destinations for the scene or individual models;
    +- feedback loops, by choosing an input that should read the previous step;
    +- undo, redo, recovery files, and saving the simulation setup as Julia code.
     
    -Application target and binding dialogs can ask Julia to preview the concrete
    -objects selected by a declaration. This is important for `Many`, relative
    -scopes such as `SelfPlant`, and composite models containing several plant instances.
    +When selecting objects or connecting inputs, preview the matching objects
    +before applying the change. This helps check selections such as `Many` and
    +`SelfPlant`, especially when the simulation contains several plants.
     
     ## Models From Other Packages
     
    @@ -242,34 +250,36 @@ using HTTP
     session = GraphEditor.edit_graph(model)
     ```
     
    -The `+` buttons next to ports use exact declared variable names only. For an
    +The `+` buttons next to inputs and outputs search by exact variable name. For an
     input named `LAI`, the editor lists loaded models whose `outputs_` contains
     `LAI`. For an output named `LAI`, it lists models whose `inputs_` contains
     `LAI`, as well as compatible applications already present in the CompositeModel. This is
    -a composition aid, not a scientific compatibility inference.
    +a way to find possible connections. You still need to check the units and
    +physical meaning of the values.
     
     When the CompositeModel is saved as Julia code, required package imports are emitted for
     the model types used by the CompositeModel.
     
     ## Invalid And Cyclic Composite Models
     
    -Simulation compilation remains strict, but graph compilation preserves as much
    -structure as possible and attaches diagnostics. This lets the editor display
    -incomplete selectors, missing initialization, ambiguous writers, and cycles.
    +The editor can display an incomplete simulation and show its problems, even
    +when it cannot run yet. These include missing starting values, selections
    +that do not identify the intended objects, several models trying to set the
    +same output, and circular dependencies.
     
    -Cycle edges are shown in red. The break workflow asks which consumer input
    -should read its previous accepted timestep value and asks for initial values
    -when the affected target objects do not already provide one. The action changes
    -the application input policy for every target selected by that application.
    +Connections in a circular dependency are shown in red. To break a loop, choose
    +which input should read the previous step's value. The editor also asks for
    +starting values if the objects do not have them. This change applies to every
    +object selected by that model application.
     
     !!! warning
    -    `PreviousTimeStep` changes model semantics. It disconnects the selected
    -    input from current-step producers during one run step. Use it only when that
    -    lag and its initialization value are scientifically intentional.
    +    `PreviousTimeStep` changes the calculation: the model reads an older value
    +    instead of the value calculated at the current step. Use it only when this
    +    delay and the starting value make sense for your scientific model.
     
     ## Saving And Recovery
     
    -The **Save** action writes readable Julia code whose final binding is
    +The **Save** action writes readable Julia code ending with
     `model = CompositeModel(...)`. Once a path is selected, every successful edit rewrites
     that file. The editor also keeps a temporary recovery file and lists recent
     CompositeModel scripts in **Open**.
    @@ -283,12 +293,12 @@ source = Authoring.scenario_source(
     )
     ```
     
    -This is editable scenario code. It is distinct from
    -`Authoring.compiled_model_source(model)`, which exposes the resolved execution
    -plan for inspection and is not a second scheduler.
    +Use this code to edit or share the simulation setup.
    +`Authoring.compiled_model_source(model)` provides a more detailed view of how
    +PlantSimEngine will run the calculations.
     
    -Generated code is best effort for arbitrary Julia values and external runtime
    -resources. Templates and instances are written inline. Named environment values are
    +Some Julia values and external resources cannot be recreated automatically
    +from a saved script. Templates and instances are written into the script. Named environment values are
     referenced through a `scenario_environments` named tuple, and the generated header
     lists the keys that must be supplied when reopening the file:
     
    diff --git a/docs/src/guides/modelers/port_existing_model.md b/docs/src/guides/modelers/port_existing_model.md
    index bc5c2ee49..dc51bb18b 100644
    --- a/docs/src/guides/modelers/port_existing_model.md
    +++ b/docs/src/guides/modelers/port_existing_model.md
    @@ -1,9 +1,9 @@
     # Port an existing model
     
     Start from a calculation you already understand and test it before moving it.
    -Separate fixed parameters, changing object state, environmental inputs, and
    -the values it produces. The goal is to preserve the equation while making
    -those roles explicit.
    +Identify its fixed parameters, the values that change during a simulation,
    +the environmental data it needs, and the results it calculates. Keep the
    +equation unchanged while giving each quantity a clear place.
     
     ## Begin with an existing calculation
     
    @@ -20,16 +20,17 @@ expected = old_lai_step(1.0f0, 10.0f0, 0.02f0)
     expected
     ```
     
    -Before porting a real model, record the units, interval, assumptions, and
    -expected results from its original implementation. If it returns a rate,
    -identify where time integration occurs.
    +Before porting a real model, record its units, time interval, assumptions,
    +and expected results. If it calculates a rate, check how that rate becomes
    +an amount: for example, where a daily growth rate is multiplied by the
    +number of days.
     
     ## Map each quantity to its role
     
     | Quantity | Role in this example | Place in PlantSimEngine |
     |---|---|---|
     | Response coefficient | Fixed parameter for one update | Model field |
    -| Current LAI | Changing input state, leaf area per ground area | `status.lai` |
    +| Current LAI | Current leaf area per ground area | `status.lai` |
     | Air temperature | Environmental input, degrees Celsius | `environment.T` |
     | New LAI | Result of this update | `status.lai_next` |
     
    @@ -58,8 +59,9 @@ should reuse its package's existing process where appropriate; see
     
     ## Preserve the physical meanings
     
    -The input and output LAI use the same area basis. Temperature has its own
    -contract:
    +Both LAI values describe leaf area per unit ground area. Temperature is in
    +degrees Celsius. We record these meanings with `VariableContract`, which
    +also describes whether each variable is a current value, a rate, or a total:
     
     ```@example port-existing-model
     const DOCS_LAI_CONTRACT = VariableContract(
    @@ -77,8 +79,14 @@ PlantSimEngine.variable_contracts_(::DocsLAIGrowth) = (
     )
     ```
     
    -Contracts describe meaning at connections. They do not silently convert a
    -quantity or establish its scientific validity.
    +Here `temporal=:instantaneous` and `aggregation=:state` describe current
    +values. `extent=:intensive` means that adding values from two objects does
    +not give their combined value: two air temperatures, for example, cannot
    +be added to obtain the temperature of both objects together.
    +
    +These descriptions help check that connected models interpret a variable in
    +the same way. They do not convert values or prove that the equation is
    +scientifically valid.
     
     ## Keep the calculation readable
     
    @@ -128,11 +136,13 @@ explicitly how that value becomes the next input; see
     
     ## Extend only what your model needs
     
    -Keep time-varying memory in each object's status and fixed parameters in the
    -model. Preserve compatible numerical types rather than converting everything
    -to `Float64`. Assign instantaneous outputs on every execution path,
    -including early returns.
    +Keep values that change over time in each object's status and fixed
    +parameters in the model. Preserve the numerical types your model supports
    +rather than converting every value to `Float64`. If an output describes the
    +current step, assign it every time the model runs, including when a condition
    +ends the calculation early. Otherwise, an old result may be mistaken for
    +today's result.
     
    -If users need to replace or schedule a subprocess independently, expose it
    -as a separate model. Use [Coupling models](@ref) for its connections and
    +If users need to replace one part of the calculation or run it at a different
    +frequency, make that part a separate model. Use [Coupling models](@ref) for its connections and
     [Model repository layout and tests](@ref) for broader validation.
    diff --git a/docs/src/guides/modelers/repository_and_tests.md b/docs/src/guides/modelers/repository_and_tests.md
    index 5587d9b53..e52b3ec6d 100644
    --- a/docs/src/guides/modelers/repository_and_tests.md
    +++ b/docs/src/guides/modelers/repository_and_tests.md
    @@ -29,13 +29,15 @@ docs/src/models/
     └── growth.md
     ```
     
    -Use `process.jl` only when this package owns the process declaration.
    -Otherwise import the abstract process type from its owner. Keep one readable
    -file per hypothesis and make includes and exports explicit in `MyModels.jl`.
    +Use `process.jl` only when this package declares a new process. If another
    +package already declares it, import that package's abstract process type.
    +Keep one readable file per hypothesis. List the files to include and the
    +names users can import in `MyModels.jl`.
     
     A process page should compare the alternatives: equations, assumptions,
    -required data, units, parameters, validity domain, references, and validation
    -status. Label teaching models and unfinished experiments clearly.
    +required data, units, parameters, references, and the conditions under which
    +each model has been tested. Label teaching models and unfinished experiments
    +clearly.
     
     ## Test from the equation outward
     
    @@ -45,10 +47,10 @@ Each level answers a different question:
     |---|---|
     | Direct equation test | Does the implementation reproduce a known calculation? |
     | Declaration check | Are required inputs, outputs, and physical meanings explicit? |
    -| Small composition | Can the model obtain its inputs and run on the intended object? |
    +| Small simulation | Can the model obtain its inputs and run on the intended object? |
     | Coupled scenario | Does it interact correctly with the other models used in this study? |
     
    -Start with the canonical biomass example from
    +Start with the biomass example from
     [Implement a basic model](@ref):
     
     ```@example repository-tests
    @@ -72,18 +74,19 @@ scenario = CompositeModel(
     @test final_state(run!(scenario)).biomass_increment == 15.0f0
     ```
     
    -These small checks isolate the equation from input routing and scheduling.
    +These small checks test the equation first, then test how the simulation
    +supplies its inputs and runs it.
     For a scientific model, add edge cases and a trusted reference calculation
     or dataset. Agreement with the reference needs a numerical tolerance
     appropriate to the calculation.
     
     ## Add checks for the features you use
     
    -A model with cross-object inputs needs tests that the intended objects supply
    -them. A model with several cadences needs checks at their update boundaries.
    -A growing scenario needs checks before and after organs are added or removed.
    -An iterative controller needs tests of both rejected trials and accepted
    -publication.
    +If a model reads values from other objects, check that it uses the intended
    +objects. If coupled models run at different frequencies, check what happens
    +when each one updates. For a growing plant, check results before and after
    +organs are added or removed. For a model that tries several solutions, test
    +both rejected trials and the results it finally accepts.
     
     Use `Diagnostics` to inspect the relevant connections and schedules, then
     assert the scientific relationship you intend to preserve. For example,
    @@ -100,13 +103,14 @@ from equation checks. See [Numerical Reliability](@ref) and the
     Before sharing a model, check that:
     
     - it belongs to the intended process;
    -- its inputs, outputs, units, bases, and timing are documented;
    +- its inputs, outputs, units, and timing are documented, including whether
    +  each value is per organ, per plant, or per unit area;
     - its equation and fixed parameters are easy to find;
     - each object's changing state stays separate;
     - direct tests and the relevant coupled tests pass;
     - scientific validation and remaining uncertainty are stated.
     
    -When a scenario becomes long, group its application definitions into named
    -functions by domain, such as leaf, plant, and soil processes. Keep those
    -functions as ordinary collections of `ModelSpec` values so the scenario
    -remains inspectable.
    +When a simulation setup becomes long, group the `ModelSpec` definitions into
    +functions for leaf, plant, and soil processes, for example. Have those
    +functions return the `ModelSpec` values so readers can still see which
    +models are used and how they are connected.
    diff --git a/docs/src/guides/modelers/stateful_models.md b/docs/src/guides/modelers/stateful_models.md
    index 7058b72d2..dc33988ba 100644
    --- a/docs/src/guides/modelers/stateful_models.md
    +++ b/docs/src/guides/modelers/stateful_models.md
    @@ -20,8 +20,9 @@ daily observations:
     
     **mean = (today + previous observation + older observation) / 3**
     
    -All three observations are dimensionless fractions. This is a teaching
    -example of memory ownership, not a model of plant water stress.
    +All three observations are dimensionless fractions. This example shows how
    +each soil object keeps its own past observations. It does not model plant
    +water stress.
     
     ```@example object_memory
     using Dates, Test, PlantSimEngine
    @@ -43,7 +44,9 @@ PlantSimEngine.environment_outputs_(::DocsThreeDayMean) = NamedTuple()
     
     The two memory fields are outputs because this model updates them. Their
     initial values must represent observations before the first simulated day.
    -Each object can supply different initial values.
    +Each object can supply different initial values. The `VariableContract`
    +descriptions below record that the observations are soil-water fractions,
    +while the result is a mean of three daily samples.
     
     ```@example object_memory
     const FRACTION_SAMPLE = VariableContract(
    @@ -92,8 +95,8 @@ sample.mean_fraction
     
     ## Keep two objects' histories independent
     
    -Here the observations stay constant during the example. A real scenario
    -would supply the daily values from forcing data or another model.
    +Here the observations stay constant during the example. A real simulation
    +would read each day's value from a dataset or another model.
     
     ```@example object_memory
     dry = Object(
    @@ -131,20 +134,22 @@ day_two = (
     
     Both objects use the same model. Their separate status fields keep the dry
     soil's earlier observations from affecting the wet soil's mean. If an
    -algorithm needs a mutable history array, each object must own its own array
    -as well; do not put one shared buffer in the model.
    +algorithm stores earlier values in an array, give each object its own array
    +as well. An array stored in the shared model would mix the objects' histories.
     
     ## Delays, repeated calls, and output history
     
    -Use `PreviousTimeStep` when an input should read a producer's previous
    -accepted timestep. The [model execution reference](../../model_execution.md)
    -shows that declaration. [Control Advanced Execution](@ref) explains intentional
    -multiple writers with `Updates`.
    -
    -A call with `publish=false` suppresses publication; it does not automatically
    -restore every state field the scientific algorithm mutates. An iterative
    -controller must manage trial state and acceptance deliberately. Advance a
    -history like the one above once per accepted observation.
    +Use `PreviousTimeStep` when an input should read another model's accepted
    +result from the previous time step. The
    +[model execution reference](../../model_execution.md) shows how to declare
    +this input. If several models deliberately update the same variable, use
    +`Updates` to set their order; see [Control Advanced Execution](@ref).
    +
    +A trial call with `publish=false` does not save its result as an accepted
    +output sample. It can still change values in `status`, and those changes
    +are not automatically undone. A model that tries several solutions must
    +decide which changes to keep. Update a history like the one above once per
    +accepted observation, so rejected trials do not enter the mean.
     
     Use [Collecting And Plotting Outputs](@ref) to retain and analyse a result
     series. Keeping outputs and maintaining a model's own short memory serve
    diff --git a/docs/src/guides/multiscale/concepts.md b/docs/src/guides/multiscale/concepts.md
    index 9d201c6e4..3c0296ed9 100644
    --- a/docs/src/guides/multiscale/concepts.md
    +++ b/docs/src/guides/multiscale/concepts.md
    @@ -1,32 +1,39 @@
     # How Multiscale Composite Models Execute
     
    -One application executes once for every object selected by its
    -`ModelSpec(...; on=...)` selector. State belongs to the object, while topology
    -and labels belong to the scenario.
    -`Self()` is the current object, `SelfPlant()` is its plant-instance root, and
    -`SceneScope()` is model-wide. Cardinality wrappers decide whether zero, one,
    -or many matches are valid.
    +An **application** is a model configured with `ModelSpec`: it says where the
    +model runs and where its inputs come from. For example,
    +`ModelSpec(model; on=Many(scale=:Leaf))` runs that model once on each leaf.
    +Each leaf keeps its own values in `Status`. You define the objects, their
    +labels, and their parent relationships when building the simulation.
     
    -More objects mean more qualified streams. Removing an object stops future
    -execution but preserves its accepted historical samples.
    +Selectors choose objects by these labels and relationships. `Self()` means
    +the object whose model is currently running. `SelfPlant()` limits a search
    +to that object's plant, starting from the top of its structure.
    +`SceneScope()` allows the search to include the whole simulation. Wrap a
    +selection in `One`, `OptionalOne`, or `Many` to require exactly one match,
    +allow zero or one, or accept a collection.
     
    -Canonical selector patterns are:
    +Common choices are:
     
     | Relationship | Pattern |
     | --- | --- |
     | application targets every leaf | `ModelSpec(model; on=Many(scale=:Leaf))` |
    -| input from this same object | omit `inputs` when the producer is unique |
    +| input from this same object | omit `inputs` when exactly one model supplies the matching output |
     | input from one ancestor | `One(Ancestor(scale=:Plant))` |
     | input from this plant's leaves | `Many(scale=:Leaf, within=SelfPlant())` |
     | input from shared soil | `One(scale=:Soil, within=SceneScope())` |
     | optional named organ | `OptionalOne(name=:fruit, within=SelfPlant())` |
     
    -`Self()` always means the current target object. It never implicitly means the
    -model, process, species, or plant. Prefer object IDs and labels for identity,
    -and use `Scope(name)` only when the model explicitly defines that scope.
    +For a model running on a leaf, `Self()` means that leaf, not its plant or its
    +species. Use object IDs and labels to select specific objects or groups.
    +`Scope(name)` refers to a named search area that you must define in the
    +simulation before using it.
     
    -One application produces a separate stream for every selected object and
    -output variable. Stream keys also include application identity, so repeated
    -applications of the same process cannot overwrite each other. Use
    -`Diagnostics.explain_applications`, `Diagnostics.explain_objects`, and `Diagnostics.explain_bindings` to
    -verify target and source multiplicities before a long run.
    +Saved results identify the application, object, and variable. Each leaf's
    +history therefore stays separate, including when you use the same process
    +more than once. Removing an organ stops its future calculations but keeps
    +the results already saved for it.
    +
    +Before a long run, use `Diagnostics.explain_objects` to check the plant
    +structure, `Diagnostics.explain_applications` to check where models run, and
    +`Diagnostics.explain_bindings` to check where they read their inputs.
    diff --git a/docs/src/guides/multiscale/import_mtg.md b/docs/src/guides/multiscale/import_mtg.md
    index 0bbda33ba..4190ab2b6 100644
    --- a/docs/src/guides/multiscale/import_mtg.md
    +++ b/docs/src/guides/multiscale/import_mtg.md
    @@ -3,7 +3,7 @@
     A MultiScaleTreeGraph (MTG) stores a plant's organs and their relationships.
     Use it when you already have a measured or generated architecture. The models
     and value connections are the same as in [One multiscale plant](../../journeys/users/one_plant.md);
    -the MTG supplies the objects and their parent links.
    +the MTG supplies the objects and records which organ each one belongs to.
     
     Install `MultiScaleTreeGraph` in your project to run this example. We create a
     small MTG here so no external data file is needed:
    @@ -27,9 +27,11 @@ this reduced architecture omits stems and petioles. For an existing MTG file, re
     
     ## Choose which attributes become simulation state
     
    -Node IDs and parent links are imported automatically. By default, `scale`
    -comes from the MTG symbol. Numerical attributes are **not** automatically
    -copied into model status: supply the values your models need explicitly.
    +PlantSimEngine imports each node's ID and parent automatically. By default,
    +it uses the MTG symbol, such as `:Leaf`, as the object's `scale` label.
    +It does **not** automatically copy numerical attributes into `Status`,
    +where models read and store their values. The function below chooses the
    +initial values to import: each leaf's carbon biomass in this example.
     
     ```@example import_mtg
     initial_status(node) = MultiScaleTreeGraph.symbol(node) == :Leaf ?
    @@ -65,12 +67,14 @@ plant_area
     
     ## Keep the link to the MTG when the plant grows
     
    -`CompositeModel(root; ...)` retains the MTG adapter. It can resolve a source
    -node to an object with `object_id(model, leaf_1)`, and `add_organ!` reuses the
    -chosen status import rule when new organs appear. Required attributes must be
    -available when that rule runs, or the rule must deliberately initialize them.
    +`CompositeModel(root; ...)` keeps the relationship between MTG nodes and
    +simulation objects. Use `object_id(model, leaf_1)` to find the object for a
    +node. When you create an organ with `add_organ!`, PlantSimEngine reuses your
    +`initial_status` function to set its initial values. The new node must have
    +the attributes this function reads, or the function must supply suitable
    +initial values itself.
     See [Growth within a time step](../../tutorials/growing_plant/part1_growth.md).
     
     Use `objects_from_mtg(root; status=initial_status)` when you only want a
    -one-time list of `Object`s to assemble yourself. This projection does not keep
    -the MTG identity index needed for later node lookup or organ creation.
    +list of `Object`s to assemble yourself. It does not keep the node-to-object
    +lookup needed to find nodes or create new organs through the MTG later.
    diff --git a/docs/src/guides/multiscale/manual_calls.md b/docs/src/guides/multiscale/manual_calls.md
    index edf8f73e3..db49c70b2 100644
    --- a/docs/src/guides/multiscale/manual_calls.md
    +++ b/docs/src/guides/multiscale/manual_calls.md
    @@ -1,36 +1,45 @@
     # Manual Calls Across Objects
     
    -Declare parent-owned execution with
    -`ModelSpec(model; calls=(:name => One(...),))` or a `Many(...)` selector. In
    -the kernel, execute every resolved target with `run_call!(context, :name)`.
    -The returned `CallTargets` collection is always vector-like, including for
    -`One` and `OptionalOne`.
    +Some calculations need to control when another model runs. For example, a
    +plant model may need to run its leaf models several times while solving an
    +energy balance. Declare the models it can call with
    +`ModelSpec(model; calls=(:name => One(...),))`, using `Many(...)` to select
    +several objects. Inside its `run!` function, call
    +`run_call!(context, :name)` to run all the selected models. The returned
    +`CallTargets` is a collection, even when `One` or `OptionalOne` selects a
    +single object.
     
    -Use the narrowest execution path that matches the algorithm:
    +Choose how to make the call according to what your calculation needs:
     
     - `run_call!(context, :name; sampled_environment=environment)` executes all
    -  targets directly through cached typed batches. Prefer it when the caller has
    -  already sampled one model-facing environment for every target.
    -- `call_model(context, :name)` returns the concrete model for a call that
    -  resolves to exactly one target. It is useful when dispatch or model
    -  parameters must be inspected before the bulk call.
    +  selected models with the same environmental values. Use it when you have
    +  already prepared the values each model needs. PlantSimEngine groups these
    +  calls for efficient execution.
    +- `call_model(context, :name)` returns the model when the call selects exactly
    +  one target. Use it to inspect the model's type or parameters before running
    +  the calculation.
     - `call_targets(context, :name)` followed by `run_call!(target)` supports
    -  object selection, custom ordering, target status inspection, or a different
    -  sampled environment per target.
    +  running selected objects individually. Use it to choose their order,
    +  inspect their values, or supply different environmental values to each one.
     
    -`environment=trial_state` has different semantics from
    -`sampled_environment=value`. The former is a transient backend state that each
    -target samples through its compiled environment handle. The latter is already
    -in the model-facing form and is forwarded without sampling.
    +The two environment keywords serve different purposes.
    +`environment=trial_state` supplies a trial version of the environment, from
    +which PlantSimEngine retrieves the conditions at each selected object.
    +`sampled_environment=value` supplies the environmental variables directly in
    +the form the model reads, so no further sampling occurs.
     
    -A target used only by calls is absent from root scheduling. Trial calls default
    -to `publish=false`; publish only an accepted execution.
    +A model used only through these calls does not also run independently from
    +the simulation schedule. Calls default to `publish=false`, so trial results
    +are not saved in the output history. Use `publish=true` for the accepted
    +calculation.
     
     ## Initialize a newly registered object
     
    -Use `Initializer`, not an ordinary manual `Call`, when the target application
    -must remain in the root schedule but a creator needs to run it once on a new
    -object after its normal slot already passed:
    +Suppose a leaf model has already run for this timestep and a growth model
    +then creates a new leaf. The growth model can use `Initializer` to run that
    +leaf model once on the new leaf. The leaf model still runs normally from the
    +simulation schedule on other timesteps. Declare the initializer in the
    +growth model's `calls`, then pass the new object to `run_initializer!`:
     
     ```julia
     creator = ModelSpec(
    @@ -58,74 +67,105 @@ function PlantSimEngine.run!(::GrowthModel, status, environment, constants, cont
     end
     ```
     
    -Initializer selectors follow the ordinary contextual-scope rules. A call from
    -a plant object defaults to `Self()`, so a creator targeting a new descendant
    -must state `within=Subtree()` explicitly. A scene creator may instead use
    -`within=SceneScope()` when the target is scene-wide.
    -
    -The target application must use `on=Many(...)`, an explicit `application=`,
    -and exactly the caller's cadence and phase. It remains root-scheduled and owns
    -its canonical outputs. The compiler orders it before the creator and orders
    -the execution owners of same-step consumers after the creator. That owner is
    -the consumer itself for an ordinary scheduled application, the root hard-call
    -owner for a manual callee, or the consumer's creator when it is another
    -initializer target. If both calls belong to the same creator, its kernel must
    -invoke the initializers in dependency order; there is no meaningful self-edge
    -to impose that intra-kernel order. Targeted newborn
    -execution supports the global environment, canonical local outputs,
    -non-temporal inputs, and `PreviousTimeStep` inputs, including canonical input
    -sources written through another application's `outputs_to`. It rejects nested
    -calls, distributed or stream-only outputs on the initializer target itself,
    -other temporal policies, mixed manual ownership, multiple initializer owners,
    -and any overlapping local or distributed canonical writer for the target's
    -outputs. Each initialized output must have one canonical writer; `Updates`
    -ordering cannot make a later writer safe because targeted execution occurs
    -inside the creator's kernel.
    -
    -Only direct, non-temporal downstream consumers may observe the initialized
    -output later in that same step. `run_initializer!` deliberately publishes no
    -mid-step stream sample, so the compiler rejects downstream `HoldLast` windows,
    -`Interpolate`, `Integrate`, `Aggregate`, and `PreviousTimeStep` bindings that
    -could consume an initializer target's newborn output. This is distinct from a
    -`PreviousTimeStep` input *used by the initializer itself*, whose newborn
    -fallback is supported. This first initializer contract does not admit a
    -temporal downstream binding at all. When later history is required, publish
    -the value from a distinct scheduled application and consume that application's
    -history on a later timestep.
    -
    -An initializer binding stores only its statically validated application
    -identity. It does not collect pre-existing target objects or build cached
    -execution batches; `run_initializer!` resolves only the explicit newborn.
    -
    -`run_initializer!` accepts exactly one object added by the current pure
    -addition event, mutates its canonical local status without adding a mid-step
    -output-history sample, and returns that canonical `Status`. A second call for
    -the same application/object pair is an error. The pair is reserved before the
    -model runs, so a failed initializer remains marked and cannot be retried after
    -an unknown partial mutation in the same lifecycle event. Existing, reparented,
    -foreign, or refresh-fallback targets are also errors. Use ordinary `Call` and
    -`run_call!` for trial execution or repeated controller-owned calls.
    +By default, a call from a plant searches `Self()`, the plant object itself.
    +Set `within=Subtree()` to find a new leaf below it. A model that creates
    +objects anywhere in the scene can instead use `within=SceneScope()`.
    +
    +### Scheduling and supported inputs
    +
    +Name the target application explicitly with `application=`. That application
    +must select objects with `on=Many(...)` and run at exactly the same interval
    +and phase as the creator. The phase is the offset at which the repeating
    +schedule starts.
    +
    +PlantSimEngine schedules the target application before the creator. For
    +models that need its new output in the same step, it schedules the relevant
    +calculation after the creator:
    +
    +| How the reading model runs | What must run after the creator |
    +|---|---|
    +| Directly from the simulation schedule | The reading model itself |
    +| Through hard calls | The scheduled controller at the top of its call chain |
    +| As another initializer target | That target's creator |
    +
    +If one creator calls two initializers and the second needs the first's
    +output, the creator must call them in that order inside its `run!` function.
    +PlantSimEngine cannot reorder statements inside your function.
    +
    +An initializer supports the global environment and writes the new object's
    +own status values. These are its **canonical outputs**: the current values
    +other models normally read, as opposed to saved output history. It may read
    +current inputs or use `PreviousTimeStep`, including inputs supplied to the
    +object by another application's `outputs_to`.
    +
    +The following restrictions apply to the target application:
    +
    +- It cannot make nested calls, write to other objects through `outputs_to`,
    +  or produce outputs marked `stream_only`.
    +- Its input time policies are limited to `PreviousTimeStep`; it cannot use
    +  `HoldLast`, `Interpolate`, `Integrate`, or `Aggregate`.
    +- It cannot also be owned by an ordinary manual call or by a second creator.
    +- No other application may write its canonical outputs, either locally or
    +  through `outputs_to`. `Updates` does not relax this restriction: the
    +  initializer runs inside its creator, outside the target's normal slot.
    +
    +### Reading initialized values
    +
    +Other models can read the new values directly later in the same timestep.
    +They cannot read them through a time policy: `run_initializer!` does not add
    +a sample to the output history partway through a step. PlantSimEngine
    +therefore rejects downstream `HoldLast`, `Interpolate`, `Integrate`,
    +`Aggregate`, and `PreviousTimeStep` connections to an initializer's output.
    +This restriction applies to those connections altogether, not just to the
    +first step.
    +
    +There is a distinction between an initializer **reading** a
    +`PreviousTimeStep` input, which is supported with a fallback for the new
    +object, and another model trying to read the initializer's **output** through
    +`PreviousTimeStep`, which is not supported. If you need output history, have
    +a separate scheduled application publish the value and read that
    +application's history on a later timestep.
    +
    +### Initialize each new object once
    +
    +`run_initializer!` runs only on the new object you pass to it. The declaration
    +identifies and checks the application in advance; it does not build a
    +collection of existing objects to run.
    +
    +The call changes the new object's `Status` and returns it, without saving an
    +extra history sample. It accepts exactly one object created during the
    +current addition event, and that event must contain only additions. Existing
    +objects, reparented objects, objects from another simulation, or objects that
    +require a full structural refresh first are rejected.
    +
    +You may initialize a given application/object pair only once. PlantSimEngine
    +marks the pair before running the model, so if the model fails after changing
    +some values, you cannot retry it in the same event. Use ordinary `Call` and
    +`run_call!` when you need trial calculations or repeated calls.
     
     ## Compiled plans and changing objects
     
    -The call declaration is compiled once with the scenario. Its call name,
    -applications, selector, multiplicity, ordering, and execution batches remain
    -fixed during the simulation. Ordinary calls therefore do not resolve selectors
    -or rebuild public target wrappers in their execution loop.
    -
    -Objects may still be created, removed, or reparented during growth. At the
    -structural refresh barrier, PlantSimEngine updates only the affected resolved
    -target buffers. Later applications in the same timestep see the new targets;
    -applications that already ran are not repeated. The following ordinary
    -timestep returns to the cached execution path.
    -
    -`call_targets(context, name; objects=newborn)` can build a targeted partial
    -view without crossing that barrier only while the pending lifecycle delta is a
    -pure addition. If the same event also removes or reparents an object, the
    -manual-call API performs the full binding and environment refresh before it
    -resolves the requested objects. This preserves current topology membership but
    -has the cost of a mid-kernel refresh and consumes the pending dirty state.
    -`run_initializer!` is stricter: it rejects such a mixed structural event.
    -
    -Explicit target cadence must match the caller. A target without an explicit
    -cadence inherits the caller's invocation timing.
    +When preparing the simulation, PlantSimEngine checks each call declaration
    +and prepares how it will execute. The call name, selected applications,
    +selector rules, `One`/`Many` requirement, execution order, and model groups
    +stay fixed. Ordinary calls reuse this preparation instead of searching for
    +objects again every time they run.
    +
    +The objects themselves may still be added, removed, or reparented during
    +growth. After the model making a structural change finishes, PlantSimEngine
    +updates the affected object lists. Applications later in the same timestep
    +use the updated lists; applications that already ran are not repeated. On
    +the next timestep, calls reuse the updated preparation.
    +
    +To call a newly created object before that update,
    +`call_targets(context, name; objects=newborn)` can select it directly if the
    +pending changes only add objects. If the same event also removes or
    +reparents an object, this call first updates all affected model connections
    +and environment lookups. This ensures that selection uses the current plant
    +structure, but the extra work takes place inside your model's `run!`
    +function, and those pending changes are then marked as processed.
    +`run_initializer!` is stricter: it rejects events that mix additions with
    +removal or reparenting.
    +
    +If the called model declares its own execution interval, it must match the
    +caller's. Otherwise it follows the caller's timing.
    diff --git a/docs/src/guides/multiscale/value_coupling.md b/docs/src/guides/multiscale/value_coupling.md
    index ff75ba093..7b5118e12 100644
    --- a/docs/src/guides/multiscale/value_coupling.md
    +++ b/docs/src/guides/multiscale/value_coupling.md
    @@ -1,19 +1,31 @@
     # Coupling Values Across Objects
     
    -- `One(...)` requires exactly one source.
    -- `OptionalOne(...)` accepts zero or one.
    -- `Many(...)` supplies a stable object-ID ordered carrier.
    -
    -Use `var=` to rename a source and `application=` to distinguish repeated
    -processes. Homogeneous many-source values use a `RefVector`; heterogeneous
    -values use an object-aware reference carrier. Inspect both through
    -`Diagnostics.input_carrier`, `Diagnostics.input_value`, and `Diagnostics.explain_bindings`, not internal fields.
    +To read values from other objects, describe which objects supply the input:
    +
    +- `One(...)` requires exactly one source object, such as the plant to which
    +  a leaf belongs.
    +- `OptionalOne(...)` allows zero or one source object.
    +- `Many(...)` supplies a collection, such as the areas of all leaves on a
    +  plant, ordered by object ID.
    +
    +Set `var=` when the source variable has a different name from the input.
    +Set `application=` when you need to specify which model application supplies
    +the value, for example when using the same process more than once.
    +
    +PlantSimEngine shares these values by reference: the receiving model reads
    +the current source values without copying them. A collection with the same
    +value type throughout uses `RefVector`; one with different types uses
    +`ObjectRefVector`. These collections are called **input carriers** in the
    +diagnostics. Use `Diagnostics.input_value` to read the values,
    +`Diagnostics.input_carrier` to inspect their container, and
    +`Diagnostics.explain_bindings` to see where they come from.
     
     ## Keep identities aligned with values
     
    -A model that only reduces or broadcasts over a `Many` input can keep using the
    -ordinary status field. When a model must associate a value with the object that
    -owns it, request the identity-aware view from the current `RunContext`:
    +If a model only needs to sum or multiply the values in a `Many` input, use
    +the input's `status` field as usual. If it also needs to know which leaf or
    +other object each value belongs to, call `bound_input` using `context`, the
    +information PlantSimEngine passes to each `run!` call:
     
     ```julia
     function PlantSimEngine.run!(model, status, environment, constants, context)
    @@ -28,24 +40,23 @@ function PlantSimEngine.run!(model, status, environment, constants, context)
     end
     ```
     
    -`BoundMany` wraps the same live `RefVector` or heterogeneous carrier already
    -installed in `status.irradiance`; it does not copy values or identities.
    -Positions follow compiled `ObjectId` order and have no botanical meaning.
    -Identity lookup is unambiguous when written as
    -`irradiance[ObjectId(:leaf_12)]`; integer indexing remains positional.
    +The returned `BoundMany` gives access to both values and their object IDs,
    +without copying either. Its values are the same ones available through
    +`status.irradiance`. Their order follows `ObjectId`, not the position of an
    +organ on the plant. Use `irradiance[ObjectId(:leaf_12)]` to read a named leaf's
    +value, or an integer index to read a position in the collection.
     
    -Obtain the view during each model invocation. Lifecycle refresh keeps the
    -current view aligned when possible and may replace it after insertion,
    -removal, or reparenting, so model code must not cache a `BoundMany` across a
    -lifecycle barrier.
    +Call `bound_input` each time the model runs. PlantSimEngine may replace the
    +collection after an object is added, removed, or moved to a different parent,
    +so do not store it in your model for later calls.
     
     ## Publish one computation to many objects
     
    -Some models execute once on a scene or plant but compute one value per organ.
    -A light model is the typical example: the scene application owns the
    -calculation and cadence, while each leaf owns its local irradiance values.
    -Declare that relationship with `outputs_to`, then publish the solver result by
    -object identity:
    +Some models run once for a scene or plant but compute one value per organ.
    +For example, a light model may calculate illumination for the whole scene
    +and then store each leaf's irradiance on that leaf. Declare these
    +destinations with `outputs_to`, and use object IDs to assign each result to
    +the right leaf:
     
     ```julia
     PlantSimEngine.@process "scene light" verbose = false
    @@ -75,10 +86,11 @@ function PlantSimEngine.run!(
     end
     ```
     
    -Here `solve` is an adapter around the scene solver. It returns any
    -Tables.jl-compatible value with an `object_id` column and the declared result
    -columns, for example `incident_par` and `absorbed_par`. Rows may arrive in any
    -order because `assign_outputs!` maps them to destinations by `ObjectId`.
    +Here `solve` calls your scene light calculation and returns its results as a
    +table supported by Tables.jl, such as a `DataFrame`. The table needs an
    +`object_id` column and one column for each declared output, for example
    +`incident_par` and `absorbed_par`. Rows may be in any order:
    +`assign_outputs!` uses `ObjectId` to put each result on the right leaf.
     
     Declare the destinations on the scene application:
     
    @@ -99,12 +111,12 @@ light_application = ModelSpec(
     )
     ```
     
    -Inside the kernel, `targets.columns.incident_par` and
    -`targets.columns.absorbed_par` are the live destination carriers.
    -`object_ids(targets)` is the aligned, read-only identity view. Direct
    -positional writes are appropriate only when the producing algorithm is already
    -using that exact identity order; identified external results should go through
    -`assign_outputs!`.
    +Inside `run!`, `targets.columns.incident_par` and
    +`targets.columns.absorbed_par` give direct access to the selected leaves'
    +values. `object_ids(targets)` lists their IDs in the same order; this list
    +cannot be modified. Write directly by position only if your calculation
    +already uses that exact order. For a separate result table with its own IDs,
    +use `assign_outputs!` to match the rows to leaves.
     
     ## Consume those values normally
     
    @@ -142,13 +154,13 @@ applications = (
     )
     ```
     
    -The compiler knows that `:scene_light` owns `:absorbed_par` on each selected
    -leaf. It binds the leaf input and schedules the scene writer before the leaf
    -consumer even though the consumer appears first in the tuple. No per-leaf copy
    -model, `from_status=true`, or manual `after=:scene_light` declaration is
    -needed.
    +PlantSimEngine knows that `:scene_light` supplies `:absorbed_par` for each
    +selected leaf and that `:leaf_assimilation` needs it. It connects the two and
    +runs the light calculation first, even though the assimilation model appears
    +first in the tuple. You do not need an extra model to copy the light values
    +or an `after=:scene_light` instruction to set their order.
     
    -## Assignment contract
    +## Rules for assigning results
     
     `assign_outputs!` supports two public forms:
     
    @@ -157,39 +169,43 @@ assign_outputs!(targets, result_table; id=:object_id)
     assign_outputs!(targets, result_ids, result_columns)
     ```
     
    -The first accepts any Tables.jl-compatible column or row table. The second
    -accepts an `AbstractVector` of IDs and a `NamedTuple` of columns, avoiding the
    -table adapter on a stable columnar path. Both forms use the same rules:
    +The first accepts any row or column table supported by Tables.jl. The second
    +takes an `AbstractVector` of IDs and a `NamedTuple` of value columns, which
    +is useful when your solver already returns separate arrays. Both forms use
    +the same rules:
     
     | Result content | Behavior |
     |---|---|
     | One row for every current destination | Required |
    -| Unknown, duplicate, extra, or missing IDs | Rejected before mutation |
    +| Unknown, duplicate, extra, or missing IDs | Rejected before any destination value changes |
     | Every variable declared by `OutputTo` | Required |
     | Additional columns such as solver metadata | Ignored |
    -| Source columns overlapping destination storage | Rejected, except exact-order self-assignment of the same column |
    +| Result columns sharing memory with destination columns | Rejected, except assigning a column to itself in exactly the same order |
     
    -Only `coverage=:exact` is supported. A filtered, abscised, or non-geometrized
    -organ must therefore be handled deliberately by the scene adapter or excluded
    -by the destination selector; PlantSimEngine never silently retains an old
    -value or substitutes zero for a missing result.
    +Only `coverage=:exact` is supported: you must supply one result for every
    +selected object. If the light solver skips an organ because it has no
    +geometry or has been removed from its scene, decide how to handle it. Either
    +return an appropriate value or exclude that organ from the destination
    +selector. PlantSimEngine will report a missing result instead of keeping an
    +old value or substituting zero.
     
     ## Reuse stable columns efficiently
     
    -PlantSimEngine caches the result-row permutation by the identity of the ID
    -column. Reusing the same ID vector promises that its IDs and order remain
    -unchanged; mutate only the result value columns in place. Replace the ID vector
    -when membership or ordering changes. A lifecycle refresh invalidates this
    -cache automatically, and the next invocation rebuilds it against the refreshed
    -destinations.
    -
    -Homogeneous destination values use typed `RefVector` columns and the stable
    -exact-order path can run without allocations after compilation. If selected
    -statuses hold different concrete value types, PlantSimEngine falls back to an
    -`ObjectRefVector` and converts against each destination reference. The public
    -API is identical, but the homogeneous representation is the performance path
    -to prefer for large per-organ assignments.
    -
    -Like `BoundMany`, an `OutputTargets` view belongs to the current invocation and
    -lifecycle generation. Obtain it from `RunContext` each time rather than storing
    -it in the model.
    +PlantSimEngine remembers how the result rows match the destination objects.
    +You can reuse the same ID vector to avoid repeating this work, but its IDs
    +and their order must remain unchanged. Update only the value columns. If
    +the objects or their order change, provide a new ID vector. After an object
    +is added, removed, or reparented, PlantSimEngine automatically rebuilds the
    +match on the next call.
    +
    +When all destination values have the same concrete Julia type, PlantSimEngine
    +uses `RefVector` columns. With a stable ID order that already matches the
    +destinations, assignments can run without new memory allocations after
    +compilation. If the destinations hold different types, PlantSimEngine uses
    +`ObjectRefVector` and converts each value to its destination type. Both use
    +the same API; keeping types consistent is preferable when assigning results
    +to many organs.
    +
    +Call `output_targets` each time your model runs, just as with `bound_input`.
    +Do not store the returned `OutputTargets` in the model: the selected objects
    +may change as the plant grows.
    diff --git a/docs/src/guides/multiscale/visualizing_structure.md b/docs/src/guides/multiscale/visualizing_structure.md
    index 62780c5a8..9f90bbd67 100644
    --- a/docs/src/guides/multiscale/visualizing_structure.md
    +++ b/docs/src/guides/multiscale/visualizing_structure.md
    @@ -8,7 +8,8 @@ dependencies, and object parent links for plant structure.
     ## Draw the parent links
     
     This small example needs `DataFrames` and `CairoMakie` in your project. It
    -draws one plant with two leaves from the public object diagnostics:
    +draws one plant with two leaves. `Diagnostics.explain_objects` lists the
    +objects and their parents so we can draw the connections:
     
     ```@example structure_plot
     using PlantSimEngine, DataFrames, CairoMakie
    @@ -53,10 +54,11 @@ nothing # hide
     For a larger structure, choose a tree-layout algorithm or positions from your
     geometry data. Keep object IDs as the link between results, geometry, and
     labels. After growth or pruning, call `Diagnostics.explain_objects(model)`
    -again to draw the current topology; retained simulation outputs still include
    +again to draw the current structure. Saved simulation results still include
     the history of removed organs.
     
    -For plant instances, `Diagnostics.explain_instances(model)` identifies their
    -roots. `Diagnostics.explain_scopes(model)` helps check the groups used by
    -selectors. Rendering stays outside the process equations, so the same model
    -can be used with or without a visualization.
    +When using plant templates, `Diagnostics.explain_instances(model)` identifies
    +the object at the top of each plant's structure. Use
    +`Diagnostics.explain_scopes(model)` to check the search areas used to select
    +objects. The drawing code is separate from the process models, so you can
    +run the same simulation with or without a visualization.
    diff --git a/docs/src/guides/time/advanced_time_environment.md b/docs/src/guides/time/advanced_time_environment.md
    index eb4ddf9c6..cb9a4ac12 100644
    --- a/docs/src/guides/time/advanced_time_environment.md
    +++ b/docs/src/guides/time/advanced_time_environment.md
    @@ -1,14 +1,15 @@
     # Choose Compatible Time Steps
     
    -Use this page when a simulation rejects an application cadence, or when
    -several models need different time steps. For choosing how values pass between
    -those models, start with [Different model cadences](../../journeys/users/cadences.md).
    +Use this page when a simulation rejects how often you want a model to run,
    +or when several models need different time steps. To choose how those models
    +share values between updates, start with
    +[Different model cadences](../../journeys/users/cadences.md).
     
     ## Find a common base step
     
    -The simulation advances on a fixed base step, supplied by the environment's
    -`duration`. Each `every` must be a positive integer multiple of that duration.
    -PlantSimEngine does not insert intermediate steps automatically.
    +The simulation advances by a fixed interval, called the **base step**. Set it
    +with the environment's `duration`. A model's `every` value must be a positive
    +whole number of base steps. PlantSimEngine does not add smaller steps for you.
     
     | Model cadences | A suitable base step |
     |:--|:--|
    @@ -17,15 +18,16 @@ PlantSimEngine does not insert intermediate steps automatically.
     | Every 250 ms and every second | 250 ms |
     
     A smaller step may also work, but increases the number of simulation steps.
    -Choose one supported by the equations and by the available forcing data.
    -Resampling measurements requires an explicit interpolation or aggregation
    -choice; changing `duration` alone does not create the missing observations.
    +Choose one that suits the equations and your weather data. If you need values
    +between measurements, choose how to estimate them. If you need less frequent
    +values, choose how to combine the measurements, for example by averaging.
    +Changing `duration` alone does not calculate these values.
     
     ## Example: hourly and 90-minute sampling
     
    -Two temperature readers illustrate the schedule without introducing a new
    -scientific model. Both start at the first time point, then run at their own
    -cadence:
    +Two simple models read temperature at different intervals. Both run at the
    +start of the simulation, then one runs every hour and the other every
    +90 minutes:
     
     ```@example compatible_steps
     using PlantSimEngine, Dates, DataFrames
    @@ -47,9 +49,9 @@ rows = collect_outputs(simulation; sink=DataFrame)
     combine(groupby(rows, :application_id), nrow => :samples)
     ```
     
    -The seven base time points cover 0 to 3 hours: the hourly reader publishes
    -four samples, and the other publishes three. The first sample is at the
    -origin, not after a completed hour.
    +The seven time points cover 0 to 3 hours. The hourly reader records four
    +results, and the other records three. Both record their first result at
    +time zero.
     
     ```@example compatible_steps
     @assert count(==(:hourly), rows.application_id) == 4 # hide
    @@ -57,16 +59,23 @@ origin, not after a completed hour.
     nothing # hide
     ```
     
    -## Check the resolved configuration
    +## Check when models run and where inputs come from
     
    -Use `Diagnostics.explain_schedule(model)` to inspect cadence and clock origin.
    -Use `Diagnostics.explain_bindings(model)` for the selected producer, temporal
    -policy, and window, and `Diagnostics.explain_environment_bindings(model)` for
    -environment sources and reducers.
    +Use these tables to check the configuration:
     
    -`every` overrides a model's default `timespec`. When cadence comes from the
    -environment base step, a `timestep_hint` can check compatibility; an explicit
    -`every` is the scenario author's choice and must suit the equations. Fixed
    -periods such as `Day(1)` are supported. Calendar months have varying lengths,
    -so `Month(1)` is rejected. Windows are rolling durations, not automatically
    -aligned civil days or previous complete calendar periods.
    +- `Diagnostics.explain_schedule(model)` shows when each model starts and how
    +  often it runs.
    +- `Diagnostics.explain_bindings(model)` shows which model supplies each input
    +  and how earlier values are used, including the rule and time window.
    +- `Diagnostics.explain_environment_bindings(model)` shows the weather or
    +  spatial data each model reads and how values are combined over time.
    +
    +Setting `every` replaces the model's default cadence from `timespec`. If you
    +leave the cadence to the environment's base step, the model can check it with
    +`timestep_hint`. If you set `every` yourself, you must check that the equations
    +support that interval.
    +
    +Fixed periods such as `Day(1)` are supported. Calendar months vary in length,
    +so `Month(1)` is rejected. A time window looks back over its specified
    +duration. For example, `Day(1)` does not automatically mean the preceding
    +midnight-to-midnight day.
    diff --git a/docs/src/guides/time/hourly_daily_weekly.md b/docs/src/guides/time/hourly_daily_weekly.md
    index 1e1080572..555cc2e95 100644
    --- a/docs/src/guides/time/hourly_daily_weekly.md
    +++ b/docs/src/guides/time/hourly_daily_weekly.md
    @@ -2,18 +2,19 @@
     
     Compare daily and weekly water uptake from the same hourly leaf rates. The
     rates below are deliberately constant teaching values: 1 and 2 mg of water
    -per leaf per second. They demonstrate timing and aggregation, not a model of
    -plant water demand.
    +per leaf per second. They show how to calculate totals over different time
    +intervals; they do not predict plant water demand.
     
    -Each total integrates the hourly rates over its own rolling window, then adds
    -the two leaf amounts. The daily and weekly applications both read the hourly
    -source, so the weekly result does not add overlapping daily windows.
    +To calculate each total, multiply the hourly rates by the duration they
    +represent, then add the two leaves' amounts over a day or a week. Both totals
    +use the hourly values directly. This avoids counting the same water twice
    +by adding daily totals whose periods might overlap.
     
     ## Load the two equations
     
    -The reusable [teaching models](teaching_models.jl) publish a supplied rate and
    -sum supplied amounts. Their definitions are included at the end of this page;
    -you only need to load them to configure this simulation.
    +The reusable [teaching models](teaching_models.jl) return a supplied rate and
    +add supplied amounts. Their definitions are included at the end of this page;
    +load them now to configure this simulation.
     
     ```@example hourly-daily
     using Dates, DataFrames, PlantSimEngine
    @@ -21,12 +22,13 @@ include(joinpath(pkgdir(PlantSimEngine), "docs", "src", "guides", "time", "teach
     using .TeachingTimeModels
     ```
     
    -## Choose the clocks and windows
    +## Choose update intervals and time windows
     
    -`every` says when a model runs; `window` says how much source history its
    -input uses. Hourly, daily and weekly durations all fit an hourly base step.
    -The reducer multiplies each rate by its represented duration in seconds,
    -converting mg s⁻¹ into mg before the plant adds the leaf amounts.
    +`every` says how often a model runs; `window` says how far back it looks for
    +input values. Hourly, daily and weekly durations all fit an hourly base step.
    +The function passed to `Integrate` multiplies each rate by the duration it
    +represents, in seconds. This converts mg s⁻¹ into mg before the plant adds
    +the leaf amounts.
     
     ```@example hourly-daily
     integrate_rate = Integrate((values, durations_seconds) -> sum(values .* durations_seconds))
    @@ -64,10 +66,10 @@ totals = rows[in.(rows.application_id, Ref((:daily, :weekly))),
     totals
     ```
     
    -The first execution is at step 1. Both totals initially contain only one
    -hour of available history: `(1 + 2) × 3600 = 10800 mg`. That is a startup
    -value, not a complete day or week. The daily application runs again at steps
    -25, 49, and so on; the weekly application runs again at step 169.
    +Both totals are calculated at step 1, when only one hour of values is
    +available: `(1 + 2) × 3600 = 10800 mg`. This first result does not represent
    +a complete day or week. The daily total is calculated again at steps 25, 49,
    +and so on; the weekly total is calculated again at step 169.
     
     ```@example hourly-daily
     first_daily = only(totals[(totals.application_id .== :daily) .& (totals.timestep .== 1), :value])
    @@ -79,34 +81,37 @@ full_week = only(totals[(totals.application_id .== :weekly) .& (totals.timestep
     (startup_mg=first_daily, daily_mg=full_day, weekly_mg=full_week)
     ```
     
    -These are fixed-duration rolling windows, not calendar-aligned civil days or
    -weeks. All application durations must be integer multiples of the base step;
    -choose a finer common step if necessary. Calendar months and adaptive steps
    -are not supported by this scheduler.
    +Each window looks back over its specified duration. It does not automatically
    +start at midnight or at the beginning of a calendar week. Each model's cadence
    +must be a whole number of base steps; choose a smaller common step if needed.
    +Calendar months and timesteps that change during a run are not supported.
     
    -`Integrate()` without a reducer only sums samples. Use it for values that are
    -already the amounts you intend to add, after checking the selected window and
    -sample boundaries. Rates require duration weighting as above. Use
    -`Aggregate(reducer)` when the intended result is a mean or another statistic.
    +`Integrate()` without a function only adds values. Use it when the values
    +already represent amounts, and check that the selected window includes each
    +intended amount once. For rates, multiply by duration as above. Use
    +`Aggregate(reducer)` to calculate a mean or another statistic, supplying your
    +calculation as the `reducer` function.
     
     ## Scientific contracts and outputs
     
    -The teaching models leave variable contracts undeclared so this page can
    -isolate the numeric time policies. These policies do not change contract
    -metadata. A production model with declared rate and amount contracts needs
    -an explicit conversion model between those contracts; see
    +These teaching models do not declare `VariableContract`s, which describe a
    +variable's units and physical meaning. The rules for combining values over
    +time do not change these declarations. If your models declare a rate contract
    +on one side and an amount contract on the other, connect them through a small
    +model that performs the conversion and declares both meanings. See
     [Coupling models](../coupling.md).
     
    -Input windows retain the history needed by the simulation. Your analysis
    -outputs are a separate choice: see [Collecting And Plotting Outputs](../data/outputs_plotting.md)
    -for retaining selected results. Inspect the schedule when checking how many
    -daily or weekly publications to expect, including startup.
    +PlantSimEngine keeps the earlier values needed for each input window. Choose
    +separately which results to save for your own analysis: see
    +[Collecting And Plotting Outputs](../data/outputs_plotting.md). Check the
    +schedule to find how many daily or weekly results to expect, including the
    +first partial result.
     
     ## Teaching model source
     
     These simple equations are shared with the [cadence tutorial](../../journeys/users/cadences.md).
    -They copy each leaf's supplied rate and sum the converted leaf amounts. The
    -weekly model uses a separate output name so both totals coexist on the plant.
    +They copy each leaf's supplied rate and add the converted leaf amounts. The
    +weekly model uses a different output name so the plant can store both totals.
     
     ```@eval
     using Markdown, PlantSimEngine
    diff --git a/docs/src/index.md b/docs/src/index.md
    index 14a2c914b..000860d22 100644
    --- a/docs/src/index.md
    +++ b/docs/src/index.md
    @@ -116,8 +116,8 @@ MAESPA.
     What happens if carbon gain stops increasing in proportion to absorbed light?
     Here, the same small simulation is run with a linear response and then a
     saturating response. Only the carbon-gain model changes. Both models declare
    -the same inputs, outputs and units, so PlantSimEngine can check that their
    -interfaces are compatible.
    +the same inputs, outputs and units, so PlantSimEngine can check whether
    +one can replace the other.
     
     ![Two teaching models predict different carbon gains from the same absorbed light: a linear response and a saturating response.](home-model-comparison.svg)
     
    @@ -220,10 +220,11 @@ results = collect_outputs(simulation; sink=DataFrame)
     nothing # hide
     ```
     
    -PlantSimEngine connects these models because each required variable has one
    -matching source. The result table contains their outputs over time. The
    -[step-by-step tutorial](journeys/users/one_object.md) shows how to inspect the
    -connections and continue a simulation; the
    +The thermal-time model supplies the LAI model, which supplies the light
    +model. PlantSimEngine connects them using their matching input and output
    +names. The result table contains their outputs over time. The
    +[step-by-step tutorial](journeys/users/one_object.md) explains these connections
    +and shows how to continue a simulation; the
     [plotting guide](guides/data/outputs_plotting.md) explains how to display results.
     
     ## Scientific models and further reading
    diff --git a/docs/src/introduction/why_julia.md b/docs/src/introduction/why_julia.md
    index 9ddc7dda3..21766ea0f 100644
    --- a/docs/src/introduction/why_julia.md
    +++ b/docs/src/introduction/why_julia.md
    @@ -28,9 +28,10 @@ profiling, and type-inspection tools to find where optimization is useful.
     Keeping the model in Julia makes it possible to maintain the same tests while
     improving its implementation.
     
    -Performance still depends on the algorithm, data representation, allocations,
    -and work requested by the simulation. Package loading and first execution can
    -include compilation time, so measure them separately from repeated runs.
    +Performance depends on the algorithm, how data are stored, how much memory
    +must be created, and how much work the simulation requests. The first run can
    +also take longer while Julia prepares the code for execution. Measure that
    +first run separately from repeated runs.
     Julia's [performance guide](https://docs.julialang.org/en/v1/manual/performance-tips/)
     explains these distinctions. PlantSimEngine's
     [benchmarking guidance](../developers.md) adds the costs of scenario
    @@ -39,8 +40,8 @@ initialization, structural updates, and output collection.
     ## Share the software environment with the experiment
     
     Julia's package manager supports a separate environment for each project.
    -Its `Project.toml` records dependencies and its `Manifest.toml` records resolved
    -package versions. Sharing these files, the Julia version, model code, and
    +Its `Project.toml` lists the packages the project needs, and its `Manifest.toml`
    +records the exact versions used. Sharing these files, the Julia version, model code, and
     input data helps others recreate an experiment. See the official
     [environment guide](https://pkgdocs.julialang.org/v1/environments/) and
     [PlantSimEngine installation](../prerequisites/installing_plantsimengine.md).
    diff --git a/docs/src/introduction/why_plantsimengine.md b/docs/src/introduction/why_plantsimengine.md
    index 154cede4d..d9796b8a5 100644
    --- a/docs/src/introduction/why_plantsimengine.md
    +++ b/docs/src/introduction/why_plantsimengine.md
    @@ -15,7 +15,7 @@ of processes or assumptions:
     - **Canopy ecophysiology:** connect weather, local conditions and organ processes,
       including calculations that must iterate together. The
       [MAESPA-style synthesis](../journeys/users/maespa_synthesis.md) demonstrates
    -  these coupling mechanisms with pedagogical models.
    +  how these calculations can work together, using teaching models.
     - **Functional–structural plant modelling:** apply processes to organs,
       connect their results to the plant, and update the structure during growth.
       Start with [one multiscale plant](../journeys/users/one_plant.md).
    @@ -41,14 +41,14 @@ and whether it is a rate or an accumulated amount. The same variable name
     alone is insufficient. Moving from a quantity per unit ground area to a total
     per plant, for example, needs an explicit conversion.
     
    -Replacing a model for selected plants within one application requires a
    -compatible complete interface. Broader changes may require reconnecting
    -affected inputs. See
    +To replace a model only for selected plants, its inputs, outputs, and other
    +requirements must match the original model. A replacement that needs an extra
    +input, such as soil water content, requires changes to the simulation setup. See
     [model compatibility and replacement](../step_by_step/model_switching.md).
     
    -For model authors, this separation keeps a process implementation readable
    -from inputs to outputs. For researchers assembling models, it makes the
    -choice of hypotheses visible in the scenario. Both can test a component
    +Model authors can read each equation together with its inputs and outputs.
    +Researchers assembling models can see which equations and assumptions were
    +chosen in the simulation setup. Both can test a component
     before investigating its behaviour in the full system.
     
     ## Choose the representation that answers your question
    @@ -60,8 +60,8 @@ Different models can be applied to different selections of these objects.
     
     The same process equation can be reused over compatible objects while the
     scenario handles their selection and connections. A plant-level model can,
    -for example, read values from its own leaves. The modeller supplies the
    -aggregation equation and any required area, mass, or temporal conversion.
    +for example, read values from its own leaves. The modeller decides how to combine these values, for example by summing leaf
    +areas, and supplies any required area, mass, or time conversion.
     Changing resolution also requires checking the assumptions and validity of
     the chosen models. See [value coupling across objects](../guides/multiscale/value_coupling.md).
     
    @@ -91,10 +91,10 @@ missing inputs, the sources of values, execution order, local environmental
     conditions, and which results are saved. You can inspect why a particular leaf
     receives a value, which model supplies it, and when it is updated. The
     [graph viewer](../guides/graph_visualizer_editor.md) provides another view of
    -the same composition.
    +how the models are connected.
     
     These reports help separate a coupling problem from a problem in an equation
    -or its assumptions. Declared contracts expose mismatches at model boundaries;
    +or its assumptions. Declared units and physical meanings help reveal incompatible connections;
     scientific validation still needs suitable observations, reference results,
     and tests. See the [model authoring API](../API/API_public.md) and
     [model testing guide](../guides/modelers/repository_and_tests.md).
    @@ -112,7 +112,7 @@ reports a median of 5.3 microseconds for one leaf and one time step of its
     coupled energy-balance, photosynthesis, and stomatal-conductance benchmark.
     That result concerns the implementations, versions, inputs, and hardware used
     in the study. For your scenario, measure initialization, repeated execution,
    -structural changes, and retained outputs separately using the
    +structural changes, and saving results separately using the
     [benchmarking guidance](../developers.md). A public parallel or distributed
     executor remains [planned work](../planned_features.md).
     
    @@ -126,7 +126,7 @@ interfaces, assemble a scenario, inspect its connections, and run tests with
     the tools available in your development environment.
     
     PlantSimEngine provides a versioned [AI agent skill](../agent_skill.md), a
    -[loaded model catalog](../API/model_catalog.md), and the same authoring tools
    +[catalog of available models](../API/model_catalog.md), and the same authoring tools
     used by people. You supply the coding agent and its execution environment.
     Model assumptions, physical conversions, supporting references, and scientific
     validation remain the responsibility of the researcher. Begin with
    diff --git a/docs/src/journeys/modelers/basic_model.md b/docs/src/journeys/modelers/basic_model.md
    index 3dc9ae82f..d2c07c063 100644
    --- a/docs/src/journeys/modelers/basic_model.md
    +++ b/docs/src/journeys/modelers/basic_model.md
    @@ -6,7 +6,7 @@ This teaching example describes daily biomass production as:
     
     For an efficiency of 1.5 g dry matter per mol of photons and 10 mol of
     intercepted photons per plant, the result is 15 g dry matter per plant.
    -These values illustrate the interface; they are not a calibrated crop model.
    +These values show how to write a model; they are not a calibrated crop model.
     
     We will give that equation a name, declare its variables and units, test it,
     and run it on two plants. Before adding your own model, use
    @@ -54,8 +54,11 @@ Main.DocsSources.section(
     )
     ```
     
    -A `VariableContract` states the physical meaning of a value at a connection
    -between models. Here both quantities are daily totals for one plant:
    +A `VariableContract` describes a variable's units and meaning. It records,
    +for example, whether a value is per plant or per square metre, and whether it
    +is a rate or a daily total. This helps check that two connected models
    +interpret a value in the same way. Here both quantities are daily totals
    +for one plant:
     
     ```@eval
     Main.DocsSources.section(
    @@ -64,6 +67,11 @@ Main.DocsSources.section(
     )
     ```
     
    +Read these settings as follows: `unit` names the measurement unit,
    +`basis=:plant` says it refers to one plant, and `temporal=:day` with
    +`aggregation=:total` says it is a daily total. `extent=:extensive` means the
    +amounts from several plants can be added to obtain their combined amount.
    +
     Attach those descriptions to the corresponding variables:
     
     ```@eval
    @@ -74,8 +82,9 @@ Main.DocsSources.section(
     )
     ```
     
    -Matching contracts help check connections. Converting between physical bases
    -or units requires an explicit [adapter model](../../guides/coupling.md).
    +These descriptions help check connections, but do not convert values. If one
    +model supplies radiation per square metre and another needs radiation per
    +plant, write the conversion in an [adapter model](../../guides/coupling.md).
     
     ## Write the equation
     
    @@ -89,9 +98,10 @@ Main.DocsSources.section(
     )
     ```
     
    -The remaining arguments carry environmental forcing, constants, and execution
    -context. This simple equation does not need them. Object selection and
    -simulation timing belong in the scenario.
    +The other arguments provide environmental data, constants, and tools for
    +calling other models or changing the simulated objects. This equation does
    +not need them. Choose which plants use the model and when it runs in the
    +simulation setup below.
     
     ## Test one calculation
     
    @@ -123,7 +133,8 @@ validation = Authoring.validate_model(model; strict=true)
     validation.valid
     ```
     
    -That check validates the interface. Scientific validation needs appropriate
    +That check finds missing or inconsistent model declarations. To check whether
    +the equation describes real plants, compare its results with appropriate
     observations or reference results.
     
     ## Run the model on two plants
    @@ -147,8 +158,9 @@ result_2 = final_state(simulation, :plant_2).biomass_increment
     (plant_1=result_1, plant_2=result_2)
     ```
     
    -PlantSimEngine calls the equation for each selected plant. Its state remains
    -separate, and the model contains no loop over plants.
    +PlantSimEngine calls the equation once for each selected plant. Each plant
    +keeps its own input and result, so you do not need to write a loop over plants
    +inside the model.
     
     ## Continue with your own model
     
    diff --git a/docs/src/journeys/modelers/cross_object_values.md b/docs/src/journeys/modelers/cross_object_values.md
    index a2a2d874b..f7bb971bc 100644
    --- a/docs/src/journeys/modelers/cross_object_values.md
    +++ b/docs/src/journeys/modelers/cross_object_values.md
    @@ -55,8 +55,8 @@ growth = final_state(simulation, :leaf).growth
     growth
     ```
     
    -`from_status=true` reads the explicitly supplied soil state. When a soil model
    -produces that value, select its application as described in
    +`from_status=true` reads the value we stored in the soil object's `Status`.
    +When a soil model calculates that value instead, select that model as described in
     [Coupling models](@ref). The development equation itself stays unchanged.
     
     ## Read several values and combine them
    @@ -116,15 +116,16 @@ plant_result = final_state(run!(plant), :plant).Rm
     plant_result
     ```
     
    -`Many` provides the selected values in a vector-like input. `Subtree()`
    -restricts the search to this plant's descendants. This avoids accidentally
    -adding another plant's leaves.
    +`Many` gathers the selected values so the model can read them like a vector.
    +`Subtree()` searches this plant and its descendants. With `scale=:Leaf`,
    +only its leaves contribute, so another plant's leaves are not included.
     
    -For a real process chain, the producer and consumer must use compatible
    -units, physical bases, and time intervals. An average per unit leaf area
    -cannot become a plant total by an unweighted sum; supply the necessary areas
    -and write that conversion explicitly.
    +Before adding real leaf values, check that they use the same units and time
    +interval. Also check whether they describe a whole leaf or one square metre
    +of leaf area. For values per square metre, multiply each value by its leaf's
    +area before adding them to obtain a plant total.
     
    -Use `Diagnostics.explain_bindings(plant)` to inspect the selected sources.
    -The implementation uses live references, so the aggregation equation can
    -remain independent of object identity and count.
    +Use `Diagnostics.explain_bindings(plant)` to check which leaves supply the
    +values. PlantSimEngine keeps the inputs connected to those leaves' current
    +results. The equation only needs to add the values; it does not need to look
    +up individual leaves or count them.
    diff --git a/docs/src/journeys/modelers/environment_and_cadence.md b/docs/src/journeys/modelers/environment_and_cadence.md
    index a7d71106f..cfa4a30a7 100644
    --- a/docs/src/journeys/modelers/environment_and_cadence.md
    +++ b/docs/src/journeys/modelers/environment_and_cadence.md
    @@ -1,8 +1,9 @@
     # Implement Environment And Cadence Traits
     
    -A process may read air temperature from its environment and may have a
    -preferred time step. Declare those requirements beside the model; the scenario
    -chooses the data provider and can configure its execution cadence.
    +A model may need air temperature or may be intended to run once a day.
    +Describe these requirements alongside the model's equation. The simulation
    +setup then chooses where the temperature comes from and how often the model
    +runs. This update frequency is also called its **cadence**.
     
     Start with [Understand Environments](@ref) and
     [Give Models Different Cadences](@ref) for the scenario-user perspective.
    @@ -10,7 +11,7 @@ Start with [Understand Environments](@ref) and
     ## Declare an environmental input
     
     This teaching model simply copies the environmental temperature to an output.
    -It isolates the interface before introducing a biological equation.
    +It shows how to read environmental data before adding a biological equation.
     These are its actual definitions in `examples/ToySpatialEnvironment.jl`:
     
     ```@eval
    @@ -22,7 +23,8 @@ Main.DocsSources.section(
     ```
     
     `environment_inputs_` declares `T`, and `run!` reads it from
    -`environment.T`. The model does not choose a weather file or spatial provider.
    +`environment.T`. The model does not need to know whether the temperature
    +comes from a weather file or varies with position in the canopy.
     
     Test that read directly:
     
    @@ -56,9 +58,9 @@ temperature with object state.
     
     ## Give a model a default cadence
     
    -The next teaching model adds a fixed increment whenever it runs. Its source
    -declares a default of 24 simulation steps and allows consumers to hold its
    -last output between updates:
    +The next teaching model adds a fixed increment whenever it runs. By default,
    +it runs every 24 simulation steps. Other models can keep reading its latest
    +result until it runs again:
     
     ```@eval
     Main.DocsSources.section(
    @@ -67,9 +69,9 @@ Main.DocsSources.section(
     )
     ```
     
    -`timespec` gives a model default. `output_policy` gives a default interpretation
    -for consumers reading between publications. The equation still updates only
    -when the application runs.
    +`timespec` sets the default update frequency. `output_policy` describes how
    +other models read the result between updates. Here `HoldLast` tells them to
    +use the latest available value; it does not run this equation again.
     
     `ClockSpec(24.0, 1.0)` means every 24 base steps, starting at step 1. It
     corresponds to a day only when the base step is an hour. In a scenario,
    @@ -94,10 +96,11 @@ growth = final_state(daily_simulation).daily_growth
     growth
     ```
     
    -The model runs at steps 1 and 25, adding 2 each time. A consumer can override
    -`HoldLast` when averaging or accumulating values has the appropriate physical
    -meaning. See [Give Models Different Cadences](@ref) before choosing that policy.
    +The model runs at steps 1 and 25, adding 2 each time. When connecting another
    +model, you can choose to average or add results over an interval instead of
    +keeping the last value. The choice depends on what the variable represents;
    +see [Give Models Different Cadences](@ref).
     
    -Keep a model default only when it belongs to the scientific implementation.
    -Data providers, scenario-specific timing, and the choice of output history
    -remain part of the simulation configuration.
    +Give a model a default update frequency only when its equations require one.
    +Choose weather data, timing for a particular study, and results to save in
    +the simulation setup.
    diff --git a/docs/src/journeys/modelers/hard_dependencies.md b/docs/src/journeys/modelers/hard_dependencies.md
    index e893a4326..e92ae156e 100644
    --- a/docs/src/journeys/modelers/hard_dependencies.md
    +++ b/docs/src/journeys/modelers/hard_dependencies.md
    @@ -10,11 +10,12 @@ When a model only needs another model's result, use an ordinary input instead.
     
     ## Declare the model you need to call
     
    -The teaching controller below selects one leaf, tries two prescribed
    -temperatures, and finally accepts a third. It demonstrates the call mechanism;
    -it does not solve an energy-balance equation.
    +The teaching model below acts as a **controller**: it chooses when to run
    +another model. It selects one leaf, tries two prescribed temperatures, and
    +finally accepts a third. This shows how to run trials; it does not solve an
    +energy-balance equation.
     
    -Its declaration selects leaf readers by process within the current plant.
    +Its declaration asks for temperature-reading models on the current plant's leaves.
     These definitions are extracted from `examples/ToyAdvancedControl.jl`:
     
     ```@eval
    @@ -25,9 +26,10 @@ Main.DocsSources.section(
     )
     ```
     
    -`Call` declares the requirement. The controller will execute it explicitly.
    -Process and relative scope allow reuse without knowing a future scenario's
    -application names.
    +`Call` declares which models the controller needs. The controller chooses
    +when to run them. Asking for a process on the current plant's leaves lets
    +you reuse it without knowing the names a future simulation will give those
    +model applications.
     
     ## Run trials and accept one result
     
    @@ -41,13 +43,15 @@ Main.DocsSources.section(
     )
     ```
     
    -`call_targets` finds the declared targets; the object filter chooses the leaf
    -for this example. Each trial uses `publish=false`, so it does not create an
    -accepted output sample. The final call uses `publish=true`.
    +`call_targets` lists the models and objects that match the `Call` declaration.
    +Each match is called a **target**. Here the controller chooses the target for
    +one leaf. Each trial uses `publish=false`, so its result is not saved as an
    +accepted output sample. The final call uses `publish=true` to save the
    +accepted result for output history and time-based connections.
     
    -An actual iterative model must define its own trial calculation, convergence
    -criterion, and treatment of state. Publication suppression does not undo
    -arbitrary changes made by a trial.
    +For a real solver, you must decide how to calculate each trial and when a
    +solution is close enough. You must also handle any values changed by a
    +rejected trial: `publish=false` does not restore them automatically.
     
     ## Compose a small scenario
     
    @@ -85,22 +89,23 @@ accepted = final_state(simulation, :plant)
     )
     ```
     
    -The controller's default `dep` declaration supplies the call. A scenario can
    -override its selection with `ModelSpec(...; calls=...)`. Inspect
    -`Diagnostics.explain_calls(model)` to check the resolved targets.
    +The controller's `dep` declaration describes the models it calls by default.
    +To choose different models or objects in a simulation, set
    +`ModelSpec(...; calls=...)`. Use `Diagnostics.explain_calls(model)` to check
    +which models and objects were selected.
     
     ## Choose the simplest call operation
     
     | Your algorithm needs… | Use |
     |---|---|
    -| To execute every declared target | `run_call!(context, :readers)` |
    -| To inspect one resolved model's parameters or type | `call_model(context, :reader)` |
    -| To select objects, inspect target state, or use distinct trial values | `call_targets(context, :readers)`, then call the selected target |
    +| To run all selected models and objects | `run_call!(context, :readers)` |
    +| To read one called model's parameters or type | `call_model(context, :reader)` |
    +| To choose objects, read their current values, or give them different trial values | `call_targets(context, :readers)`, then call the selected target |
     
    -`call_model` requires exactly one resolved target. The bulk `run_call!`
    -path can take `sampled_environment=value` when every target uses the same
    -already-sampled environment.
    +`call_model` requires exactly one match. If you have already prepared the
    +environmental values that all selected objects should use, pass them as
    +`sampled_environment=value` to `run_call!`.
     
    -Supported structural changes refresh the selected objects. Keep ordinary
    -calls through these public operations so your controller continues to use
    -the current targets.
    +When objects are added, removed, or moved to a different parent through the
    +PlantSimEngine functions, the selection is updated. Use the call functions
    +above so your controller uses that updated selection.
    diff --git a/docs/src/journeys/modelers/mutable_environment.md b/docs/src/journeys/modelers/mutable_environment.md
    index 8c8820182..1e7a10739 100644
    --- a/docs/src/journeys/modelers/mutable_environment.md
    +++ b/docs/src/journeys/modelers/mutable_environment.md
    @@ -1,8 +1,9 @@
     # Implement A Mutable Environment Controller
     
    -Some coupled calculations also update their environment. A canopy controller,
    -for example, may evaluate a trial temperature before accepting the
    -environmental state that other processes should use.
    +Some models also change their environment. A canopy energy-balance model,
    +for example, may try several temperatures before deciding which temperature
    +other models should use. A model that manages these trials is called a
    +**controller**.
     
     This teaching example uses prescribed temperatures to explain that sequence.
     It does not implement a physical canopy solver. Start with
    @@ -12,7 +13,7 @@ It does not implement a physical canopy solver. Start with
     ## Declare the accepted values you can write
     
     A controller declares both the model it calls and the environmental variables
    -it may commit. Here `environment_outputs_` names temperature `T`. These are
    +it may update. Here `environment_outputs_` names temperature `T`. These are
     the actual declarations in `examples/ToySpatialEnvironment.jl`:
     
     ```@eval
    @@ -23,9 +24,9 @@ Main.DocsSources.section(
     )
     ```
     
    -Object outputs record what the controller observed.
    -`environment_outputs_` separately declares what it may write to the
    -environment provider.
    +The variables in `outputs_` record the temperatures seen during the
    +calculation. The variable in `environment_outputs_` is the temperature the
    +controller may change in the shared environment.
     
     ## Evaluate a trial, then commit an accepted state
     
    @@ -38,19 +39,21 @@ Main.DocsSources.section(
     )
     ```
     
    -The first call evaluates the reader against a trial environment without
    -publishing an accepted sample. `commit_environment!` writes the accepted
    -environmental values; the final reader call publishes the accepted result.
    +The first call gives the reader a trial temperature. Its result is not saved
    +as an accepted output sample. `commit_environment!` then stores the accepted
    +temperature in the shared environment. The final reader call saves the
    +result calculated at that temperature.
     
    -In a scientific controller, the trial calculation and acceptance criterion
    -belong to your algorithm. Also account for state changed during a rejected
    -trial: suppressing publication is not a general rollback operation.
    +For a scientific controller, you must choose how to calculate trials and
    +when to accept a solution. You must also handle any values changed by a
    +rejected trial: `publish=false` does not restore them automatically.
     
     ## Connect the controller to a provider
     
    -The example provider stores temperature in a named canopy cell. The reader
    -can sample it; the controller additionally receives `sink=:cells`, which
    -permits the supported write operation for this provider.
    +An **environment provider** supplies data such as temperature to the models.
    +This example stores temperature in a named canopy cell. The reader can read
    +that temperature. The controller also gets `sink=:cells`, which allows it to
    +write accepted temperatures into the provider's cells.
     
     ```@example modeler_mutable_environment
     using Test, PlantSimEngine
    @@ -88,16 +91,17 @@ state = final_state(simulation)
     )
     ```
     
    -The provider starts at 20, the trial reads 30, and the accepted value is 22.
    -Use `Diagnostics.explain_environment_bindings(model)` to inspect the provider
    -and sink, and `Diagnostics.explain_outputs(simulation)` to inspect retained
    -samples.
    +The temperature starts at 20, the trial uses 30, and the accepted value is 22.
    +Use `Diagnostics.explain_environment_bindings(model)` to check where models
    +read and write environmental data. `Diagnostics.explain_outputs(simulation)`
    +shows the saved results.
     
    -The accepted state must provide every declared environmental output. If the
    -controller is itself inside an unpublished outer trial, its descendant
    -publications and environment writes are suppressed too.
    +When accepting a solution, supply a value for every variable declared in
    +`environment_outputs_`. If another controller calls this one as a trial with
    +`publish=false`, none of its nested calls can save accepted output samples
    +or change the shared environment.
     
    -A package providing a different spatial environment implements the separate
    -[Environment Backend Extensions](@ref) interface. A process-model author
    -normally uses that provider through `Environment` and the public call and
    -commit operations shown here.
    +To add a new kind of spatial environment provider, see
    +[Environment Backend Extensions](@ref). When writing a process model, you
    +can normally use an existing provider through `Environment` and the call
    +and commit functions shown here.
    diff --git a/docs/src/journeys/users/advanced_execution.md b/docs/src/journeys/users/advanced_execution.md
    index b7db5eea3..948cad334 100644
    --- a/docs/src/journeys/users/advanced_execution.md
    +++ b/docs/src/journeys/users/advanced_execution.md
    @@ -1,17 +1,20 @@
     # Control Advanced Execution
     
    -## New concept: parent-controlled execution and explicit publication
    +## Let one model control another's calculations
     
    -Most coupling should remain a value dependency through `inputs`. Use a hard
    -call only when a parent algorithm must decide whether, when, or how often
    -another model runs—for example, while iterating toward an accepted leaf
    -temperature.
    +Use `inputs` when a model simply needs a value calculated by another model.
    +Sometimes a model also needs to control when the other model runs, or repeat
    +its calculation several times. For example, a heat-balance solver might try
    +several leaf temperatures before accepting a result. PlantSimEngine calls
    +this a **hard call**. You declare it with `calls` and run it with `run_call!`.
     
    -## Declare parent-controlled targets
    +## Choose which models the controller can call
     
    -Start with one plant and two leaves. The reader application is selected by the
    -controller's `calls` declaration, so it is call-only rather than independently
    -scheduled:
    +Start with one plant and two leaves. A controller on the plant can call a
    +temperature reader on either leaf. In this example, the readers run only when
    +the controller calls them; the simulation does not also run them separately.
    +The `Diagnostics.explain_calls` table shows which leaves the controller can
    +call:
     
     ```@example journey_advanced_execution
     using PlantSimEngine, DataFrames
    @@ -77,16 +80,18 @@ select(
     )
     ```
     
    -`run_call!(context, :readers)` executes every resolved target and returns a
    -vector-like `CallTargets` collection. `One` still returns a collection of one;
    -`OptionalOne` returns zero or one; `Many` returns zero or more.
    +`run_call!(context, :readers)` runs the reader for every selected leaf and
    +returns a `CallTargets` collection that you can loop over. Even `One` returns
    +a collection, containing one target. `OptionalOne` returns zero or one target;
    +`Many` returns zero or more.
     
    -## Inspect, select, iterate, then publish once
    +## Try several values and save the accepted result
     
    -`ToySelectiveCallControllerModel` needs different treatment per target, so its
    -kernel first calls `call_targets(context, :readers)` without executing
    -anything. It records the total, restricts the same declared call to
    -`:sun_leaf`, runs two temperature trials, and accepts one result:
    +The controller in this example wants to run only the sun leaf's reader.
    +`call_targets(context, :readers)` lists the available targets without running
    +them. The controller counts both leaves, selects `:sun_leaf`, tries two
    +temperatures, then records one accepted result. The repeated calls follow
    +this pattern:
     
     ```@example journey_advanced_execution
     function run_selected_trials!(
    @@ -109,9 +114,10 @@ function run_selected_trials!(
     end
     ```
     
    -`publish=false` is the default. Trials may update the called model's current
    -status for convergence checks, but they neither append output samples nor
    -commit mutable environment state. Publish exactly the accepted execution.
    +`publish=false` is the default. Each trial updates the called model's status,
    +so the controller can inspect it and decide whether to continue. The trial
    +does not add a result to the output history or save changes to the environment.
    +Use `publish=true` for the accepted calculation.
     
     ```@example journey_advanced_execution
     simulation = run!(model; outputs=:all)
    @@ -121,11 +127,11 @@ simulation = run!(model; outputs=:all)
     )
     ```
     
    -The controller resolved two targets but selected only `:sun_leaf`. Object
    -selection uses `call_targets(context, name; objects=(ObjectId(:sun_leaf),))`;
    -it does not depend on iteration order. Two trials
    -left no history; its accepted call published once, while `:shade_leaf` was
    -never executed:
    +The controller found two leaves but ran only `:sun_leaf`. It selects that leaf
    +by name with `call_targets(context, name; objects=(ObjectId(:sun_leaf),))`,
    +so changing the order of the leaves would not change the selection. The two
    +trials were not recorded. The accepted calculation recorded one result, and
    +the reader on `:shade_leaf` did not run:
     
     ```@example journey_advanced_execution
     filter(
    @@ -134,21 +140,28 @@ filter(
     )
     ```
     
    -Use `run_call!(context, name; environment=trial_state)` when every target
    -should sample the same provider-aware trial state through its own compiled
    -handle. If the caller has already sampled the model-facing environment, use
    -`run_call!(context, name; sampled_environment=value)` to execute all targets
    -through cached typed batches. Use `call_targets` and
    -`run_call!(target; sampled_environment=...)` only for selection, custom order,
    -status inspection, or distinct already-sampled environments. For one target,
    -`call_model(context, name)` provides allocation-free access to its concrete
    -model when an algorithm must dispatch on model type or read its parameters.
    +There are two ways to supply trial conditions:
     
    -## Order intentional duplicate writers
    +- `run_call!(context, name; environment=trial_state)` lets each target read
    +  its own local values from the trial environment. Use this when, for example,
    +  a trial canopy temperature field gives different values to different leaves.
    +- `run_call!(context, name; sampled_environment=value)` passes values you have
    +  already chosen, such as `(T=22.0,)`, directly to all targets.
     
    -One canonical variable normally has one writer. Two applications that both
    -claim `stock` therefore fail compilation unless their relationship is
    -intentional and ordered. `Updates` makes that ownership explicit:
    +Both forms run all selected targets together. Use `call_targets` followed by
    +`run_call!(target; sampled_environment=...)` when you need to choose individual
    +targets, change their order, inspect their status, or give each one different
    +values. For a call with one target, `call_model(context, name)` gives access to
    +the model itself, for example to read a parameter. It preserves the concrete
    +model type and does not allocate memory.
    +
    +## Let two models update the same value
    +
    +Normally, only one model may set a given variable on an object. If two models
    +both set `stock`, PlantSimEngine needs to know which value to keep. If the
    +second model is meant to change the first model's result, use `Updates` to
    +specify their order. Without that instruction, this configuration is rejected
    +before either model runs:
     
     ```@example journey_advanced_execution
     writer_model = CompositeModel(
    @@ -184,10 +197,10 @@ select(
     )
     ```
     
    -`initial_stock` owns the first canonical write and `adjusted_stock` explicitly
    -updates it afterward. The alternative value is useful as a retained comparison
    -but must not replace canonical status, so `output_routing` marks it
    -`:stream_only`.
    +`initial_stock` sets `stock` first, then `adjusted_stock` changes it. The third
    +model calculates an alternative value that we want to keep for comparison.
    +`output_routing=(stock=:stream_only,)` records that alternative in its own
    +output history without replacing the object's `stock` value.
     
     ```@example journey_advanced_execution
     writer_simulation = run!(writer_model; outputs=:all)
    @@ -202,18 +215,9 @@ writer_simulation = run!(writer_model; outputs=:all)
     )
     ```
     
    -The canonical result is `8`; the three application-specific streams retain
    -`4`, `8`, and `99`. A stream-only output is not a fallback writer and is not
    -selected by an application-free `OutputRequest`; request its application
    -explicitly when retaining only selected outputs.
    -
    -## Page recap
    -
    -- **You added:** one declared hard call, selective trials, one accepted
    -  publication, explicit update ordering, and one stream-only alternative.
    -- **PlantSimEngine inferred:** the call-only schedule, concrete targets,
    -  canonical writer ownership, and update edge.
    -- **You keep explicit:** when each target runs, `publish`, per-target forcing,
    -  intentional duplicate writers, and non-canonical streams.
    -- **New API names:** `calls`, `call_targets`, `run_call!`, `CallTargets`,
    -  `Updates`, `output_routing`, and `:stream_only`.
    +The object's final `stock` is `8`. The three models' output histories keep
    +their respective values: `4`, `8`, and `99`. The `:stream_only` setting never
    +lets the alternative replace the stored `stock`, even if no other model sets
    +it. If you use `OutputRequest` to save selected results, name
    +`:alternative_stock` explicitly to retain its output; requesting `stock`
    +without an application name does not select this alternative.
    diff --git a/docs/src/journeys/users/cadences.md b/docs/src/journeys/users/cadences.md
    index e8d606580..9d34960bc 100644
    --- a/docs/src/journeys/users/cadences.md
    +++ b/docs/src/journeys/users/cadences.md
    @@ -1,23 +1,25 @@
     # Give Models Different Cadences
     
    -## New concept: application clocks and temporal input policies
    +## Run models at different intervals
     
    -Let canopy development update daily while light interception responds every
    -hour. Then convert hourly water-uptake rates to daily amounts. These teaching
    -examples show how to choose what a slower or faster model receives from its
    -source.
    +Canopy development might need an update once a day, while light interception
    +needs one every hour. A model's **cadence** is how often it runs. This page
    +shows how an hourly light model uses a daily LAI value, then how to calculate
    +daily water uptake from hourly rates.
     
    -Choose a base step that divides every application cadence: hourly and
    -90-minute applications can share a 30-minute base step. A duration such as
    -`every=Minute(90)` on an hourly base step is rejected. Fixed durations include
    -subsecond periods; calendar months and adaptive timesteps are not application
    -cadences.
    +The simulation's **base step** is its smallest time interval. Each model's
    +cadence must be a whole number of base steps: hourly and 90-minute models can
    +share a 30-minute base step. PlantSimEngine rejects `every=Minute(90)` with
    +an hourly base step. You can use fixed durations, including fractions of a
    +second. Calendar months and timesteps that change during a run are not
    +supported as model cadences.
     
     ## Hold a daily state for an hourly model
     
    -Reuse the thermal-time, LAI, and light chain. The environment advances hourly;
    -thermal time and LAI run daily; light interception runs hourly. `HoldLast`
    -makes each hourly light execution read the latest published daily LAI.
    +Reuse the models that calculate thermal time, LAI, and absorbed light. The
    +weather advances hourly. Thermal time and LAI update daily, while light is
    +calculated every hour. `HoldLast` tells the light model to keep using the most
    +recent LAI value until a new one is calculated.
     
     ```@example journey_cadences
     using PlantSimEngine, Dates, DataFrames
    @@ -77,12 +79,14 @@ light_samples = rows[(rows.variable .== :aPPFD) .& in.(rows.timestep, Ref((1, 2,
     light_samples
     ```
     
    -The daily models execute at steps 1 and 25, rather than waiting until the end
    -of the first day. The first LAI value is held for hourly light calculations
    -until the next daily update. A model that needs a complete preceding day
    -must handle its initial history explicitly.
    +The daily models run at steps 1 and 25. They start at the beginning of the
    +simulation, so the first update does not wait for a whole day of weather.
    +The hourly light model uses the first LAI value until the next daily update.
    +If your equation needs a full preceding day, decide what it should do at the
    +start, before that history is available.
     
    -The schedule reports physical cadence in seconds and in base steps:
    +Check how often each model runs, in seconds (`dt_seconds`) and in base steps
    +(`dt_steps`):
     
     ```@example journey_cadences
     select(
    @@ -93,8 +97,9 @@ select(
     )
     ```
     
    -The temporal binding is explicit even though the producer and consumer share
    -an object:
    +You can also check which rule each input uses to read earlier values. The LAI
    +input uses `HoldLast` because we chose it in `inputs` above. Sharing a canopy
    +object does not choose that rule for us:
     
     ```@example journey_cadences
     select(
    @@ -107,8 +112,8 @@ select(
     )
     ```
     
    -The daily applications publish at steps 1 and 25; the hourly application
    -publishes on all 25 steps:
    +There are two results for each daily model, at steps 1 and 25. The hourly
    +model has a result at every step. The `nsamples` column counts them:
     
     ```@example journey_cadences
     select(
    @@ -119,17 +124,20 @@ select(
     )
     ```
     
    -`HoldLast` is appropriate because LAI is a state: between daily updates, its
    -latest value remains meaningful.
    +`HoldLast` fits this example because LAI describes the canopy at a given time.
    +The daily development model assumes that LAI stays unchanged between updates.
     
     ## Integrate a rate into an amount
     
    -`Integrate(reducer)` can integrate rates using sample durations. The default
    -`Integrate()` only sums values; it does not multiply by elapsed time. The
    -explicit reducer below uses durations in seconds. If a leaf publishes a constant
    -water-uptake rate in mg per second, integrating 24 hourly samples produces a
    -daily amount in mg. The plant adds the amounts from two leaves. The constant
    -rates are teaching values, not predictions of water demand.
    +To turn a water-uptake rate into an amount, multiply each rate by the time it
    +represents, then add the amounts. `Integrate(reducer)` lets you supply that
    +calculation as a function, called a **reducer**. Be careful: `Integrate()`
    +without this function only adds the values; it does not multiply by time.
    +
    +The function below uses durations in seconds. For a rate in mg per second,
    +24 hourly values give a daily amount in mg. The plant then adds the amounts
    +from its two leaves. The constant rates are teaching values, not predictions
    +of water demand.
     
     Load the [teaching models](../../guides/time/teaching_models.jl). One copies a
     leaf's supplied water-uptake rate; the other sums amounts from leaves. Their
    @@ -199,41 +207,28 @@ daily_rows = water_rows[water_rows.application_id .== :daily_amount, [:timestep,
     daily_rows
     ```
     
    -These windows use fixed durations, not calendar-aligned civil days. The
    +Each window looks back over a fixed duration; it does not automatically start
    +at midnight. The
     [weekly example](../../guides/time/hourly_daily_weekly.md) extends this same
    -calculation to seven days and checks its initial history. Use
    -`Aggregate(reducer)` instead when the desired quantity is a mean, minimum,
    -maximum, or another reduction of observations rather than a time integral.
    -
    -!!! note "Temporal policies and scientific contracts"
    -
    -    This numerical example leaves variable contracts undeclared. In this
    -    release, temporal policies do not transform `VariableContract` metadata:
    -    connected ports must still declare identical contracts. A rate contract
    -    and a total contract therefore cannot be connected directly through
    -    `Integrate()`.
    -
    -    For contracted models, put the rate-to-amount calculation in a named
    -    adapter. Its incoming binding uses the producer's rate contract and its
    -    output declares the amount contract consumed downstream. For a varying
    -    rate, accumulate at the producer cadence or use a correctly averaged
    -    rate; multiplying one held sample by a long duration is valid only when
    -    the rate is constant over that interval. Keep contracts on both sides;
    -    see [Coupling models](@ref).
    -
    -There is no same-step feedback cycle in either example, so
    -`PreviousTimeStep` is not needed. It should be introduced only when a real
    -scientific dependency intentionally reads the preceding step to break such a
    -cycle.
    -
    -## Page recap
    -
    -- **You added:** daily and hourly application clocks, `HoldLast` for a state,
    -  and then `Integrate` for a rate.
    -- **PlantSimEngine inferred:** the base-step ratios, publication schedule, and
    -  bounded temporal storage needed by the consumers.
    -- **You keep explicit:** each application cadence, the physical meaning of its
    -  temporal policy, and the integration window.
    -- **New API names:** `every`, `HoldLast`, `Integrate`, `Aggregate`,
    -  `window`, `Diagnostics.explain_schedule`, and
    -  `Diagnostics.explain_outputs`.
    +calculation to seven days and checks the first result separately. Use
    +`Aggregate(reducer)` to calculate a mean, minimum, maximum, or another
    +statistic from the values in a window.
    +
    +!!! note "When models declare units and physical meaning"
    +
    +    A `VariableContract` describes a variable's units and physical meaning.
    +    The teaching models above do not declare these contracts. If your models
    +    do declare them, the two sides of a connection must match. `Integrate()`
    +    changes the values but does not change their declared contract, so it
    +    cannot directly connect a rate contract to an amount contract.
    +
    +    Instead, write a small conversion model. Its input declares the rate
    +    contract, and its output declares the amount contract expected by the
    +    next model. If the rate changes, calculate each amount when the rate
    +    updates, or use a rate correctly averaged over the interval. Multiplying
    +    one held value by a long duration is only correct when the rate stays
    +    constant over that interval. See [Coupling models](@ref).
    +
    +Neither example needs `PreviousTimeStep`: no two models wait for each other's
    +result in the same step. Use that option only when an equation should read
    +the preceding step. See [Diagnosing Dependency Cycles](@ref) for an example.
    diff --git a/docs/src/journeys/users/environments.md b/docs/src/journeys/users/environments.md
    index 10bcd54e0..470506b34 100644
    --- a/docs/src/journeys/users/environments.md
    +++ b/docs/src/journeys/users/environments.md
    @@ -1,17 +1,18 @@
     # Understand Environments
     
    -## New concept: declared sampling from global and spatial sources
    +## Give models weather and light
     
    -Use weather whose column names differ from those expected by the models,
    -then give two canopies different radiation supplies. An **environment**
    -provides external values such as air temperature and incident light.
    -This page uses teaching models and illustrative forcing.
    +An **environment** supplies values such as air temperature and incident light.
    +Here you will first use weather data whose column names differ from the names
    +your models expect. Then you will give a sunny canopy more light than a shaded
    +one. The models and weather values are chosen to explain these operations.
     
    -## Global sampling and source names
    +## Match weather names to model inputs
     
    -The source does not need to use the model-facing names. Here a global provider
    -has `air_temperature` and `incident_par`; `Environment(...; sources=...)`
    -remaps them for the two model applications.
    +Suppose your weather data use the names `air_temperature` and `incident_par`,
    +but your models expect `T` and `Ri_PAR_f`. Use `sources` in `Environment(...)`
    +to say which weather variable supplies each model input. `provider=:global`
    +means that the weather values do not depend on the object's position.
     
     ```@example journey_environments
     using PlantSimEngine, Dates, DataFrames
    @@ -81,11 +82,11 @@ using PlantSimEngine.Examples
     ```
     
     `ToyDegreeDaysCumulModel` reads `environment.T`; `Beer` reads
    -`environment.Ri_PAR_f`. These are not status inputs and are not outputs owned
    -by the target object.
    +`environment.Ri_PAR_f`. These values come from the weather data. They are
    +separate from values stored on the canopy, such as its LAI.
     
    -The environment diagnostic distinguishes the variables seen by each model from
    -the actual source names:
    +The table below shows the names each model expects (`required_inputs`) and the
    +names it reads from the weather data (`source_inputs`):
     
     ```@example journey_environments
     select(
    @@ -98,18 +99,19 @@ select(
     )
     ```
     
    -Global sampling has no spatial handle. The forcing above is intentionally
    -strict: apart from timeline `duration`, it exposes only the two remapped source
    -variables. Removing either source makes `validate_environment_inputs` fail
    -before simulation.
    +The `handle` column is empty because neither model needs a spatial location
    +to read this weather. The example supplies only `duration` and the two weather
    +variables. If either weather variable is missing,
    +`validate_environment_inputs` reports it before you run the simulation.
     
    -## Spatial sampling
    +## Give each canopy its own light
     
    -Spatial backends keep the same model-facing declaration. They additionally
    -associate each object with its source location. The small
    -`ToySpatialEnvironment` example maps a cell label to either a sunny or shaded
    -canopy. This label-based fixture demonstrates sampling; it does not compute
    -light transport from a geometric scene:
    +When weather or light varies across space, an **environment backend** supplies
    +the values at each object's location. The models still read the same input
    +names. The small `ToySpatialEnvironment` example below stores a light value in
    +each of two cells, named `:sun` and `:shade`. Each canopy's `geometry` says
    +which cell to use. These light values are supplied directly; this example does
    +not calculate how light travels through a canopy.
     
     ```@example journey_environments
     spatial_environment = ToySpatialEnvironment(
    @@ -154,8 +156,9 @@ Both canopies have LAI = 2 m² m⁻². The sunny canopy receives four times the
     incident PAR of the shaded canopy, so its absorbed PAR is also four times
     as large. Both results use ground area, not individual leaf area.
     
    -The optional diagnostic below shows two different cached source locations
    -(called handles), one for each canopy:
    +The table below lets you check that each canopy reads from its intended cell.
    +PlantSimEngine stores this location in a value called a **handle**, so it does
    +not need to find the cell again at every step:
     
     ```@example journey_environments
     select(
    @@ -167,19 +170,6 @@ select(
     )
     ```
     
    -The scientific `Beer` kernel is unchanged. It sees only
    -`environment.Ri_PAR_f`; the backend owns the meaning of each handle. Backend
    -authors can inspect the implementation of `ToySpatialEnvironment` in the
    -environment extension reference.
    -
    -## Page recap
    -
    -- **You added:** explicit environment declarations, global source remapping,
    -  and then a spatial backend with object geometry.
    -- **PlantSimEngine inferred:** global sampling, validation of required source
    -  names, and one cached spatial handle per application/object target.
    -- **You keep explicit:** model-facing environment names, scenario source
    -  remaps, provider/backend choice, and geometry used by a spatial backend.
    -- **New API names:** `environment_inputs_`, `Environment`,
    -  `validate_environment_inputs`, `ToySpatialEnvironment`, and
    -  `Diagnostics.explain_environment_bindings`.
    +The `Beer` equation is unchanged. It reads `environment.Ri_PAR_f` in both
    +examples; the environment supplies the correct value. To provide your own
    +spatial data, see [Environment Backend Extensions](@ref).
    diff --git a/docs/src/journeys/users/maespa_synthesis.md b/docs/src/journeys/users/maespa_synthesis.md
    index c4b33d3a9..029df0919 100644
    --- a/docs/src/journeys/users/maespa_synthesis.md
    +++ b/docs/src/journeys/users/maespa_synthesis.md
    @@ -1,28 +1,27 @@
     # MAESPA-Style Synthesis
     
    -## New concept: synthesis without another runtime mechanism
    -
    -This page is an integrated reference, not an onboarding example. It combines
    -the ideas developed independently in the earlier journeys into a small
    -MAESPA-style stand: two species, five leaves, hourly canopy and soil exchange,
    -daily allocation and LAI, iterative leaf calls, and accepted mutable canopy
    -air.
    -
    -This is a non-calibrated teaching example inspired by MAESPA's process
    +Bring the earlier tutorials together in a small MAESPA-style stand with two
    +species and five leaves. Canopy and soil exchange run hourly; carbon
    +allocation and LAI run daily. Within each hour, a controller repeatedly runs
    +the leaf models to find a consistent canopy air temperature and humidity.
    +This is an advanced example; start with the individual tutorials if these
    +steps are unfamiliar.
    +
    +This is an uncalibrated teaching example inspired by MAESPA's process
     structure. It is not a validated MAESPA implementation. Leaf illumination is
     uniform with complete absorption, the canopy has one air layer, and the soil
     water model uses prescribed withdrawals and bounds rather than a complete
     soil hydraulic balance. The example tests coupling and carbon accounting;
    -its trajectories should not be used as empirical predictions.
    +its results should not be used to predict a real stand's behaviour.
     
    -If any individual mechanism is unfamiliar, follow its focused link in
    -[How the pieces compose](@ref) before reading the implementation.
    +The table in [How the pieces compose](@ref) links each part of this example
    +to the tutorial that explains it.
     
     ## Run the reference case
     
    -The complete, tested source lives in
    -`examples/maespa_model_example.jl`. Supply 25 hourly forcing rows so the daily
    -scheduler runs at its initial boundary and again 24 base steps later:
    +The complete, tested source is in `examples/maespa_model_example.jl`. Run it
    +with 25 hourly weather records. The daily models run on the first step and
    +again 24 hours later, so this is long enough to see two calls:
     
     ```@example journey_maespa_synthesis
     using PlantSimEngine, DataFrames
    @@ -39,8 +38,9 @@ model = result.model
     nothing
     ```
     
    -The model contains one scene, one soil object, and two template instances with
    -different species parameters and leaf counts:
    +The model contains one object for the whole scene, one soil object, and two
    +plants built from the same template. The plants have different species
    +parameters and numbers of leaves:
     
     ```@example journey_maespa_synthesis
     (
    @@ -52,11 +52,13 @@ different species parameters and leaf counts:
     )
     ```
     
    -## Inspect the compiled architecture
    +## Check when models run and where their inputs come from
     
    -The execution schedule makes the two cadences and parent-controlled
    -applications visible. Leaf energy balance and soil water are call-only under
    -the scene controller; allocation and LAI run daily:
    +Check when each calculation runs. The scene controller decides when to call
    +leaf energy balance and soil water; these models do not run independently.
    +Allocation and LAI run once per day. In the table, `root_scheduled` means a
    +model runs directly from the simulation schedule, `manual_call_only` means
    +another model calls it, and `dt_steps` gives the interval in hourly steps:
     
     ```@example journey_maespa_synthesis
     schedule = DataFrame(Diagnostics.explain_schedule(result.compiled))
    @@ -79,8 +81,10 @@ select(
     )
     ```
     
    -The scene energy-balance application resolves all five leaves through one
    -`Many` hard call and the soil through one `One` hard call:
    +The scene energy-balance model calls all five leaf models with `Many` and the
    +single soil model with `One`. These are **hard calls**: the calling model
    +controls when the other models run. The table lists the models and objects
    +called from `:scene_eb`:
     
     ```@example journey_maespa_synthesis
     calls = DataFrame(Diagnostics.explain_calls(result.compiled))
    @@ -93,9 +97,11 @@ select(
     )
     ```
     
    -Each plant allocation application receives a live vector of only its own
    -descendant leaves. The scene receives stand-wide vectors and one scalar soil
    -potential:
    +Each plant's allocation model reads carbon values from its own leaves. The
    +scene model reads areas from all leaves and water potential from the soil.
    +Inspect `source_ids` below to check where each input comes from. The values
    +are shared by reference, meaning the reader sees the current source values
    +without copying them:
     
     ```@example journey_maespa_synthesis
     bindings = DataFrame(Diagnostics.explain_bindings(result.compiled))
    @@ -122,11 +128,17 @@ select(
     
     ## Follow trial canopy air to its accepted state
     
    -The scene controller reads above-canopy `:forcing`, iterates leaf models
    -against typed trial canopy air with `publish=false`, commits the converged
    -state to `sink=:canopy`, and then publishes one accepted leaf execution. Leaf
    -applications read the committed `:canopy` provider. Those routes are compiled
    -into opaque handles:
    +The scene controller starts from the above-canopy weather, named `:forcing`.
    +It tries different canopy air conditions and runs the leaf models for each
    +trial with `publish=false`, so these intermediate results are not saved in
    +the output history. Once the calculation converges, it commits the accepted
    +air conditions with `sink=:canopy` and runs the leaves once more to publish
    +their accepted results.
    +
    +The leaf models then read the accepted conditions from `:canopy`. The table
    +below shows which environmental variables each model reads or changes. Its
    +`handle` column contains an internal identifier used to retrieve those
    +conditions; you do not need to interpret that identifier:
     
     ```@example journey_maespa_synthesis
     environment_bindings = DataFrame(
    @@ -149,16 +161,17 @@ select(
     )
     ```
     
    -`MaespaSingleLayerEnvironment` is intentionally a one-layer canopy backend.
    -Its handle still separates forcing, canopy, and commit-sink routes per
    -application/object. A voxel or multilayer backend can replace it without
    -changing the model-facing environment contract; the two-cell proof is in
    -[Modify The Environment](@ref), and backend implementation belongs in
    +`MaespaSingleLayerEnvironment` supplies one set of air conditions for the
    +whole canopy. It keeps the above-canopy weather separate from the canopy
    +conditions that the controller changes. You could replace it with an
    +environment that represents several layers or 3D cells, while keeping the
    +same variables available to the process models. See the two-cell example in
    +[Modify The Environment](@ref) and the implementation guide in
     [Environment Backend Extensions](@ref).
     
    -## Check the scientific handoffs
    +## Check units and carbon accounting
     
    -The example keeps the conversions visible:
    +Check the units and conversions as values pass between models:
     
     | Quantity | Meaning and units |
     |---|---|
    @@ -166,7 +179,7 @@ The example keeps the conversions visible:
     | Leaf `aPPFD` | Absorbed photon flux in µmol photons m[leaf]⁻² s⁻¹: `Ri_PAR_f * constants.J_to_umol`, assuming uniform illumination and complete absorption. |
     | Leaf `A` | Net CO₂ assimilation in µmol CO₂ m[leaf]⁻² s⁻¹. |
     | Leaf `leaf_carbon` | Cumulative net assimilation in g elemental C: sum of `A * leaf_area * duration_seconds * 12e-6`. |
    -| Plant `daily_growth` | Net C since this plant's previous allocation, in g C per allocation interval; the initial scheduler call is a startup interval. |
    +| Plant `daily_growth` | Net C since this plant's previous allocation, in g C per allocation interval. The first call covers only the start of the simulation, not a full day. |
     | Plant carbon pools | Allocated g elemental C, not g dry matter; the unassigned allocation fraction remains in `reserve_pool`. |
     | `scene_transpiration` | Accepted water loss in mm over the current hourly forcing interval. |
     
    @@ -174,7 +187,8 @@ Every leaf receives the same above-canopy irradiance here. There is no
     shading, scattering, or leaf-angle calculation; a radiation model would need
     to replace that assumption for a realistic stand.
     
    -The final snapshots expose state independently of retained history:
    +Use `final_state` to read the latest values, whether or not you saved their
    +history:
     
     ```@example journey_maespa_synthesis
     scene = final_state(simulation, :model)
    @@ -196,8 +210,8 @@ plants = final_state(simulation, Many(scale=:Plant))
     Allocation reads cumulative leaf C without resetting it. Each plant stores
     the cumulative amount it has already accounted for and allocates only the
     difference at the next daily call. This prevents the same carbon being
    -allocated again on later days. Between allocation calls, a new signed carbon
    -increment can remain pending:
    +allocated again on later days. Carbon gained or lost since the last
    +allocation remains pending until the next one:
     
     ```@example journey_maespa_synthesis
     DataFrame([
    @@ -213,13 +227,13 @@ DataFrame([
     ```
     
     At each allocation, the three pools sum to `accounted_carbon`. Adding pending
    -C recovers cumulative net assimilation. These are signed carbon accounts:
    -negative net assimilation reduces them. The example does not model initial
    +C recovers cumulative net assimilation. These accounts can increase or
    +decrease: negative net assimilation reduces them. The example does not model initial
     biomass, construction respiration, dry-matter conversion, or limits on
     withdrawing reserves, so the pools are not predictions of organ mass.
     
    -Retained output counts confirm the cadence boundary: hourly scene and leaf
    -variables have 25 samples, while daily LAI and allocation variables have two:
    +Count the saved values to check how often the models ran. Hourly scene and
    +leaf variables have 25 samples; daily LAI and allocation variables have two:
     
     ```@example journey_maespa_synthesis
     output_summary = DataFrame(Diagnostics.explain_outputs(simulation))
    @@ -246,49 +260,32 @@ select(
     
     ## How the pieces compose
     
    -| Construct in this synthesis | Role here | Focused journey |
    +| Part of the example | What it does here | Tutorial |
     |---|---|---|
    -| `CompositeModelTemplate` and two `ObjectInstance`s | Reuse one species-specific application set across several plants | [Instantiate Several Plants](@ref) |
    -| Scene, plant, internode, leaf, and soil objects | Represent one registry without prescribing plant architecture | [Build One Multiscale Plant](@ref) |
    -| Scalar and `Many` live-reference bindings | Couple soil-to-scene and leaf-to-plant/scene values | [Build One Multiscale Plant](@ref) |
    -| Hourly and daily applications with `HoldLast` | Keep canopy exchange and allocation on scientific cadences | [Give Models Different Cadences](@ref) |
    -| Forcing and canopy providers with compiled handles | Sample global forcing and committed canopy state through one contract | [Understand Environments](@ref) |
    -| Typed trials and explicit accepted commit | Iterate canopy air without publishing rejected states | [Modify The Environment](@ref) |
    -| Nested hard calls and accepted publication | Let scene energy balance control leaf and soil execution | [Control Advanced Execution](@ref) |
    -| `Simulation`, final state, and retained streams | Separate current canonical state from requested history | [Couple Models On One Object](@ref) |
    -
    -This 25-hour reference keeps plant topology fixed because organogenesis is not
    -part of its scientific question. A growth model can add, reparent, or remove
    -organs through the same registry and refresh machinery; that independent
    -lifecycle is demonstrated in [Modify Plant Structure](@ref). Keeping it out of
    -this synthesis prevents canopy iteration, daily allocation, and topology
    -mutation from becoming one inseparable example.
    -
    -## Reference invariants
    -
    -The automated example test verifies the important handoffs rather than exact
    -floating-point trajectories:
    -
    -- the stand contains two isolated instances and five correctly routed leaves;
    -- plant allocation vectors contain only descendant leaves;
    -- the scene call resolves every leaf and the shared soil application;
    -- hourly and daily output counts match their cadences;
    -- accepted canopy air is committed separately from above-canopy forcing;
    -- leaf fluxes are finite and aggregate consistently at scene scale;
    +| `CompositeModelTemplate` and two `ObjectInstance`s | Reuse the same models for plants with different species parameters | [Instantiate Several Plants](@ref) |
    +| Scene, plant, internode, leaf, and soil objects | Represent the chosen plant structure and shared soil | [Build One Multiscale Plant](@ref) |
    +| `One` and `Many` inputs | Read one soil value or a collection of leaf values | [Build One Multiscale Plant](@ref) |
    +| Hourly and daily models with `HoldLast` | Keep using the last daily value between daily calculations | [Give Models Different Cadences](@ref) |
    +| Above-canopy weather and canopy conditions | Supply each model with the environment it needs | [Understand Environments](@ref) |
    +| Trial air conditions and an accepted result | Find consistent canopy conditions, then save the accepted result | [Modify The Environment](@ref) |
    +| Hard calls | Let scene energy balance decide when leaf and soil models run | [Control Advanced Execution](@ref) |
    +| `Simulation`, `final_state`, and saved outputs | Read current values and analyse changes over time | [Couple Models On One Object](@ref) |
    +
    +Plant structure stays fixed during this 25-hour example. To add or remove
    +organs with a growth model, follow [Modify Plant Structure](@ref).
    +
    +## What the tests check
    +
    +The automated tests check that the parts work together as intended:
    +
    +- the stand contains two plants, with five leaves assigned to the right plant;
    +- each plant's allocation model reads only its own leaves;
    +- the scene controller calls every leaf and the shared soil model;
    +- hourly and daily output counts match their scheduled intervals;
    +- accepting new canopy conditions does not replace the above-canopy weather;
    +- leaf fluxes are finite and their totals agree with the scene results;
     - PAR energy is bounded by shortwave energy and converted to photon units;
    -- 73 accepted hourly samples span three daily intervals and an initial
    -  scheduler call, with no carbon added by rejected leaf iterations;
    +- a longer run with 73 hourly samples covers three daily intervals plus the
    +  initial call, with no carbon added by rejected trial calculations;
     - each plant allocates every C increment once, preserves cumulative leaf C,
       and conserves the sum of its leaf, wood, reserve, and pending C accounts.
    -
    -## Page recap
    -
    -- **You added:** no new primitive; you assembled the earlier topology,
    -  cadence, hard-call, environment, and output mechanisms into one stand.
    -- **PlantSimEngine inferred:** application order, plant-local bindings,
    -  concrete call targets, environment handles, and the hourly/daily schedule.
    -- **You keep explicit:** species parameters, topology, scientific iteration,
    -  accepted state, output requests, and the invariants used to validate the
    -  result.
    -- **New API names:** none. Every API in this synthesis was introduced on a
    -  focused earlier journey.
    diff --git a/docs/src/journeys/users/mental_model.md b/docs/src/journeys/users/mental_model.md
    index ffec0a6b2..fa80d89ca 100644
    --- a/docs/src/journeys/users/mental_model.md
    +++ b/docs/src/journeys/users/mental_model.md
    @@ -1,55 +1,63 @@
     # A Mental Model For PlantSimEngine
     
    -## New concept: composition
    -
    -PlantSimEngine is a framework for composing and running scientific models. It
    -does not prescribe a plant architecture, and it is not itself a library of
    -crop, tree, or organ equations. Model packages provide those equations;
    -PlantSimEngine connects them to simulated entities, orders their execution, and
    -manages time, environments, and retained results.
    -
    -Seven ideas are enough to read a PlantSimEngine simulation:
    -
    -| Idea | Meaning |
    -|:--|:--|
    -| **Process** | A scientific responsibility, such as thermal time or light interception |
    -| **Model** | One reusable implementation of a process |
    -| **Application** | One configured use of a model in a simulation |
    -| **Object** | A simulated entity with stable identity, such as a scene, plant, leaf, or soil layer |
    -| **Status** | The current values owned by an object |
    -| **Environment** | Values sampled from outside object status, such as weather or microclimate |
    -| **Simulation** | A running timeline, including the live model, current step, and any retained output streams |
    -
    -The distinction between a model and an application is important. A model
    -author writes an equation once. A simulation author can then apply it to one
    -whole plant, every leaf, selected soil layers, or several named groups without
    -putting an object loop inside the equation.
    -
    -Objects are equally general. Scales and parent/child links describe the
    -topology chosen by the simulation author; PlantSimEngine does not require a
    -particular hierarchy. A simple simulation can have one object. A detailed one
    -can have scenes, several plants, organs, voxels, and shared resources.
    -
    -Values reach a model in two ways:
    -
    -- status inputs come from the target object or from outputs of other model
    -  applications;
    -- environment inputs are sampled from the environment selected for that
    -  application and object.
    -
    -Before running, PlantSimEngine compiles applications into concrete
    -application/object targets, resolves value connections, and determines a valid
    -execution order. During the timestep loop, model kernels work with their own
    -parameters, the resolved status view, the sampled environment, constants, and
    -a runtime context.
    -
    -## Page recap
    -
    -- **You added:** no configuration yet—only the vocabulary used by every later
    -  journey.
    -- **PlantSimEngine infers:** application order and unambiguous value
    -  connections once a simulation is assembled.
    -- **You keep explicit:** scientific equations, object topology, ambiguous
    -  cross-object connections, time policies, and requested output history.
    -- **New API names:** none yet. The next page introduces `CompositeModel`,
    -  `run!`, `Simulation`, `final_state`, and `collect_outputs`.
    +PlantSimEngine connects scientific models and runs them together. For example,
    +one model can calculate the light absorbed by a canopy, and another can use
    +that light to calculate biomass production. You choose the equations and the
    +parts of the plant or environment they describe.
    +
    +This page explains the terms used in the guides. If you have not run an
    +example yet, start with [your first simulation](one_object.md).
    +
    +## Models describe calculations; objects describe what is simulated
    +
    +A **process** is something you want to calculate, such as photosynthesis or
    +light interception. A **model** provides the equations for that process.
    +Two models for the same process can use different equations or assumptions.
    +
    +An **object** represents something in your simulation: a leaf, a plant, a
    +canopy, or a soil layer, for example. Each object has a **status**, which
    +stores its current values, such as leaf area or water content.
    +
    +You decide how much detail to represent. A canopy can be one object, or it
    +can contain several plants with individual leaves. PlantSimEngine does not
    +require a particular plant structure.
    +
    +## An application says where and how to use a model
    +
    +A **model application** combines a model with instructions about where to run
    +it, how often to run it, and where to get its inputs. For example, you could
    +apply one photosynthesis model to every leaf and run it once an hour.
    +
    +You write the photosynthesis equation once. PlantSimEngine then runs it for
    +each selected leaf, using that leaf's own values and local conditions. You
    +can also apply different models to different groups of leaves or plants.
    +
    +In the [first simulation](one_object.md), all models describe the same canopy,
    +so `CompositeModel` creates these applications for you. Later guides use
    +`ModelSpec` to choose objects and connections explicitly.
    +
    +## Inputs come from object values or the environment
    +
    +A model needs **inputs** to calculate its **outputs**. For example, a
    +light-interception model reads LAI and calculates absorbed light. Another
    +model can then use that absorbed light as an input.
    +
    +Weather and local growing conditions are supplied through an **environment**.
    +This can be a weather table shared by the whole simulation, or a source of
    +local conditions that differ between leaves or soil layers.
    +
    +Before the simulation starts, PlantSimEngine checks where each input will
    +come from and puts the calculations in order. If a biomass model needs the
    +light model's result, the light model runs first. You can inspect these
    +connections in the [graph viewer](../../guides/graph_visualizer_editor.md).
    +
    +## A simulation keeps track of progress and results
    +
    +Calling `run!` starts a **simulation**. It keeps track of the current time
    +step and the current values for every object. It also saves the history of
    +the outputs you requested.
    +
    +Use `final_state` to read the latest values, `collect_outputs` to make a table
    +of saved results, and `step!` or `continue!` to advance the same simulation.
    +The [results guide](../../guides/data/outputs_plotting.md) shows how to select
    +and plot those outputs.
    diff --git a/docs/src/journeys/users/mutable_environments.md b/docs/src/journeys/users/mutable_environments.md
    index 6840a6214..7fce0d452 100644
    --- a/docs/src/journeys/users/mutable_environments.md
    +++ b/docs/src/journeys/users/mutable_environments.md
    @@ -1,14 +1,21 @@
     # Modify The Environment
     
    -## New concept: trial state versus accepted state
    +## Try a value before keeping it
     
    -The previous environment journey sampled read-only global and spatial values.
    -Now a controller evaluates one typed trial state, accepts a different state,
    -and commits it explicitly.
    +Some models need to try several temperatures before choosing a solution.
    +For example, a model might adjust canopy air temperature until its heat
    +balance is close enough to zero. A **controller** is a model that manages
    +these repeated calculations.
     
    -Start with one cell. `ToyEnvironmentReaderModel` declares `T` as an environment
    -input. The controller declares `T` as an environment output: this is permission
    -to commit that variable, not an ordinary object-status output.
    +This teaching example shows how to try one temperature, then keep a different
    +one. It does not solve a heat balance: the trial and final temperatures are
    +chosen in advance. **Committing** the final value means writing it back to
    +the environment so that later calculations can use it.
    +
    +Start with one spatial cell. `ToyEnvironmentReaderModel` reads temperature
    +`T` from the environment. The controller declares `T` with
    +`environment_outputs_` to say that it may change this environment variable.
    +This declaration does not add `T` to the leaf's stored status.
     
     ```@example journey_mutable_environment
     using PlantSimEngine, DataFrames
    @@ -24,9 +31,10 @@ using PlantSimEngine.Examples
     )
     ```
     
    -The controller's kernel uses the current typed trial-state path. A trial call
    -changes the callee status for inspection but does not publish output history or
    -commit backend state:
    +The first function asks the reader to calculate a result using a trial
    +environment. You can inspect that result in the reader's status.
    +`publish=false` prevents it from being added to the output history, and this
    +call does not change the stored environment:
     
     ```@example journey_mutable_environment
     function run_trial!(context, trial_environment)
    @@ -39,7 +47,9 @@ function run_trial!(context, trial_environment)
     end
     ```
     
    -Accepted state is explicit and separate:
    +After choosing a final value, save it in the environment with
    +`commit_environment!`. Then run the reader with `publish=true` to record
    +its accepted result:
     
     ```@example journey_mutable_environment
     function commit_and_publish!(context, accepted_environment)
    @@ -53,9 +63,10 @@ function commit_and_publish!(context, accepted_environment)
     end
     ```
     
    -`ToyEnvironmentControllerModel` applies those two operations in its kernel.
    -Configure the reader as its one hard-call target and give only the controller a
    -commit sink:
    +`ToyEnvironmentControllerModel` performs these two operations in its `run!`
    +function. The `calls` setting below lets it run the reader. Only the controller
    +has `sink=:cells`, which tells the environment where to save the accepted
    +temperature:
     
     ```@example journey_mutable_environment
     environment = ToySpatialEnvironment(
    @@ -104,8 +115,9 @@ state = final_state(simulation)
     )
     ```
     
    -The trial was `30`, but the accepted and committed temperature is `22`.
    -Only the accepted reader call published:
    +The reader tried 30 °C, then used the accepted value of 22 °C. The environment
    +now stores 22 °C. The table below shows that the reader recorded only its
    +accepted result:
     
     ```@example journey_mutable_environment
     select(
    @@ -116,10 +128,11 @@ select(
     )
     ```
     
    -## Preserve distinct handles under `Many`
    +## Give each leaf its own local conditions
     
    -Extend the same backend to two spatial cells. One `Many` application samples
    -both, while each target retains its own compiled handle:
    +Now give one leaf a sunny cell and the other a shaded cell. `Many(scale=:Leaf)`
    +runs the same reader on both leaves, but each leaf reads its own cell's
    +temperature:
     
     ```@example journey_mutable_environment
     spatial_environment = ToySpatialEnvironment(
    @@ -156,6 +169,9 @@ spatial_states = final_state(spatial_simulation, Many(scale=:Leaf))
     Dict(id => state.temperature_seen for (id, state) in spatial_states)
     ```
     
    +The sun leaf reads 26 °C and the shade leaf reads 18 °C. A **handle** stores
    +the cell used by each leaf. You can check that the two handles are different:
    +
     ```@example journey_mutable_environment
     select(
         DataFrame(Diagnostics.explain_environment_bindings(spatial_model)),
    @@ -164,18 +180,7 @@ select(
     )
     ```
     
    -This ordinary user page does not require the backend implementation protocol.
    -Framework builders can follow [Environment Backend Extensions](@ref); the
    -complete MAESPA-style synthesis later combines mutable microclimate, several
    -plants, and iterative leaf calls.
    -
    -## Page recap
    -
    -- **You added:** one typed trial, one explicit accepted commit, one accepted
    -  publication, and then two spatial cells.
    -- **PlantSimEngine inferred:** the reader call target, publication boundary,
    -  commit permission check, and distinct `Many` handles.
    -- **You keep explicit:** trial and acceptance logic, `publish`, the committed
    -  variables, controller sink, and backend state type.
    -- **New API names:** `environment_outputs_`, `run_call!`,
    -  `commit_environment!`, `publish`, and `calls`.
    +For a larger example that adjusts canopy air conditions and repeats leaf
    +calculations across several plants, see [MAESPA-Style Synthesis](@ref).
    +To connect your own source of environmental data, see
    +[Environment Backend Extensions](@ref).
    diff --git a/docs/src/journeys/users/one_object.md b/docs/src/journeys/users/one_object.md
    index 991c7f640..9ead09721 100644
    --- a/docs/src/journeys/users/one_object.md
    +++ b/docs/src/journeys/users/one_object.md
    @@ -1,23 +1,31 @@
     # Couple Models On One Object
     
    -## New concept: automatic same-object coupling over time
    +In this example, we combine three models to calculate how a canopy develops
    +and absorbs light over 30 days. We represent the whole canopy as one
    +**object**: one part of the simulated system, with its own values. We do not
    +describe individual leaves here.
     
    -This teaching example couples three existing models on one simulated entity,
    -called an **object**. Here, that object represents a canopy without describing
    -individual organs. A **process** is a scientific calculation, such as thermal
    -time or light interception; a model implements its equations. The toy models
    -below demonstrate coupling and are not a calibrated crop model:
    +The three models pass values to one another:
     
    -1. `ToyDegreeDaysCumulModel` reads temperature and accumulates thermal time.
    -2. `ToyLAIModel` reads cumulative thermal time and computes LAI.
    -3. `Beer` reads LAI and radiation and computes absorbed PAR.
    +1. `ToyDegreeDaysCumulModel` uses temperature to calculate cumulative thermal
    +   time, a measure of accumulated warmth.
    +2. `ToyLAIModel` uses that thermal time to calculate leaf area index (LAI).
    +3. `Beer` uses LAI and incoming radiation to calculate the light absorbed by the canopy.
    +
    +These are teaching models with illustrative parameters. They show how to
    +connect calculations; their results are not predictions for a particular crop.
     
     Start with the [tutorial installation](../../prerequisites/installing_plantsimengine.md)
     if these packages are not yet available in your Julia project.
     
    -The weather file is supplied forcing data for now. Environments get their own
    -journey later. Its radiation columns contain daily totals in MJ m⁻² d⁻¹;
    -we convert them to mean fluxes in W m⁻², as required by `Beer`.
    +## Prepare the weather data
    +
    +We use a weather file included with PlantSimEngine. Each row describes one day.
    +The file records daily radiation totals in MJ m⁻² d⁻¹. `Beer` needs the average
    +radiation during that day in W m⁻², so the code below converts those columns.
    +`SW` means shortwave radiation, `PAR` is the light used for photosynthesis,
    +and `NIR` means near-infrared radiation. The weather data are called the
    +**environment** in this simulation.
     
     ```@example journey_one_object
     using PlantSimEngine, PlantMeteo, Dates, DataFrames
    @@ -30,7 +38,15 @@ weather = read_weather(
         :Ri_NIR_f => (x -> x .* 1e6 ./ 86_400) => :Ri_NIR_f;
         duration=Day,
     )
    +nothing # hide
    +
    +```
     
    +## Connect the models
    +
    +Put the three models together in a `CompositeModel` and give it the weather data:
    +
    +```@example journey_one_object
     model = CompositeModel(
         ToyDegreeDaysCumulModel(),
         ToyLAIModel(),
    @@ -39,32 +55,37 @@ model = CompositeModel(
     )
     ```
     
    -No `ModelSpec` or selector is needed when all models run on the one object made
    -by the concise constructor.
    +PlantSimEngine connects these models automatically. The thermal-time model
    +provides `TT_cu` to the LAI model, which provides `LAI` to `Beer`. Each input
    +has the same name as an output from exactly one other model on this canopy.
    +PlantSimEngine also runs the models in that order, so each calculation can
    +use the result it needs.
    +
    +## Run 30 days and look at the results
     
    -Run the first thirty daily steps and retain the model outputs. This short
    -winter window is useful for learning how to run and continue a simulation;
    -thermal time accumulates slowly and LAI stays small. The
    -[homepage](../../index.md) and [plotting guide](../../guides/data/outputs_plotting.md)
    -show longer or more varied runs.
    +`run!` starts the simulation. Here, `steps=30` runs the first 30 weather rows,
    +and `outputs=:all` saves the results from every model at each step.
    +`collect_outputs(simulation; sink=DataFrame)` gathers those saved results
    +into a DataFrame, the table type provided by DataFrames.jl. `DataFrame` is
    +already the default, so you can also write `collect_outputs(simulation)`.
    +
    +These first 30 days fall in winter, so thermal time increases slowly and LAI
    +stays small. The [plotting guide](../../guides/data/outputs_plotting.md) shows
    +how to plot results and compare two canopies.
     
     ```@example journey_one_object
     simulation = run!(model; steps=30, outputs=:all)
    -results = collect_outputs(simulation)
    -
    -thermal_time = results[results.variable .== :TT_cu, :value]
    -lai = results[results.variable .== :LAI, :value]
    -evolution = DataFrame(
    -    step=1:length(thermal_time),
    -    TT_cu=thermal_time,
    -    LAI=lai,
    -)
    -vcat(first(evolution, 3), last(evolution, 3))
    +results = collect_outputs(simulation; sink=DataFrame)
    +first(select(results, :timestep, :variable, :value), 8)
     ```
     
    -`TT_cu` is cumulative thermal time in °C d; LAI is leaf area per ground area
    -in m² m⁻². The table is retained history. The latest values are also available directly,
    -whether or not history was requested:
    +Each row records one variable on one day. The last line selects three
    +columns and shows the first eight rows: four variables for each of the first
    +two days. `TT` is daily thermal time and `TT_cu` is cumulative thermal time,
    +both in °C d. LAI is leaf area per ground area, in m² m⁻².
    +
    +`final_state` gives the latest values, here at the end of day 30. It is also
    +available when you run a simulation without saving its history:
     
     ```@example journey_one_object
     state_at_day_30 = final_state(simulation)
    @@ -73,29 +94,17 @@ state_at_day_30 = final_state(simulation)
         TT_cu=state_at_day_30.TT_cu,
         LAI=state_at_day_30.LAI,
         aPPFD=state_at_day_30.aPPFD,
    -    retained_streams=length(outputs(simulation)),
     )
     ```
     
    -`aPPFD` is absorbed PAR in μmol m⁻² of ground s⁻¹, averaged over the daily
    -forcing interval. It is not a flux per unit leaf area.
    -
    -PlantSimEngine inferred both status connections because each has one
    -unambiguous producer on the same object. This focused diagnostic shows the
    -resolved sources and the live reference carriers:
    +`aPPFD` measures absorbed photosynthetically active radiation (PAR), the light
    +available for photosynthesis. Its unit here is μmol of photons per m² of
    +ground per second, averaged over the day. The area refers to the ground
    +covered by the canopy, rather than the area of its leaves.
     
    -```@example journey_one_object
    -select(
    -    DataFrame(Diagnostics.explain_bindings(model)),
    -    :application_id,
    -    :input,
    -    :source_application_ids,
    -    :carrier_kind,
    -)
    -```
    +## Continue for another day
     
    -A `Simulation` owns a continuing timeline. Advancing it does not rebuild a
    -separate result object:
    +`step!` runs the next day and adds its results to the same simulation:
     
     ```@example journey_one_object
     step!(simulation)
    @@ -106,20 +115,3 @@ state_at_day_31 = final_state(simulation)
     You have now extended the same history to day 31. Continue with
     [several independent objects](several_objects.md), or
     [plot the results](../../guides/data/outputs_plotting.md).
    -
    -!!! tip "Optional numerical choices"
    -    If your study needs `Float32` or uncertainty values, see
    -    [Numerical Reliability](@ref). Those choices are independent of the
    -    coupling and output steps introduced here.
    -
    -## Page recap
    -
    -- **You added:** three models, supplied weather, a 30-step run, and retained
    -  outputs.
    -- **PlantSimEngine inferred:** the one object, three applications, their
    -  execution order, and the `TT_cu` and `LAI` connections.
    -- **You keep explicit:** model parameters, forcing data, number of steps, and
    -  whether output history is retained.
    -- **New API names:** `CompositeModel`, `run!`, `Simulation`, `final_state`,
    -  `collect_outputs`, `outputs`, `current_step`, `step!`, and
    -  `Diagnostics.explain_bindings`.
    diff --git a/docs/src/journeys/users/one_plant.md b/docs/src/journeys/users/one_plant.md
    index 046cdb70b..a612fb49b 100644
    --- a/docs/src/journeys/users/one_plant.md
    +++ b/docs/src/journeys/users/one_plant.md
    @@ -1,26 +1,28 @@
     # Build One Multiscale Plant
     
    -## New concept: topology and cross-object values
    -
     Share an absorbed-light supply between two leaves, then compute how their
    -surfaces contribute to the plant total. This teaching example adds a
    -parent/child structure: one plant object owns two leaf objects. The leaf
    -surfaces and radiation values are illustrative.
    +areas contribute to the plant total. This teaching example represents the
    +plant and its two leaves as three objects. Each leaf names the plant as its
    +`parent`, which records that the leaf belongs to that plant. The leaf areas
    +and radiation values are illustrative.
     
    -The scope picture for this page is:
    +The structure is:
     
    -> `:plant` — `Subtree()` from here contains `:plant`, `:leaf_1`, and `:leaf_2`
    -> ├─ `:leaf_1` — `Self()` is `:leaf_1`; `SelfPlant()` resolves to `:plant`
    -> └─ `:leaf_2` — `Self()` is `:leaf_2`; `SelfPlant()` resolves to `:plant` +> `:plant`
    +> ├─ `:leaf_1`
    +> └─ `:leaf_2` -Selectors still filter that scope. For example, -`Many(scale=:Leaf, within=Subtree())` selects the two leaves when evaluated for -the plant application. +To choose objects for a calculation, use a **selector** such as +`Many(scale=:Leaf)`. The `within` option limits where to look. For a model +running on `:plant`, `Subtree()` includes that plant and everything below it, +so `Many(scale=:Leaf, within=Subtree())` selects its two leaves. For a model +running on a leaf, `Self()` means that leaf, and `SelfPlant()` means its plant. -## First pass: one scalar value from plant to leaves +## First pass: share light between leaves -Start with leaf surfaces and total plant surface supplied as status. The only -new value connection sends the plant-level absorbed light to each leaf. +Start by supplying the leaf areas and their total in `Status`, where each +object stores its values. Each leaf model will read the plant's absorbed +light and total leaf area to calculate its own share. We use one **common reference ground area** for the whole plant. Its supplied `aPPFD` is 120 μmol m⁻² of reference ground s⁻¹. Each leaf receives a share in @@ -84,13 +86,17 @@ scalar_states = final_state(scalar_simulation, Many(scale=:Leaf)) Dict(id => state.aPPFD for (id, state) in scalar_states) ``` -The surfaces are in m² of leaves. To obtain a mean leaf-area photon flux from -one contribution, multiply it by the plant's reference ground area and divide -by that leaf's area. Such a change of basis belongs in an explicit model when -coupling to a leaf photosynthesis model; see [Coupling models](../../guides/coupling.md). +The areas stored in `surface` are in m² of leaves. A leaf photosynthesis model +may instead need light per m² of leaf. To make that conversion, multiply a +leaf's contribution by the plant's reference ground area, then divide by +that leaf's area. Write this conversion as a model so the units are clear +when connecting the two calculations; see [Coupling models](../../guides/coupling.md). -The leaf model reads its own `surface` directly from each leaf status. -`SelfPlant()` makes the other two scalar sources plant-local: +Each leaf model reads `surface` from that leaf and uses `SelfPlant()` to find +the plant's `aPPFD` and total `surface`. In the table below, `consumer_id` +identifies the object reading a value, and `source_ids` identifies where the +value comes from. `carrier_kind` describes how PlantSimEngine shares it; +`ref` means the model reads the source's current value directly. ```@example journey_one_plant select( @@ -102,7 +108,7 @@ select( ) ``` -## Second pass: compute and aggregate leaf surfaces +## Second pass: calculate leaf areas and their total Now replace the supplied surfaces with two existing models: @@ -113,8 +119,9 @@ Here the leaf carbon biomasses are 50 and 100 g C, and the specific leaf area is 0.02 m² g C⁻¹. Their calculated areas are therefore 1 and 2 m², preserving the light shares from the first pass. -This is the first vector-like cross-object input. It comes after the scalar -connection above, and differs only in the new `:leaf_surfaces` binding. +The plant model now needs a collection of values: one area from each leaf. +The `Many(...)` selector for `:leaf_surfaces` provides that collection, which +`ToyPlantLeafSurfaceModel` adds together. ```@example journey_one_plant computed_objects = ( @@ -196,9 +203,10 @@ The resulting plant surface should be 3 m² and the light contributions should still be 40 and 80 μmol m⁻² of reference ground s⁻¹. You can now [reuse this configuration on several plants](several_plants.md). -The following optional diagnostic shows that the plant reads its own leaves. -Its `RefVector` is a collection of live values; individual scalar inputs use -single references: +The optional table below lets you check the connections. For +`:leaf_surfaces`, the plant should read from both leaves. Its `RefVector` +holds references to their current areas, so the plant sees the new values +after the leaf models update them. Inputs with just one source use `ref`. ```@example journey_one_plant select( @@ -210,14 +218,3 @@ select( :carrier_kind, ) ``` - -## Page recap - -- **You added:** parent/child topology, plant-to-leaf scalar connections, and - one plant-local vector aggregation. -- **PlantSimEngine inferred:** same-leaf `surface` coupling, application order, - and live scalar/vector reference carriers. -- **You keep explicit:** object parentage, the scope of cross-object searches, - and the source application when selecting a produced value. -- **New API names:** `parent`, `Self`, `SelfPlant`, `Subtree`, and the - `application` and `var` selector fields. diff --git a/docs/src/journeys/users/several_objects.md b/docs/src/journeys/users/several_objects.md index 456084b26..980442c0c 100644 --- a/docs/src/journeys/users/several_objects.md +++ b/docs/src/journeys/users/several_objects.md @@ -1,12 +1,10 @@ # Run The Coupling On Several Objects -## New concept: stable object identity and `Many` - Run the same teaching models on two independent canopies with different initial development stages. Each canopy is one object; `Many(scale=:Canopy)` selects both. This lets you compare the two canopies without writing a loop -inside any process model. The selected initial thermal times make their -different leaf area indices visible even over this short, five-day run. +inside each model. We start the canopies at different cumulative thermal +times so their leaf area indices differ visibly over this short, five-day run. ```@example journey_several_objects using PlantSimEngine, PlantMeteo, Dates, DataFrames @@ -51,11 +49,12 @@ model = CompositeModel( ) ``` -`:canopy_a` and `:canopy_b` are stable object identities. Their initial -cumulative thermal times differ, but the same three model kernels execute for -both. The model implementations contain no loop over canopies. +The names `:canopy_a` and `:canopy_b` identify the canopies throughout the +simulation. Both use the same three models, but each keeps its own values. +PlantSimEngine runs the models for each canopy, so the model code does not +need a loop over canopies. -Run five steps and inspect each independent final status: +Run five daily steps and read the final values for each canopy: ```@example journey_several_objects simulation = run!(model; steps=5, outputs=:all) @@ -70,8 +69,8 @@ LAI is in m² of leaves per m² of ground; `aPPFD` is in μmol of absorbed PAR per m² of ground per second. The CSV radiation totals were converted to mean fluxes just as in the one-object example. -Retained streams are keyed by application, object, and variable, so the two -objects do not overwrite one another: +The saved results include the model application name, canopy name, and +variable name. This keeps the two LAI time series separate: ```@example journey_several_objects rows = collect_outputs(simulation) @@ -84,8 +83,8 @@ lai_rows = rows[rows.variable .== :LAI, [ first(lai_rows, 6) ``` -The application diagnostic confirms that each application compiled to both -objects: +You can also check which objects each model will run on. The table below +should list both canopies for each application: ```@example journey_several_objects select( @@ -95,17 +94,7 @@ select( ) ``` -This remains a same-scale simulation. Parent/child topology and cross-object -value selection are introduced in [one multiscale plant](one_plant.md). To compare the time -series visually, follow [Collecting And Plotting Outputs](../../guides/data/outputs_plotting.md). - -## Page recap - -- **You added:** two explicit `Object`s, stable ids, one shared `Many` selector, - and named `ModelSpec` applications. -- **PlantSimEngine inferred:** two targets per application plus independent - same-object `TT_cu` and `LAI` connections for each canopy. -- **You keep explicit:** which objects exist, their initial status, application - names, and the selector describing the target set. -- **New API names:** `Object`, `Status`, `ModelSpec`, `Many`, and - `Diagnostics.explain_applications`. +The two canopies run independently here. In [one multiscale plant](one_plant.md), +you will connect a plant to its leaves and pass values between them. To +compare the canopy time series visually, follow +[Collecting And Plotting Outputs](../../guides/data/outputs_plotting.md). diff --git a/docs/src/journeys/users/several_plants.md b/docs/src/journeys/users/several_plants.md index 5e76706d9..c9bc26cef 100644 --- a/docs/src/journeys/users/several_plants.md +++ b/docs/src/journeys/users/several_plants.md @@ -1,11 +1,9 @@ # Instantiate Several Plants -## New concept: templates, instances, and overrides - Apply the configuration from [one multiscale plant](one_plant.md) to two plants, then change the specific leaf area of a third. A `CompositeModelTemplate` -stores the reusable model configuration; each `ObjectInstance` supplies the -actual plant and its leaves. +stores the models and their connections so you can reuse them. Each +`ObjectInstance` supplies a plant, its leaves, and their initial values. As in that teaching example, light values are contributions per m² of a plant's reference ground area per second, in μmol of absorbed PAR. A plant's @@ -55,11 +53,12 @@ plant_template = CompositeModelTemplate(( ), ), )) +nothing # hide ``` -An `ObjectInstance` supplies the concrete root and organs. These two instances -reuse the same template while keeping different initial plant radiation and -leaf biomasses: +Create two plants from the template. Here `root` is the object representing +the whole plant, at the top of its structure; it does not mean a botanical +root. The plants have different absorbed light and initial leaf biomasses: ```@example journey_several_plants plant_a = ObjectInstance( @@ -122,10 +121,11 @@ plant_states = final_state(simulation, Many(scale=:Plant)) Dict(id => (surface=state.surface, aPPFD=state.aPPFD) for (id, state) in plant_states) ``` -Plant A aggregates surfaces `1 + 2 = 3 m²`; plant B aggregates `1 + 1 = 2 m²`. -Those totals prove that `Subtree()` did not mix leaves between instances. -Likewise, each pair of leaf light contributions sums to its own plant's -supplied flux on that plant's common ground-area basis: +Plant A has `1 + 2 = 3 m²` of leaves; plant B has `1 + 1 = 2 m²`. +Each plant uses only its own leaves when calculating the total, because the +selector uses `Subtree()`. Likewise, each pair of leaf light contributions +adds up to the light supplied to its own plant, expressed per m² of that +plant's reference ground area: ```@example journey_several_plants leaf_states = final_state(simulation, Many(scale=:Leaf)) @@ -141,9 +141,9 @@ leaf_states = final_state(simulation, Many(scale=:Leaf)) ) ``` -The instance diagnostic shows the mounted object and application ids. Template -application names are prefixed automatically, so the two mounted graphs remain -unambiguous: +The table below lists the objects and model applications for each plant. +PlantSimEngine adds the plant instance name to each application name, which +lets you distinguish the two plants' calculations: ```@example journey_several_plants select( @@ -157,9 +157,9 @@ select( ## Override one instance -Only after the two unchanged instances work, override one application for a -new instance. This plant uses a larger specific leaf area while retaining the -same logical `:leaf_surface` application and all other template wiring: +Now create a third plant with a larger specific leaf area. Set `overrides` +to replace the model used for `:leaf_surface` on this plant. The other models +and their connections stay as defined in the template: ```@example journey_several_plants plant_c = ObjectInstance( @@ -199,17 +199,6 @@ The third plant has 6 m² of leaves: twice the area at the original specific leaf area, for the same supplied carbon biomass. This is a parameter comparison within the teaching model, not a calibrated species comparison. -There is no `SceneScope()` in this example because nothing is deliberately -shared between plants. Introduce scene-wide scope only when adding a real -shared source, such as a soil object or scene-level forcing controller. - -## Page recap - -- **You added:** one reusable `CompositeModelTemplate`, two independent - `ObjectInstance`s, and then one application override. -- **PlantSimEngine inferred:** instance-local selector scopes, prefixed mounted - application ids, and the same compiled coupling graph for each plant. -- **You keep explicit:** each instance's objects and initial values, plus the - exact application replaced by an override. -- **New API names:** `CompositeModelTemplate`, `ObjectInstance`, `overrides`, - and `Diagnostics.explain_instances`. +The plants do not share any inputs in this example. If they need to read a +shared soil object, for example, use `within=SceneScope()` in that input's +selector. This allows it to look beyond the current plant. diff --git a/docs/src/journeys/users/structure_changes.md b/docs/src/journeys/users/structure_changes.md index b09749df7..ff182070a 100644 --- a/docs/src/journeys/users/structure_changes.md +++ b/docs/src/journeys/users/structure_changes.md @@ -1,11 +1,10 @@ # Modify Plant Structure -## New concept: lifecycle changes refresh compiled targets - Start with one plant, one branch, and two leaves. Each leaf computes carbon -demand, then treats that fully met demand as accepted carbon allocation for -`ToyCBiomassModel`. This small chain gives us a conserved quantity to check -while topology changes. +demand. We assume enough carbon is available to meet that demand and pass the +full amount to `ToyCBiomassModel`, which calculates growth and respiration. +We will add a leaf, move it to the branch, and remove another leaf. After +each change, we can check that the carbon balance still holds. ```@example journey_structure using PlantSimEngine, DataFrames @@ -61,9 +60,10 @@ initial_targets = only( ## Add one leaf -Registering an object mutates the live model and marks affected compiled state -dirty. Because this call happens between simulation steps, the new leaf is -compiled before the next step: +Use `register_object!` to add a leaf with its initial thermal time. The +simulation then needs to update which leaves its models run on. Here we add +the leaf between steps, so PlantSimEngine makes that update before running +the next step with `continue!`: ```@example journey_structure register_object!( @@ -101,16 +101,18 @@ targets_after_refresh = only( ) ``` -When a lifecycle operation occurs *inside* a model kernel, PlantSimEngine -refreshes after that application. A newly registered object may therefore run -applications that remain later in the same timestep. It runs an application -that already completed only through an explicit `Initializer` binding and -`run_initializer!` call from its creator. +You can also create organs inside a model's `run!` function, for example in +a growth model. In that case, PlantSimEngine updates the simulation after +that model finishes. The new organ can take part in calculations scheduled +later in the same timestep. Calculations that already ran are not repeated +automatically: to run one of them on the new organ, the growth model must +declare an `Initializer` and call `run_initializer!`. See +[Manual Calls Across Objects](../../guides/multiscale/manual_calls.md). ## Reparent, then remove -Creation now works, so make two further changes in order. First move the new -leaf under the branch and advance: +First change the new leaf's parent from the plant to the branch, then advance +the simulation: ```@example journey_structure reparent_object!(model, :leaf_3, :branch) @@ -145,8 +147,9 @@ continue!(simulation) ## Check conservation and history -For every retained leaf sample, accepted carbon allocation equals demand. The -biomass model partitions it into biomass increment plus growth respiration: +In this example, each leaf receives all the carbon it demands. For every +saved timestep, check that this carbon equals the increase in biomass plus +the carbon used in growth respiration: ```@example journey_structure rows = collect_outputs(simulation; sink=nothing) @@ -176,8 +179,7 @@ all( ) ``` -Removed-object history remains queryable even though the object is no longer -in the registry: +You can still read a removed leaf's earlier results: ```@example journey_structure history_counts = Dict( @@ -191,20 +193,10 @@ history_counts = Dict( ) ``` -`:leaf_2` keeps the three samples published before removal; `:leaf_3` begins at -step 2, after registration, and also has three samples. +`:leaf_2` keeps the three samples saved before removal. Results for `:leaf_3` +begin at step 2, after it was added, and also contain three samples. `ToyCAllocationModel` is useful when supply is limiting and a plant controller -must divide carbon among many organ demands. This journey deliberately assumes -all demand is accepted so lifecycle timing and conservation stay visible -without introducing a controller or hard calls. - -## Page recap - -- **You added:** one leaf, then one reparenting operation, then one removal. -- **PlantSimEngine inferred:** the affected application targets, status views, - reference binding, execution batch extension, and retained stream keys. -- **You keep explicit:** initialized status for a new object, its parent, - conservation assumptions, and when removal or reparenting occurs. -- **New API names:** `register_object!`, `reparent_object!`, `remove_object!`, - and `Diagnostics.explain_applications(simulation)`. +must divide carbon among organs. Here we assume there is enough carbon to +meet every demand, so you can focus on when organs are added or removed and +how to check their results. diff --git a/docs/src/migration_composite_model.md b/docs/src/migration_composite_model.md index 395952952..8196a2d87 100644 --- a/docs/src/migration_composite_model.md +++ b/docs/src/migration_composite_model.md @@ -1,11 +1,16 @@ # Migrating To The CompositeModel/Object API +Use this page when updating code written for an older PlantSimEngine API. +It shows how earlier model mappings, input connections, and output requests +are written with `CompositeModel` and `Object`. If you are starting a new +simulation, follow [the first tutorial](journeys/users/one_object.md). + ## Refining early CompositeModel/Object code -The stabilized public surface makes several early CompositeModel/Object behaviors -explicit: +Some names and defaults changed during development of the CompositeModel/Object +API. Update early examples as follows: -| Early spelling or behavior | Stabilized API | +| Early spelling or behavior | Current API | | --- | --- | | `Self()` searched self and descendants | `Self()` selects only the current object; use `Subtree()` for self plus descendants | | omitted `tracked_outputs` retained everything | use explicit `outputs=:all`; the safe default is `outputs=:none` | @@ -18,7 +23,7 @@ explicit: `tracked_outputs` has been removed. Use `outputs=:all`, `outputs=:none`, or `outputs=requests` directly. Singular scenario `inputs` and `calls`, `OutputRequest`, object overrides, and `Updates(...; after=...)` now identify -the target by canonical `application=...` or application ID. Model-authored +the model application with `application=...` or its application ID. Model-authored `Input`/`Call` defaults may still discover a process because they cannot know scenario application names, and `Many(process=...)` remains an explicit multi-application discovery query. @@ -35,8 +40,8 @@ step!(simulation) @assert current_step(simulation) == 49 ``` -The composite-model/object API replaces the historical multiscale mapping system with -one object-address graph. +The new API stores the simulated objects and their model applications in one +`CompositeModel`, replacing the earlier multiscale mappings. New scenario code should be organized around: @@ -48,8 +53,8 @@ Updates Environment ``` -Process-model implementations do not need to know about composite models, plants, objects, or -timesteps. They keep the existing kernel contract: +The equations stay in a model's `run!` function. Models that only calculate +values can be reused without referring to the simulation's plant structure: ```julia inputs_(model) @@ -64,8 +69,8 @@ equivalents. ## Explicit Input Initialization -Input literals no longer double as ambiguous placeholder values. Declare -whether each model input is required or genuinely has a fallback: +For each input, say whether a value must be supplied or whether the model +can use a default: ```julia # Old, ambiguous @@ -113,8 +118,8 @@ model = CompositeModel( ) ``` -`Object` labels describe runtime entities. They do not prescribe plant -topology. A plant may use any hierarchy of plants, axes, internodes, segments, +`Object` labels describe the parts of the simulated system. You choose how +these parts are connected. A plant may use any hierarchy of plants, axes, internodes, segments, leaves, roots, fruits, or application-specific objects. ### Status type conversion @@ -169,10 +174,9 @@ model = CompositeModel( `objects_from_mtg(mtg; ...)` exposes the intermediate object list when it is useful to inspect or modify labels before constructing the model. By default, the adapter uses MTG node ids and scales. Runtime `Status` values belong to the -`CompositeModel` registry and are never stored in MTG attributes. A deliberate -import boundary may provide `status=node -> import_status(node)` explicitly; -maintained workflows should initialize scientific state through model objects or -model applications instead. +`CompositeModel` registry and are never stored in MTG attributes. To import values from node attributes, supply a function such as +`status=node -> import_status(node)` explicitly. Set the starting values used +in the simulation on its objects or through its model applications. ## Multiscale Inputs @@ -225,8 +229,9 @@ ModelSpec( ) ``` -Same-rate bindings use shared references or reference vectors when possible. -Cross-rate bindings use typed temporal streams. +When models run at the same rate, their inputs can read the source objects' +current values through shared references. When rates differ, PlantSimEngine +uses saved output histories and your chosen rule for reading them. ## CompositeModel-Wide Values @@ -242,8 +247,8 @@ ModelSpec(SceneWaterBalance(); name=:scene_water, on=One(scale=:Scene), inputs=( ),)) ``` -The compiler chooses the carrier. Scenario authors declare the source objects, -source variable, and temporal policy rather than a route implementation. +Specify which objects supply the value, the source variable's name, and how +to read it over time. PlantSimEngine chooses how to store the connection. ## Manual Hard Calls @@ -282,10 +287,10 @@ function PlantSimEngine.run!(model::SceneEnergyBalance, status, environment, end ``` -`run_call!` defaults to `publish=false`. Trial calls mutate target status but -do not publish temporal samples or commit mutable environment updates. -do not append temporal samples or write environment outputs. The accepted -state must use `publish=true`. +`run_call!` uses `publish=false` by default. A trial changes the called +objects' current values, but does not add samples to their output histories +or save updates to the shared environment. Use `publish=true` to record the +accepted result, and `commit_environment!` to save accepted environment changes. ## Multiple Plants And Species @@ -450,24 +455,25 @@ model object together and, by default, reuses the status initialization policy f `CompositeModel(mtg; status=...)`. Use `register_object!` when adapting another topology backend or when a complete `Object` already exists. -Structural changes refresh application targets, input carriers, call targets, -writer validation, and schedules after the application that made the change. -New objects can therefore run applications that remain later in the current -timestep, but never applications that already ran. Geometry-only changes -refresh environment bindings without rebuilding unrelated structural bindings. +After the application that adds or removes objects finishes, PlantSimEngine +updates which models run, where they get their inputs, and which other models +they can call. It also checks for conflicting outputs and updates the schedule. +New objects can run applications that are still due later in the same step. +An application that already ran is only repeated for a new object through +an explicit `Initializer` call; see [manual calls](guides/multiscale/manual_calls.md). +Changing geometry alone updates the affected environment connections. -The refreshed runtime also rebuilds homogeneous execution batches. Use -`Diagnostics.explain_execution_plan(scene_or_simulation)` to inspect the concrete -model/status/carrier types and the objects grouped into each specialized inner -loop. Exceptional per-object model overrides appear as separate ordered -batches. +PlantSimEngine also updates the groups of similar objects it runs together. +`Diagnostics.explain_execution_plan(scene_or_simulation)` lists those groups +and their model and value types. An object using a replacement model appears +in a separate group. ## Output Collection `run!(model; steps=...)` returns a `Simulation`. Use `final_state(sim)` for the -latest one-object state, `outputs(sim)` for retained typed streams, -`Diagnostics.explain_outputs(sim)` for structured diagnostics, and -`collect_outputs(sim)` for tabular rows. +latest values of a single object, `outputs(sim)` for saved output histories, +`Diagnostics.explain_outputs(sim)` for a report about those outputs, and +`collect_outputs(sim)` to gather them into a table. ```julia request = OutputRequest( @@ -483,8 +489,7 @@ sim = run!(model; steps=48, outputs=request) daily = collect_outputs(sim, :leaf_transpiration_daily) ``` -CompositeModel output requests are materialized from retained temporal streams after -the run. They use the same temporal policies as multirate inputs and export +Output requests collect saved results after the run. They use the same temporal policies as multirate inputs and export dynamic objects only over the interval where that object published samples. If several model applications implement the same process, add `application=:application_name` to select one explicitly. This is also the @@ -492,9 +497,9 @@ way to request a named `:stream_only` publisher. `outputs=:none` retains no user streams. Passing explicit requests retains only their application/variable streams plus streams needed by temporal `ModelSpec(...; inputs=...)`. Use `Diagnostics.explain_output_retention(sim)` -to inspect why each retained stream was kept. Dependency-only streams retain a -bounded policy-specific horizon, while requested streams keep complete -histories for post-run export. Export is not yet a fully online path. +to inspect why each retained stream was kept. For results needed only as inputs to other models, PlantSimEngine keeps +just enough history for the chosen time rule. Results you explicitly request +keep their full history so you can collect them after the run. ## Inspecting The Compiled Scenario @@ -512,9 +517,9 @@ Diagnostics.explain_schedule(model) Diagnostics.explain_writers(model) ``` -These functions return structured rows with concrete object ids, application -ids, processes, variables, temporal policies, carrier semantics, and resolved -targets. They are intended for both users and coding agents. +These reports identify the objects and models used by the simulation, where +their values come from, and how they exchange values over time. People and +coding agents can inspect the same reports. ## Migration Table diff --git a/docs/src/model_execution.md b/docs/src/model_execution.md index 6e5a5c907..b55373442 100644 --- a/docs/src/model_execution.md +++ b/docs/src/model_execution.md @@ -1,10 +1,11 @@ # Model Execution -This page describes how the native composite-model/object runtime executes model -applications. Use this path for new multi-object, multi-plant, soil, -microclimate, and multirate simulations. +This reference explains how PlantSimEngine prepares and runs a simulation. +Start with [your first simulation](journeys/users/one_object.md) if you are +new to the package. -The public configuration surface has one application constructor: +A **model application** says where to run a model and how to supply its +inputs. Use `ModelSpec` to describe one: ```julia ModelSpec( @@ -20,30 +21,33 @@ ModelSpec( ) ``` -Scenarios start from `CompositeModel` and model applications. +Put these applications and the objects they describe into a `CompositeModel`. ## Model Kernels And Applications -A model kernel is still an ordinary PlantSimEngine model: +A **kernel** is the `run!` function that evaluates a model's equations. +The model also declares what those equations need and produce: - `inputs_(model)` declares each status input as `Required(T)` or `Default(value)`; - `outputs_(model)` declares variables the model computes and their initial output-state values; - `environment_inputs_(model)` declares environment variables it reads; +- `environment_outputs_(model)` declares which environmental variables it + may update, such as the temperature controlled by a canopy model; - `commit_environment!(context, state)` commits accepted mutable environment state when the model intentionally controls microclimate; - `dep(model)` may declare model-author defaults; - `run!(model, status, environment, constants, context)` contains the model equations. -`Required(T)` has no initialization value: object state or a producer -application must satisfy it. `Default(value)` is installed only when the target -does not already have the input. Plain input literals are rejected because -they are ambiguous. +`Required(T)` means you must supply the input on the object, or connect it +to another model's output. `Default(value)` supplies a starting value only +when that input is absent. A plain number is not a valid input declaration: +it would not say whether the model requires a value or can use a default. -The composite-model/object layer does not change that kernel contract. It adds a -scenario-specific application around the kernel: +The equations stay in the model. Use `ModelSpec` to configure their use in +this particular simulation: ```julia ModelSpec( @@ -59,29 +63,29 @@ ModelSpec( `ModelSpec` decides where the model runs, where its inputs come from, which models it may call manually, which timestep it uses, and which environment -provider is bound to it. The model implementation stays reusable. +source supplies its growing conditions. You can reuse the same model in +other simulations with different choices. ## Compilation Before Runtime -Before the timestep loop, PlantSimEngine compiles the model into concrete -runtime carriers: - -1. `ModelSpec(...; on=...)` selectors are resolved to stable object ids. -2. `ModelSpec(...; inputs=...)` selectors are resolved to source object/application ids. -3. Same-rate inputs are wired as shared `Ref`s, `RefVector`s, or - heterogeneous object-reference vectors. -4. Temporal inputs are compiled as stream lookups with a policy such as - `HoldLast`, `Interpolate`, `Integrate`, or `Aggregate`. -5. `ModelSpec(...; calls=...)` declarations are compiled to callable target lists. -6. `Environment(...)` is bound to backend cells, layers, voxels, or global - weather providers. -7. The root application order is topologically sorted from value inputs and - `Updates(...)` ordering. -8. Root execution batches are grouped by concrete model/status/environment - types where possible. - -Selectors are not resolved in the hot loop. Runtime execution uses the -compiled indexes and carriers. +Before the first time step, PlantSimEngine prepares the simulation. This +preparation is called **compilation**. It: + +1. finds the objects selected by each application's `on` rule; +2. finds where each input value will come from; +3. connects values updated at the same rate through shared references, which + let one model read another object's current value without copying it; +4. prepares any output history and rules needed to exchange values between + models that run at different time steps; +5. lists the models that each controller can call through `calls`; +6. finds the weather source, cell, or layer that supplies each object; +7. puts applications in order so their inputs are available when needed, + including any order specified by `Updates`; +8. groups objects with matching model, status, and environment types so Julia + can run them efficiently. + +The time loop reuses this work instead of searching for every connection +again at each step. Useful inspection helpers: @@ -95,9 +99,9 @@ Diagnostics.explain_execution_plan(model) Diagnostics.explain_writers(model) ``` -These explanations are intended for both users and agents. They report the -compiled object ids, applications, carriers, clocks, environment bindings, and -manual-call targets that the runtime will use. +These reports show which objects and models were selected, where their +inputs come from, when they run, and which growing conditions they receive. +Both people and coding agents can inspect the reports. ### Readable source views @@ -120,17 +124,16 @@ source = Authoring.compiled_model_source(model) Authoring.write_compiled_model_source("compiled_model.jl", model) ``` -The generated source spells out application order, selected targets, input -provenance, hard calls, and the kernel bodies invoked through normal -PlantSimEngine status, environment, output, and lifecycle machinery. It is -optimized for explanation and review, not as an alternative scheduler. It -represents the resolved plan; `scenario_source` reconstructs the scenario that -an author can edit and compile again. +The generated code shows which models run on which objects, where their +inputs come from, and which equations and manual calls are used. It runs +through the normal PlantSimEngine machinery. Use this view to inspect +execution; use `scenario_source` when you want to edit the simulation setup. ## Soft Dependencies With Inputs -Soft dependencies are value dependencies. A consumer model reads a variable -produced by another model through `ModelSpec(...; inputs=...)`. +A **soft dependency** means that one model needs a value calculated by +another. The model supplying the value is called the **producer**; the model +reading it is the **consumer**. Connect them with `ModelSpec(...; inputs=...)`: ```julia ModelSpec(SceneLAI(ground_area); name=:scene_lai, on=One(scale=:Scene), inputs=(:leaf_areas => Many( @@ -142,24 +145,28 @@ ModelSpec(SceneLAI(ground_area); name=:scene_lai, on=One(scale=:Scene), inputs=( ),)) ``` -For same-rate inputs, the runtime installs a reference carrier into the -consumer status during compilation. A model-scale model reading all leaf areas -therefore sees a `RefVector`-like object: reading pulls current values from -source leaf statuses, and writing through the carrier mutates source refs when -the carrier supports it. +When the models run at the same rate, the input refers to the source +object's current value. A plant model reading several leaf areas receives +a reference vector such as `RefVector`: a list that reads the current value +from each leaf. This shared storage is called a **reference carrier** in +the diagnostic reports. Where it supports writing, changing an entry also +changes the source object's value. -If an input is not explicitly declared with `ModelSpec(...; inputs=...)`, the compiler can -infer simple same-object bindings when exactly one producer on the same object -outputs the same variable. Ambiguous producers are errors and should be -disambiguated with `application=...` and, when names differ, `var=...`. +You can omit an input connection when exactly one other model on the same +object provides an output with the same name. PlantSimEngine connects it +automatically. If several models provide that output, choose one with +`application=...`. Use `var=...` when the source variable has a different name. -Use `PreviousTimeStep(:x) => selector` when a feedback dependency should read -the previous sample instead of creating a same-timestep scheduling edge. +Use `PreviousTimeStep(:x) => selector` when a feedback calculation should +read the previous step's value of `x`. The receiving model then does not +need to wait for the current step's calculation of `x`. ## Hard Calls With Calls -Hard dependencies are manual calls. Use `ModelSpec(...; calls=...)` when a parent model must -control the call stack, for example during an iterative energy-balance solve. +A **hard dependency** means that one model decides when to run another. +Declare it with `ModelSpec(...; calls=...)`. For example, an energy-balance +model may need to run photosynthesis repeatedly while adjusting leaf +temperature. ```julia ModelSpec(SceneEnergyBalance(); name=:scene_energy, on=One(scale=:Scene), calls=(:leaf_energy => Many( @@ -202,20 +209,22 @@ function PlantSimEngine.run!(model::SceneEnergyBalance, status, environment, end ``` -`run_call!` defaults to `publish=false`. Trial calls mutate target statuses but -do not publish temporal samples or commit mutable environment updates. Use -`environment=trial_state` when hard-called descendants should sample temporary -state through their compiled environment handles. Call `commit_environment!` and -`run_call!(...; publish=true)` once for the accepted state. +`run_call!` uses `publish=false` by default. Each trial updates the called +objects' current values, but does not add results to their time histories or +save changes to the shared environment. Pass `environment=trial_state` to +try temporary growing conditions; each called model still gets the conditions +for its own location. Once the result is accepted, save the environment +with `commit_environment!` and run once with `publish=true` to record it. -Applications selected only by `ModelSpec(...; calls=...)` are marked manual-call-only in -`Diagnostics.explain_schedule(model)` and are skipped by the root `run!(model)` loop. +Applications used only through `ModelSpec(...; calls=...)` run when their +controller calls them. They do not also run independently from the normal +schedule. `Diagnostics.explain_schedule(model)` marks them as manual-call-only. ## Duplicate Writers With Updates -By default, one application owns each `(object, output variable)` canonical -writer. If a scenario intentionally lets several models update the same -variable, later writers must declare that order explicitly: +Normally, only one model application may calculate a given output variable +on an object. If you want several models to update that variable, state +which should run first: ```julia ModelSpec(CarbonAllocation(); name=:carbon_allocation, on=Many(scale=:Leaf)) @@ -226,12 +235,14 @@ ModelSpec(LeafPruning(); name=:leaf_pruning, on=Many(scale=:Leaf), updates=Updat This keeps ordinary duplicate outputs as errors while allowing cases such as allocation followed by pruning. `Diagnostics.explain_writers(model)` reports writer groups and the `Updates(...)` declarations that validate them. -The `after` value is the canonical application identifier shown by -`Diagnostics.explain_applications(model)`, not the process name. +For `after`, use the application ID shown by +`Diagnostics.explain_applications(model)`. A process name does not identify +a particular application when several models use that process. ## Multirate Execution -Use `ModelSpec(...; every=...)` with `Dates.Period` values for model application clocks: +**Multirate** means that models run at different time steps. Set how often +a model runs with `ModelSpec(...; every=...)` and a duration such as `Hour(1)`: The duration must be a positive integer multiple of the simulation base step. Choose a finer common base step when needed; the scheduler does not insert @@ -274,8 +285,10 @@ Supported policies are: duration weighting; - `Aggregate()`: reduce values over a window, defaulting to `MeanReducer()`. -Policies resample numeric values but do not transform `VariableContract` -metadata. A contracted rate-to-amount conversion needs an explicit adapter; +These rules combine numeric values but do not change the units or physical +meaning recorded in `VariableContract`. If you declare a rate on one side +and an amount on the other, use a conversion model with the appropriate +units and meaning declared for each side; see [Coupling models](@ref). `Integrate(...)` and `Aggregate(...)` accept reducer objects or callables that @@ -289,9 +302,9 @@ producer execution and weighted by the portion of that interval overlapping the consumer window. This includes the last value published before the window when it remains active inside the window. -Temporal windows are duration-based rolling windows. Calendar-aligned civil -days and "previous complete period" selection are not part of the public API; -there is no `CalendarWindow` compatibility type. +Time windows cover a duration relative to the current simulation time. +They do not automatically align with calendar days or select the previous +complete day, week, or month. ## Environment Sampling @@ -301,9 +314,10 @@ there is no `CalendarWindow` compatibility type. ModelSpec(CO2Probe(); name=:co2_probe, on=Many(scale=:Leaf), environment=Environment(provider=:canopy, sources=(CO2=:Ca,))) ``` -The compiler binds each application/object pair to the selected backend before -runtime. Constant weather, global tabular meteorology, grid, layer, voxel, or -octree-style microclimate backends all use the same contract: +Before running, PlantSimEngine finds the environment source for each model +and object. An environment **backend** is the code that supplies those +conditions, from a weather table or a spatial representation such as soil +layers or canopy cells. All backends use the same model-facing functions: - `environment_inputs_(model)` says what the model reads; - `environment_outputs_(model)` says what the model may commit; @@ -315,7 +329,9 @@ octree-style microclimate backends all use the same contract: - geometry and position are used by spatial backends when available; - object-to-environment links are cached and refreshed when objects move. -Backend authors implement an opaque-handle protocol: +A backend first locates the data needed by an object and saves that location +in a **handle**. The model does not need to interpret this handle; the backend +uses it to retrieve values efficiently. Backend authors implement: ```julia handle = EnvironmentAPI.bind_environment(backend, object, context, config) @@ -325,12 +341,13 @@ EnvironmentAPI.sample(backend, handle, trial_state, variable, time) # transient commit_environment!(backend, handle, accepted_state, time) ``` -`EnvironmentAPI.EnvironmentContext` identifies the application, object, scale, and process -while the handle is compiled. Runtime status and geometry are not passed to -sampling: a spatial backend resolves them once in `EnvironmentAPI.bind_environment` and stores -the resulting provider, layer, voxel, or other routing data in its concrete -handle. A controller that reads from one provider and commits to another should -encode both routes in the handle, for example +`EnvironmentAPI.EnvironmentContext` identifies the model application, object, +scale, and process when the backend prepares the handle. A spatial backend +uses `EnvironmentAPI.bind_environment` to locate the object's layer, cell, or +other data source and stores that location in the handle. Later requests for +environment values use this handle; they do not receive the object's full +status and geometry again. If a controller reads from one source and saves +updates to another, the handle must record both, for example `Environment(provider=:forcing, sink=:canopy)`. Model-level `environment_hint(...)` can provide default source bindings and @@ -346,8 +363,8 @@ Run a model with: sim = run!(model; steps=30) ``` -The returned `Simulation` contains the mutated model, compiled bindings, -environment bindings, execution plan, and retained temporal output streams. +The returned `Simulation` keeps the current model values, the prepared +connections and schedule, and any saved output histories. By default, model runs retain no user output streams. Pass `outputs=:all` to retain every published stream, or pass `OutputRequest` values to retain only @@ -417,36 +434,39 @@ move_object!(model, :leaf_4, new_geometry) update_geometry!(model, :leaf_5, new_geometry) ``` -Use `add_organ!` for an MTG-backed model. It creates the MTG node, initializes -and attaches its `Status`, registers the model object, and invalidates the -affected bindings. By default, status initialization reuses the model's MTG -policy. A framework that already supplies the complete creation attributes and -initial status may set `use_status_adapter=false`; doing so is an explicit -assertion that the configured status accessor contributes no additional fields -or side effects for that node. `register_object!` is the low-level operation -for callers that already own a complete `Object`. - -Structural changes invalidate compiled object/model bindings. Movement and -geometry changes invalidate environment bindings without rebuilding structural -input carriers. Scenario-level application, dependency, selector, cadence, -environment-sampling, and output-retention plans remain immutable; only the -affected object targets and buffers are refreshed. - -Do not mutate `Object` topology, labels, or geometry fields directly. Direct -field mutation bypasses registry indexes and cache invalidation and is -unsupported. Use the lifecycle functions above. They validate prerequisites -before mutating; in particular, `reparent_object!` rejects self-parenting and -descendant cycles without changing existing links. `ObjectInstance` roots are -immutable lifecycle anchors: removing or reparenting a root, or an ancestor -whose subtree contains one, is rejected atomically. Ordinary descendants may -still be added, removed, or reparented. - -Inside a lifecycle-capable model kernel, use `runtime_model(context)` to obtain -the live model. Objects created during a kernel call do not recursively execute -inside that call. At the safe barrier after the mutating application, -PlantSimEngine refreshes affected structural targets, value carriers, hard-call -targets, writer validation, temporal storage, execution groups, output-request -matches, and environment handles. A new object can therefore run an application +Use `add_organ!` when the plant structure comes from an MTG. It creates the +node, prepares its starting values, adds the object to the simulation, and +marks its connections for updating. By default, it reuses the function you +provided to initialize values from MTG nodes. + +Set `use_status_adapter=false` only if you supply all the new organ's +attributes and values yourself, and that initialization function has no +additional work to do. Use `register_object!` when you already have a fully +prepared `Object` to add. + +Adding or removing objects can change which models run and where their +inputs come from, so PlantSimEngine updates the affected connections. Moving +an object or changing its shape updates its environment connection without +rebuilding unrelated model connections. The rules you supplied in the +simulation setup stay the same. + +Use the functions above to change objects; assigning directly to their +structure, label, or geometry fields would leave PlantSimEngine's stored +connections out of date. The functions check each change before applying it. +For example, `reparent_object!` prevents an object from becoming its own +parent or a descendant of itself. + +The root of an `ObjectInstance` must stay in place. You cannot remove or +reparent that root, or an ancestor whose descendants contain it. Rejected +operations leave the existing structure unchanged. You can still add, +remove, or reparent ordinary descendants of the instance root. + +Inside a model's `run!` function, use `runtime_model(context)` to access the +simulation model when creating or changing objects. Creating an object does +not immediately run its models. After the application that made the change +finishes, PlantSimEngine updates the affected model selections, input and +environment connections, manual calls, output histories, and execution order. +It also checks that output variables still have valid sources. A new object can therefore run an application that remains later in the same timestep. It does not retroactively run an application that already completed unless its creator declares that application as an [`Initializer`](@ref) and explicitly calls [`run_initializer!`](@ref) on diff --git a/docs/src/model_traits.md b/docs/src/model_traits.md index 3dee94758..a6adddaa5 100644 --- a/docs/src/model_traits.md +++ b/docs/src/model_traits.md @@ -1,8 +1,12 @@ # Model Traits -Model traits describe intrinsic model behavior. Scenario-specific coupling -belongs in `ModelSpec` through `on`, `inputs`, `calls`, `every`, -`Environment`, `output_routing`, and `Updates`. +A **trait** is a function that tells PlantSimEngine something about a model: +which values it needs, which values it calculates, or how often it should run. +This page lists the declarations a model author can provide. For a complete +example, start with [writing a first model](journeys/modelers/basic_model.md). + +Use `ModelSpec` for choices that belong to a particular simulation, such as +which leaves use the model and where their inputs come from. ## Variables @@ -16,22 +20,22 @@ PlantSimEngine.inputs_(::MyModel) = ( PlantSimEngine.outputs_(::MyModel) = (assimilation=0.0,) ``` -`Required(T)` means the value must be present on the target object's `Status` -or bound from another application. `T` is an expected type, not a placeholder -value. It may be abstract or parametric, so use the scientific type contract -instead of forcing `Float64`. +`Required(T)` means the object must already have this value, or another +model must provide it. `T` describes the kind of value expected, such as +`Float64` for a floating-point number. It does not supply a starting value. +Choose a type that fits the calculation; it need not always be `Float64`. -`Default(value)` means the model can run without user initialization or a -producer for that input. PlantSimEngine installs a private copy of `value` on -each target object when the value is absent. Mutable defaults are therefore -not shared between objects. +`Default(value)` supplies a starting value when neither the user nor another +model provides one. Each object gets its own copy. For example, changing a +default array on one leaf does not change the array on another leaf. -Output literals remain initial output-state values. In the example, -`assimilation` starts at `0.0` before the first accepted model call. +The values in `outputs_` are the outputs' starting values. In this example, +`assimilation` starts at `0.0` before the model first runs. -These declarations are used for status initialization, dependency inference, -validation, and type construction. Plain input literals are rejected because -they do not say whether the value is required or genuinely optional. +PlantSimEngine uses these declarations to prepare each object's values, +connect models, and find missing inputs. Writing a plain number in `inputs_` +is rejected: it would not say whether the model needs a supplied value or +can use a default. Use `init_variables(model)` to inspect only values PlantSimEngine can initialize by itself: `Default` input values and output initial values. @@ -39,13 +43,15 @@ Required inputs are intentionally omitted. Before running a scenario, `Diagnostics.explain_initialization(model)` classifies inputs as `:required`, `:defaulted`, `:supplied`, or `:producer_bound`. A -`:required` row must be resolved before compilation can succeed. +`:required` row identifies an input you must supply before the simulation +can be prepared. ## Scientific meaning and dimensions Names and Julia types are not enough to distinguish, for example, daily PAR per ground area from daily PAR per plant. Add a `VariableContract` when a -variable participates in scientific coupling: +value will pass between models. A **variable contract** records its unit +and physical meaning: ```julia const PLANT_DAILY_PAR = VariableContract( @@ -64,33 +70,35 @@ PlantSimEngine.variable_contracts_(::PlantGrowth) = ( ) ``` -The tokens are open symbols, but a connected producer and consumer must declare -the same complete contract. Once either side declares one, a missing contract -on the other side is also a compilation error. Renaming a variable with -`var=...` does not convert its meaning; put unit, basis, or time conversion in -an explicit model boundary. +You choose the labels, such as `:plant` and `:day`. The model supplying the +value and the model reading it must declare the same complete contract. If +one declares a contract and the other does not, PlantSimEngine reports an +error before running. Renaming a variable with `var=...` does not convert +its units or meaning. Use a separate model for a conversion, such as +multiplying a quantity per unit area by plant area. -`VariableContract` is metadata only. Status and environment payloads remain -ordinary numbers, arrays, unit-bearing values, or automatic-differentiation -values, so the contract adds no wrapper to a model's numerical hot loop. +`VariableContract` describes the values without changing how they are +stored. Your equations still receive numbers or arrays, including compatible +types that carry units, uncertainty, or derivatives. Use `variable_contracts(model)` to inspect the validated declarations. Contract keys must occur in one of the model's declared status or environment traits, or in the compiled application's distributed `outputs_to` declaration. -| Role | Declaration | Ownership | +| Value or operation | Declaration | Where it comes from or goes | |---|---|---| -| Status input | `inputs_` | object state or a compiled model producer | -| Environment input | `environment_inputs_` | the active forcing backend | -| Constant | the `constants` argument | simulation configuration, not mutable status | -| Manual model call | `dep` plus `ModelSpec(...; calls=...)` | explicit callee execution; the callee keeps its own variable declarations | -| Local status output | `outputs_` | the target object's runtime status | -| Distributed output | `ModelSpec(...; outputs_to=...)` | explicitly selected destination objects | +| Object input | `inputs_` | A value on the object or an output from another model | +| Environment input | `environment_inputs_` | Weather data or local growing conditions | +| Constant | the `constants` argument | Values supplied for the simulation, such as physical constants | +| Manual model call | `dep` plus `ModelSpec(...; calls=...)` | Another model that this model decides when to run | +| Output on the current object | `outputs_` | The object on which this model runs | +| Outputs on other objects | `ModelSpec(...; outputs_to=...)` | Selected objects, such as leaves receiving a scene light calculation | ## Manual Dependencies -Implement `dep(model)` only when the model directly calls another process from -inside its own `run!` method: +Use `dep(model)` to suggest default input connections with `Input`, or models +to call with `Call`. A `Call` declaration is needed when the model directly +runs another process from inside its own `run!` function: ```julia PlantSimEngine.dep(::EnergyBalance) = ( @@ -99,10 +107,11 @@ PlantSimEngine.dep(::EnergyBalance) = ( ``` The scenario may override that default selector with -`ModelSpec(...; calls=...)`. The parent executes all resolved targets with -`run_call!(context, :photosynthesis)`, which always returns a vector-like -collection. Use `call_targets` plus `run_call!(target)` when the parent needs -selective trials and accepted publication. +`ModelSpec(...; calls=...)`. The calling model runs all selected photosynthesis models with +`run_call!(context, :photosynthesis)`, which returns a collection even when +there is only one model. Use `call_targets` to inspect that collection first +and `run_call!(target)` to run individual entries. This lets you try values +and record only the accepted result with `publish=true`. ## Timing @@ -113,11 +122,17 @@ selective trials and accepted publication. PlantSimEngine.timespec(::Type{<:DailyGrowth}) = ClockSpec(24.0, 1.0) ``` +Here, `24.0` means every 24 simulation steps and `1.0` sets the first +execution at step 1: the model runs at steps 1, 25, 49, and so on. This is +daily execution only when the simulation uses hourly steps. + Use `ModelSpec(...; every=Dates.Day(1))` when the cadence should be expressed -as a physical duration relative to the scenario environment. A +in hours or days relative to the simulation's weather time step. A `Dates.Period` is not a `ClockSpec` constructor argument. -`output_policy(model)` declares the default temporal policy per output: +`output_policy(model)` says how another model should read an output when +the two models run at different time steps. For example, it can request a +sum or a mean over a time window: ```julia PlantSimEngine.output_policy(::Type{<:MyModel}) = ( @@ -147,23 +162,31 @@ PlantSimEngine.environment_inputs_(::LeafEnergyBalance) = ( ) ``` -Mutable microclimate updates should be committed explicitly by controller -models: +If a controller changes growing conditions, declare the variables it may +update with `environment_outputs_`. For example, a canopy controller that +adjusts temperature declares: + +```julia +PlantSimEngine.environment_outputs_(::CanopyController) = (T=0.0,) +``` + +Once its calculation is accepted, the controller saves the new conditions +with `commit_environment!`. This operation requires the output declaration: ```julia commit_environment!(context, accepted_environment) ``` -For trial solves, pass a backend-specific state with `environment` so each -hard-called model keeps its compiled provider or spatial handle: +To try temporary growing conditions, pass them through `environment`. Each +called model still receives the conditions for its own location: ```julia run_call!(context, :leaf_energy; environment=trial_environment, publish=false) ``` -Diagnostic variables such as canopy temperature or vapor-pressure deficit can -still be regular `outputs_`, but status fields are not the transport mechanism -for mutable environment state. +You can also record canopy temperature or vapor-pressure deficit in +`outputs_`. Changing these outputs alone does not update the environment +that other models read; use `commit_environment!` for that. ## Precedence diff --git a/docs/src/planned_features.md b/docs/src/planned_features.md index af2e89cc4..657f27a60 100644 --- a/docs/src/planned_features.md +++ b/docs/src/planned_features.md @@ -1,20 +1,21 @@ # Roadmap -PlantSimEngine now has one composite-model/object runtime for single-object, multiscale, -multi-plant, soil, microclimate, and multirate simulations. +PlantSimEngine uses the same simulation engine for a single object or many +plants and organs, including models that run at different time steps. This +page lists work planned to extend and check these capabilities. Current priorities are: - migrate downstream model packages to `CompositeModel`, `CompositeModelTemplate`, `ObjectInstance`, and `ModelSpec`; - strengthen type-stability and allocation tests for million-object workloads; -- add broader lifecycle tests for object creation, removal, movement, and - environment-index refresh; -- improve diagnostics for ambiguous selectors, writer conflicts, and temporal - policies; +- test more combinations of adding, removing, and moving objects, including + updates to their local growing conditions; +- make error reports clearer when object selections match too many objects, + models try to set the same output, or time-step connections need attention; - validate mutable voxel, layer, and octree microclimate backends; -- expand downstream release gates and performance benchmarks; -- evaluate parallel execution for independent compiled application batches. +- test more dependent packages and simulation performance before releases; +- investigate running independent groups of model calculations in parallel. ## Environment and microclimate work @@ -43,11 +44,10 @@ commit_environment!(context, accepted_environment) run_call!(context, :leaf_energy; publish=true) ``` -The transient state is interpreted by each target backend through its opaque -compiled handle, so one call can sample different cells for different leaves. -`commit_environment!` commits only the accepted state to a mutable backend. -Future work is to validate full voxel, layer, and octree implementations on -this same model-side API. +Each leaf still receives conditions for its own location, so one call can +supply different trial values to different leaves. `commit_environment!` +saves only the accepted growing conditions. Future work will test this +approach with environments represented by cells, layers, and octrees. The full issue list is available on [GitHub](https://github.com/VirtualPlantLab/PlantSimEngine.jl/issues). diff --git a/docs/src/prerequisites/installing_plantsimengine.md b/docs/src/prerequisites/installing_plantsimengine.md index d553d0373..b4801a847 100644 --- a/docs/src/prerequisites/installing_plantsimengine.md +++ b/docs/src/prerequisites/installing_plantsimengine.md @@ -10,7 +10,8 @@ Install Julia from the [official download page](https://julialang.org/downloads/), create a project -environment, and install the packages used in the tutorials: +environment (a folder that records the packages used for this simulation), +and install the tutorial packages: ```julia using Pkg @@ -20,7 +21,8 @@ Pkg.add(url="https://github.com/VirtualPlantLab/PlantSimEngine.jl", rev="main") ``` PlantMeteo supplies weather data, DataFrames organizes the results, and -CairoMakie draws the tutorial figures. Run these commands in the Julia REPL; +CairoMakie draws the tutorial figures. Run these commands at Julia's +interactive prompt, also called the **REPL**; `my_simulation` is the project directory created relative to your current working directory. In a later session, run `Pkg.activate("my_simulation")` from the same location before using the project again. @@ -60,10 +62,11 @@ simulation = run!(model) (absorbed_PAR_umol_m2_ground_s=final_state(simulation).aPPFD,) ``` -Example models are provided by the `PlantSimEngine.Examples` submodule. They -are useful for learning and tests but are not part of the core modeling API. +`using PlantSimEngine.Examples` loads the example models, including `Beer`. +These models are included for learning and testing. -The result is absorbed photon flux in μmol m⁻² of ground s⁻¹. The inputs are +The result is the amount of light absorbed by the canopy, expressed in μmol +of photons per m² of ground per second. The inputs are illustrative, not a calibrated crop scenario. Next, [couple three models over time](../journeys/users/one_object.md), then [collect and plot their outputs](../guides/data/outputs_plotting.md). diff --git a/docs/src/prerequisites/julia_basics.md b/docs/src/prerequisites/julia_basics.md index 707ad6122..8992cbabd 100644 --- a/docs/src/prerequisites/julia_basics.md +++ b/docs/src/prerequisites/julia_basics.md @@ -1,53 +1,66 @@ # 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 : [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. - -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 page is here to list to the parts of Julia that are most relevant regarding usage of PlantSimEngine, and point to resources that can help you grasp those basics. +You can run the PlantSimEngine tutorials without knowing all of Julia. Start +by copying a complete example, then change one parameter and compare the +result. This page explains a few patterns you will see along the way. ## New to programming -It is not meant as a full-fledged from-scratch 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/). The video course [Julia Programming for Nervous Beginners](https://www.youtube.com/playlist?list=PLP8iPy9hna6Qpx0MgGyElJ5qFlaIXYf1R) is tailored for people with no programming experience. - -## Installing packages and setting up and environment - -For PlantSimEngine, you can check our documentation page on the topic: -[Installing PlantSimEngine](installing_plantsimengine.md). - -## Cheatsheets - -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 its [install instructions](https://palmstudio.github.io/Biophysics_database_palm/installation/). +The [Julia getting-started guide](https://docs.julialang.org/en/v1/manual/getting-started/) +explains how to start Julia and enter commands. If you prefer a video course, +[Julia Programming for Nervous Beginners](https://www.youtube.com/playlist?list=PLP8iPy9hna6Qpx0MgGyElJ5qFlaIXYf1R) +is aimed at people with no programming experience. -## Troubleshooting - -For more Julia learning-related difficulties, you will find quick responses on the Discourse forum: [https://discourse.julialang.org](https://discourse.julialang.org). - -### Noteworthy differences with other languages: - -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. +## Installing packages and setting up an environment -(Array indexing starts at 1, for example) +Follow [Installing PlantSimEngine](installing_plantsimengine.md) to create a +project folder and install the tutorial packages. A project environment +records which packages and versions your simulation uses. ## Essential Julia concepts for PlantSimEngine -Here's a list of the main aspects of the Julia language required (beyond package management) to understand how to use PlantSimEngine to its potential: +The first tutorials mainly use these patterns: + +| Code | Meaning | +|---|---| +| `lai = 2.0` | Store a value under the name `lai` | +| `Beer(0.6)` | Create a Beer model with an extinction coefficient of 0.6 | +| `run!(model; steps=30)` | Run a function, with the named option `steps=30` | +| `(LAI=2.0, TT=12.0)` | Group named values in a **named tuple** | +| `[1.0, 2.0, 3.0]` | Create an array of three values | +| `values[1]` | Read the first array entry; Julia indexing starts at 1 | +| `state.LAI` | Read the value named `LAI` from `state` | +| `values .* 2` | Multiply every array entry by 2 | + +Options such as `steps=30` are called **keyword arguments**. The semicolon +separates these named options from the other arguments. A dot before an +operator, as in `.*`, applies the operation to each array entry. Julia calls +this **broadcasting**. + +A function name ending in `!`, such as `run!` or `step!`, usually means that +the function changes something it was given. Here, running a simulation +updates its objects' values. + +When you start writing models, you will also meet **types** and **methods**. +A type describes a kind of value; a model type can store its parameters. +A method is a version of a function for particular types of arguments. +The [first model tutorial](../journeys/modelers/basic_model.md) introduces +these ideas with a complete equation and its parameters. -Standard notions and constructs: - -- Standard concepts of a variable, arrays, functions, function arguments -- The typing system and custom types -- Dictionaries and NamedTuple objects are used throughout the codebase - -The Julia manual goes more in-depth than lighter introductions to some of these topics, so might be more useful as a reference than a starting point. You might find other guides or courses, such as the first section in [https://julia.quantecon.org/intro.html](https://julia.quantecon.org/intro.html), chapters 0-4 and 7 of the [Learn Julia the Hard Way](https://scls.gitbooks.io/ljthw/content/) draft or the interactive [Mathigon course](https://mathigon.org/course/programming-in-julia/introduction). +## Cheatsheets -Also of importance: +The [Julia Data Science basics](https://juliadatascience.io/julia_basics) +cover common syntax and working with tables. There are also +[cheatsheets](https://palmstudio.github.io/Biophysics_database_palm/cheatsheets/) +and a [short introductory notebook](https://palmstudio.github.io/Biophysics_database_palm/basic_syntax/). -- [Keyword arguments](https://docs.julialang.org/en/v1/manual/functions/#Keyword-Arguments) (kwargs) are present in many API functions -- [Type promotion](https://docs.julialang.org/en/v1/manual/conversion-and-promotion/#Promotion), [splatting](https://docs.julialang.org/en/v1/base/base/#...), [broadcasting](https://docs.julialang.org/en/v1/manual/functions/#man-vectorized), and [comprehensions](https://docs.julialang.org/en/v1/manual/arrays/#man-comprehensions) are also very useful, but not compulsory to get started +## Troubleshooting -Many of these are also briefly presented in [this Julia Data Science](https://juliadatascience.io/julia_basics) guide, which also happens to focus on the DataFrames.jl package. +Ask Julia language questions on [Julia Discourse](https://discourse.julialang.org). +For errors from PlantSimEngine, use the +[common errors guide](../troubleshooting/common_errors.md). -Understanding more about methods, parametric types and the typing system is usually worthwhile, when working with Julia packages. +If you know R, Python, or MATLAB, Julia's +[comparison with other languages](https://docs.julialang.org/en/v1/manual/noteworthy-differences/) +explains differences you may encounter. You can read about more advanced +features, such as type promotion and parametric types, when a model needs them. diff --git a/docs/src/step_by_step/advanced_coupling.md b/docs/src/step_by_step/advanced_coupling.md index 907953a44..e5efe7d6c 100644 --- a/docs/src/step_by_step/advanced_coupling.md +++ b/docs/src/step_by_step/advanced_coupling.md @@ -10,44 +10,49 @@ meteo_day = read_weather( ) ``` -Most model coupling is a value dependency: one model writes an output, another -model reads it as an input. Some models need tighter control. For example, an -energy-balance model may call photosynthesis and stomatal-conductance models -several times while it iterates leaf temperature. +Usually, coupling means that one model calculates a value and another reads +it. Sometimes a model must also decide when the other calculation runs. For +example, an energy-balance model may run photosynthesis and +stomatal-conductance models several times while trying to find the leaf +temperature that balances heat gains and losses. -That second case is a manual call dependency. In the composite-model/object API it is -declared with `ModelSpec(...; calls=...)`. +That second case is called a **manual call** or **hard dependency**. Declare +it with `ModelSpec(...; calls=...)`. ## Soft inputs and manual calls -Use `ModelSpec(...; inputs=...)` or inferred same-object bindings when a model only needs a -value. Use `ModelSpec(...; calls=...)` when the parent model must directly run another model -inside its own `run!` method. +An ordinary input connection is also called a **soft dependency**. Set it +with `ModelSpec(...; inputs=...)`. PlantSimEngine connects inputs automatically +on the same object when exactly one model supplies the required variable. +Use `ModelSpec(...; calls=...)` when one model must run another from inside +its own `run!` function. The example process models in `examples/dummy.jl` contain both patterns: - `Process4Model` computes `var1` and `var2`; -- `Process1Model` consumes `var1` and `var2` and computes `var3`; +- `Process1Model` reads `var1` and `var2` and computes `var3`; - `Process2Model` manually calls process 1, then computes `var4` and `var5`; - `Process3Model` manually calls process 2, then computes `var6`; -- `Process5Model`, `Process6Model`, and `Process7Model` use regular soft - value dependencies. +- `Process5Model`, `Process6Model`, and `Process7Model` read other models' + results through ordinary inputs. ## Declaring manual calls in the scenario -`ModelSpec(...; calls=...)` is scenario-level wiring. The model kernel remains generic; the -scenario decides which concrete application is called. +The simulation setup chooses which models are called. The model's `run!` +function can then keep the same equations when those choices change. -Use `application=...` in scenario-level `ModelSpec(...; calls=...)` and `ModelSpec(...; inputs=...)` when you -know which mounted model application should provide the value or be called. Use -process identities in model-level contracts such as `dep(model)`, where the -model author only declares that a compatible process is required and cannot know -the names chosen by future scenarios. +A **model application** is a model configured with a name, selected objects, +and any input or timing settings. In the setup below, use `application=...` +to choose that name in a call or input connection. -This split avoids ambiguity when several applications implement the same -process. For example, two soil-water applications can share the same process but -represent different layers, parameter sets, objects, or time steps. A scenario -selector should name the application that has the intended role. +This matters when you use the same process in several places. For example, +two soil-water models may represent different layers. Naming the application +lets you choose the intended layer. + +When writing a reusable model, you will not know the names that future +simulations choose. Its `dep(model)` declaration can instead ask for the +scientific process it needs. The simulation then connects that request to +an application. ```@example scene_advanced_coupling complex_scene = CompositeModel( @@ -80,15 +85,15 @@ select( ) ``` -Applications selected by `ModelSpec(...; calls=...)` are not scheduled as independent root -applications under their caller. They run only when the parent calls them. -This gives the parent full call-stack control. +The table shows which model each named call will run. Models used only +through these calls run when their caller asks them to; they do not also +run independently at each time step. ## Running the coupled model -The regular soft dependencies are still inferred from `inputs_` and -`outputs_`. The scheduler combines those soft edges with the call ownership -rules: +PlantSimEngine still connects ordinary inputs from the models' `inputs_` +and `outputs_` declarations. It uses those connections and the manual calls +to determine which calculations run first. Inspect that order: ```@example scene_advanced_coupling select( @@ -115,16 +120,16 @@ complex_status = final_state(complex_sim) ## Writing new hard-coupled models -For new composite-model/object models, execute all targets directly when they -share meteorology and publication policy: +Inside a model, run all models and objects selected by a named call with: ```julia targets = run_call!(context, :leaf_energy; publish=true) ``` -The result is always vector-like. Retrieve targets without executing them when -an algorithm needs selective execution or an already sampled environment for -each target: +The result is a collection of **targets**, each representing a called model +on one object. To choose individual targets before running them, use +`call_targets`. This also lets you pass different environmental values to +each one: ```julia targets = call_targets(context, :leaf_energy) @@ -137,8 +142,10 @@ for (target, leaf_environment) in zip(targets, environments_by_leaf) end ``` -For a provider-aware trial state shared by the call, keep the execute-all form. -Each target still samples through its own compiled handle: +If an environment provider supplies values for different positions, you can +give it a trial state for the whole call. PlantSimEngine then reads the +appropriate values for each target's location. The following outline shows +the order; your model must define how to calculate and accept a trial: ```julia function PlantSimEngine.run!(model::SceneEnergyBalance, status, environment, @@ -154,14 +161,16 @@ function PlantSimEngine.run!(model::SceneEnergyBalance, status, environment, end ``` -`run_call!` defaults to `publish=false`, which is useful for trial iterations. -Pass non-committing trial state with the `environment` keyword. Use -`commit_environment!` and `publish=true` for the accepted state so temporal -streams and mutable environment state are published once. - -The MAESPA-style example uses the same mechanism: a model energy-balance model -calls all selected leaf energy-balance models and the shared soil model while -it solves canopy microclimate. - -Scenario wiring uses `ModelSpec(...; calls=...)`. Model authors should keep kernels generic -and only require manual calls when the model really needs call-stack control. +`run_call!` defaults to `publish=false`: trial results are not saved as +accepted samples for output history or time-based connections. Pass a trial +environment with the `environment` keyword. Once you accept a solution, +`commit_environment!` writes its environmental values, and a call with +`publish=true` saves the accepted result. Trial calculations can still change +the objects' current values, so your algorithm must handle any changes it +needs to discard. + +The MAESPA-style example uses the same approach: a canopy energy-balance +model calls the leaf energy-balance models and the shared soil model while +it solves canopy microclimate. Use manual calls for calculations that need +this control. An ordinary input is enough when a model simply reads +another model's result. diff --git a/docs/src/step_by_step/implement_a_process.md b/docs/src/step_by_step/implement_a_process.md index 8894ba07d..8ed6b645e 100644 --- a/docs/src/step_by_step/implement_a_process.md +++ b/docs/src/step_by_step/implement_a_process.md @@ -9,7 +9,7 @@ users find your implementation beside the alternatives they may want to compare. ## Find the existing family -Load the package that owns the models. This example uses the teaching models +Load the package that provides the models. This example uses the teaching models distributed with PlantSimEngine: ```@example choose_process @@ -22,10 +22,10 @@ growth_models For a scientific application, load its model package first and inspect its documentation. `Authoring.available_processes()` lists the process types -visible in the loaded modules. `Authoring.available_models(process_type)` -then lists that family's concrete model types. +available in the packages you have loaded. `Authoring.available_models(process_type)` +then lists the model types that implement that process. -Choose a candidate and inspect an actual parameterized instance: +Choose a model, give it a parameter value, and inspect its inputs and outputs: ```@example choose_process candidate = ToyRUEGrowthModel(0.2) @@ -37,19 +37,21 @@ candidate = ToyRUEGrowthModel(0.2) ``` [Loaded model catalog](@ref) explains discovery and inspection in more detail. -Discovery only sees packages loaded into Julia; also check the target -package's source and documentation before concluding that a process is absent. +These functions only find packages loaded into Julia. Also check the package +you plan to use before concluding that a process is missing. ## Decide what your new equation changes | Your change | What to create | |---|---| -| Another equation, parameterization, assumption, or resolution for the same question | A concrete model under the existing process | +| Another equation, assumption, or level of detail for the same question | A model under the existing process | | A distinct biological or physical question | A new process and its model | -| A conversion of units, basis, or aggregation between models | An explicit adapter model | +| A conversion such as radiation per square metre to radiation per plant | A small conversion model, called an adapter | -Give alternative hypotheses separate model types so users can select and test -them. Models in one process may have different inputs, outputs, or cadences. +Give alternative equations separate model types so users can select and test +them. To compare parameter values in the same equation, create instances of +that model with different parameters. Models in one process may need +different inputs, produce different outputs, or run at different frequencies. Use [Model compatibility and replacement](@ref) before substituting one. ## Declare a genuinely new process @@ -74,8 +76,10 @@ process(DocsLinearExudation(0.1)) | `@process "light_interception"` | `AbstractLight_InterceptionModel` | When another package already declares the process, import its abstract type -and subtype it. Avoid declaring a second identity with the same meaning. +and use it after `<:` in your model definition. This puts your model in the +same family as the existing alternatives. The type above is only the beginning. It still needs input and output -declarations, scientific contracts, an equation, and tests. Continue with +declarations, descriptions of the variables' units and meaning, an equation, +and tests. Continue with [Implement a basic model](@ref) to complete those steps. diff --git a/docs/src/step_by_step/model_switching.md b/docs/src/step_by_step/model_switching.md index 9fb91131e..c27ca7da4 100644 --- a/docs/src/step_by_step/model_switching.md +++ b/docs/src/step_by_step/model_switching.md @@ -32,11 +32,11 @@ water_limited = WaterLimitedCarbonGain(0.2) Absorbed PAR is a daily total in mol photons per plant; carbon gain is a daily total in g carbon per plant. The soil-water fraction is dimensionless. The -models declare these meanings in their variable contracts. +models record these units and meanings in `VariableContract` declarations. ## Check before changing a scenario -Compare the concrete model instances: +Compare the models with their chosen parameters: ```@example scene_model_switching compatible = Authoring.compare_models(linear, saturating) @@ -50,23 +50,26 @@ needs_water = Authoring.compare_models(linear, water_limited) ) ``` -The first pair has a compatible complete interface. The water-limited model -belongs to the same process, but its extra `ftsw` input needs a value. +The linear and saturating models can replace each other directly: they read +and write the same kinds of values and have compatible settings. The +water-limited model answers the same question, but it also needs `ftsw`, the +fraction of transpirable soil water. Three distinctions matter: - **Same process:** the models answer the same scientific question. -- **Usable in this scenario:** the candidate supplies what consumers need and - can obtain all its own inputs. -- **Direct override:** the complete process, inputs, outputs, contracts, - dependencies, and relevant traits are compatible. +- **Usable in this scenario:** the new model can get all its inputs and + provide the results needed by the models connected to it. +- **Direct override:** you can replace the model without changing its + connections or other settings. PlantSimEngine checks the process, variables + and their physical meaning, calls to other models, and timing requirements. Use `Override` only for the third case. For broader changes, replace the model in a `ModelSpec` and update the affected inputs or other configuration. ## Compare the two light responses -Keep the objects, forcing, and timing identical: +Keep the plants, input data, and timing identical: ```@example scene_model_switching function carbon_scenario(gain_model; values=(absorbed_par=10.0,)) @@ -92,7 +95,8 @@ saturating_gain = final_state(run!(saturating_scene)).carbon_gain Only the selected hypothesis changed. At 10 mol of absorbed photons the linear equation produces 2 g carbon and the saturating equation about 6.67 g carbon. These different outcomes reflect the chosen teaching -coefficients; interface compatibility does not imply equal results. +coefficients. Models that can replace each other do not have to give the +same result: comparing those results is the purpose of the experiment. ## Supply the additional water input @@ -112,24 +116,26 @@ water_gain = final_state(run!(water_scene)).carbon_gain (linear=linear_gain, water_limited=water_gain) ``` -This is an explicitly supplied value. In a dynamic simulation, bind `ftsw` -to a soil model or forcing data using [Coupling models](@ref). Inspect +Here we supplied the soil-water fraction ourselves. To let it change during +a simulation, read it from a soil model or dataset using [Coupling models](@ref). Inspect `Diagnostics.explain_initialization(water_scene)` to see how each required input was supplied, then `Diagnostics.explain_bindings(water_scene)` for -connections to other model applications. +connections to other models. ## Keep physical meaning and scientific validation visible -Two variables with the same name can still differ in units, area or mass -basis, temporal meaning, or aggregation. Add a named conversion model when -those meanings differ; see [explicit adapters](../guides/coupling.md). +Two variables with the same name may use different units or describe +different quantities. Check whether each value is per plant or per unit +area, and whether it is a rate, a mean, or a total. If a conversion is needed, +write it as a small model; see [explicit adapters](../guides/coupling.md). `Authoring.compare_models` reports differences in declarations. -`Authoring.validate_scenario` checks the proposed composition. Neither proves -that an equation or its parameterization is valid for your study. Compare -assumptions, domain of validity, reference data, and outputs as well. +`Authoring.validate_scenario` checks whether the models can work together in +your setup. Neither proves that an equation or its parameters are valid for +your study. Also compare assumptions, the conditions in which the models +have been tested, reference data, and simulation results. For detailed reports, inspect `requires_binding_changes` for connection changes and `requires_reconfiguration` for all differences that prevent a -direct override, including cadence changes. The [Public API](@ref) describes +direct override, including changes to how often a model runs. The [Public API](@ref) describes the complete report. diff --git a/docs/src/troubleshooting/common_errors.md b/docs/src/troubleshooting/common_errors.md index 5caa284f8..6c75af856 100644 --- a/docs/src/troubleshooting/common_errors.md +++ b/docs/src/troubleshooting/common_errors.md @@ -1,22 +1,25 @@ # Common Errors -Start with the application, object, and variable named in the error. They tell -you which part of the configuration needs attention. Many connection errors -are detected before any equation runs. +Start with the model application, object, and variable named in the error. +An application is a model configured to run on selected objects. These names +tell you where to look: for example, a light calculation on a particular +canopy may be missing LAI. PlantSimEngine catches many such problems before +running any equation. | Symptom | What it means | First action | |:--|:--|:--| -| A required input is missing | Neither initial status nor another application supplies it | Supply a measured/initial value or connect a producer | -| A selector finds too few or too many objects | The matches do not satisfy `One`, `OptionalOne`, or `Many` | Check object labels and the search scope | -| More than one source matches | The source application is ambiguous | Name the intended `application` and, if needed, `var` | -| Several models write the same variable | Canonical status has competing writers | Decide which model owns the value; use `Updates` only for intentional ordered updates | -| Variable contracts differ | Units, physical basis, or another declared meaning differ | Check the equations and add an explicit conversion model if appropriate | -| A cadence is rejected | The period does not fit the base step, or an implicit cadence violates a model's hint | [Choose compatible time steps](../guides/time/advanced_time_environment.md) | -| There is a dependency cycle | No valid same-step execution order exists | Decide whether the science requires a lag or an iterative solution | +| A required input is missing | You have not supplied a starting value or a model that calculates it | Supply the value or connect a model that provides it | +| A selector finds too few or too many objects | For example, `One` expects one match but finds two | Check the object labels and where the selector searches | +| More than one source matches | Several models could supply the input | Name the intended `application` and, if needed, `var` | +| Several models set the same variable | PlantSimEngine cannot decide which value to keep | Choose one model, or use `Updates` if one model is meant to change another's result | +| Variable contracts differ | The connected variables declare different units or physical meanings | Check what each equation expects; add a conversion model where appropriate | +| A cadence is rejected | The chosen interval does not fit the base step or is not supported by the model | [Choose compatible time steps](../guides/time/advanced_time_environment.md) | +| There is a dependency cycle | Two or more models each wait for a result from the others | Decide whether one input should come from the previous step or the equations must be solved together | ## Example: the light model needs LAI -`Beer` reads LAI from status. With no producer, you must supply it: +`Beer` needs the canopy's LAI to calculate absorbed light. If no other model +calculates LAI, supply its value yourself: ```@example missing_lai using PlantSimEngine @@ -44,11 +47,12 @@ to silence an error can pass a vector to an equation that expects one value. ## Errors while writing a model -Declare each status input with `Required(T)` or `Default(value)`, for example +Declare each status input with `Required(T)` if it must be supplied, or +`Default(value)` if the model provides a fallback value. For example, use `inputs_(::MyModel) = (LAI=Required(Real),)` with the function qualified as -`PlantSimEngine.inputs_`. A plain literal in `inputs_` is not a declaration of -required state. Define the equation as `PlantSimEngine.run!(...)` so Julia -extends the package function. +`PlantSimEngine.inputs_`. Writing a number directly in `inputs_` does not +declare a required input. Define the equation as `PlantSimEngine.run!(...)` +so that Julia adds your model's method to the package function. For the complete sequence, see [Write and test a first model](../journeys/modelers/basic_model.md). For further investigation, see [Inspect a simulation](runtime_contracts.md) diff --git a/docs/src/troubleshooting/dependency_cycles.md b/docs/src/troubleshooting/dependency_cycles.md index 312321816..db1979aa9 100644 --- a/docs/src/troubleshooting/dependency_cycles.md +++ b/docs/src/troubleshooting/dependency_cycles.md @@ -1,17 +1,17 @@ # Diagnosing Dependency Cycles -A same-step value cycle is rejected because no valid execution order exists. -Read the reported application, object, and variable edges. If the science uses -yesterday's value, put `PreviousTimeStep(:variable)` on that input. If the -science requires convergence in the current step, make one parent application -own child trials with `calls`. Otherwise reformulate the coupled equations. +A **dependency cycle** occurs when two or more models each need a new result +from the others before they can run. For example, a leaf model needs water +from a root model, but the root model needs carbon from the leaf model. +Neither can go first, so PlantSimEngine reports the problem before running +the equations. The error names the applications, objects, and variables +involved. -Application declaration order is not a cycle-resolution mechanism. - -For example, if application `:leaf` reads same-step `water` from `:root` while -`:root` reads same-step `carbon` from `:leaf`, compilation fails before either -kernel runs. If root water scientifically affects tomorrow's leaf carbon, -change only that edge: +Choose the solution that matches your equations. If the leaf should use the +root's water from the previous step, mark that input with +`PreviousTimeStep(:water)`. For a daily simulation, this means yesterday's +water. The leaf can then calculate today's carbon before the root calculates +today's water: ```julia ModelSpec( @@ -23,7 +23,15 @@ ModelSpec( ) ``` -The receiving object's initial `water` value is used until the first accepted -historical sample exists. If both values must converge within the same step, -do not add a lag: make a parent model own `calls` to the two trial models, -iterate with `publish=false`, and publish each accepted state once. +At the start, the leaf's initial `water` value is used until a result from a +previous step is available. + +If the two values must instead be solved together in the current step, use +a controller model that calls both models repeatedly. Try values with +`publish=false` and record each accepted result once with `publish=true`. +The controller must decide when the result is close enough and what to do if +the calculation does not converge. See +[Control Advanced Execution](../journeys/users/advanced_execution.md). + +You may also need to rewrite the equations. Simply changing the order of +the `ModelSpec` declarations does not resolve a cycle. diff --git a/docs/src/troubleshooting/runtime_contracts.md b/docs/src/troubleshooting/runtime_contracts.md index d08f5727e..59b0c41c2 100644 --- a/docs/src/troubleshooting/runtime_contracts.md +++ b/docs/src/troubleshooting/runtime_contracts.md @@ -1,9 +1,9 @@ # Inspect A Simulation When a result is unexpected, first check what ran, where it ran, and which -values it read. The public `Diagnostics` functions return structured rows that -can be displayed as a table. They explain the configured computation; they -do not establish that the scientific equations or parameter values are valid. +values it read. The `Diagnostics` functions return information you can display +as a table. Use them to check how the simulation is configured. Check the +scientific equations and parameter values separately. | Question | Diagnostic | |:--|:--| @@ -14,7 +14,7 @@ do not establish that the scientific equations or parameter values are valid. | When does each application run? | `Diagnostics.explain_schedule(model)` | | Which weather or spatial source is sampled? | `Diagnostics.explain_environment_bindings(model)` | | Which models are called by a controller? | `Diagnostics.explain_calls(model)` | -| Why was an output stream kept? | `Diagnostics.explain_output_retention(simulation)` | +| Why were earlier values of an output kept? | `Diagnostics.explain_output_retention(simulation)` | ## Inspect a small working example @@ -45,13 +45,16 @@ never constructed. ## After growth or movement -Structural changes refresh application targets and their connections **after -the application that changed the structure**. New objects may run applications -still remaining in that time step. They do not retroactively run earlier ones. -Changes made between simulation steps are processed before the next step. - -Movement or a geometry update invalidates the affected spatial environment -bindings. Use the public lifecycle functions so the runtime knows that a -refresh is needed. Removing an organ stops its future execution but preserves -its already retained output history. See [Modify plant structure](../journeys/users/structure_changes.md) -for an example that checks both target changes and conservation. +After adding or removing organs, PlantSimEngine updates which objects each +model runs on and where its inputs come from. This happens **after the +application that changed the structure finishes**. New organs may run models +scheduled later in the same time step. Models that already ran are not +repeated. If you change the structure between steps, these connections are +updated before the next step. + +If you move an organ or change its geometry, PlantSimEngine needs to find its +new location in the spatial environment. Use functions such as `move_object!` +and `update_geometry!` so it knows to update this connection. Removing an +organ stops its future calculations and keeps results that were already +saved. See [Modify plant structure](../journeys/users/structure_changes.md) +for an example that checks the affected objects and their carbon balance. diff --git a/docs/src/tutorials/growing_plant/part1_growth.md b/docs/src/tutorials/growing_plant/part1_growth.md index 00168ce41..b9ed2d74b 100644 --- a/docs/src/tutorials/growing_plant/part1_growth.md +++ b/docs/src/tutorials/growing_plant/part1_growth.md @@ -1,48 +1,51 @@ # Growing A Plant CompositeModel -Start with [Modify Plant Structure](@ref) for a runnable example of adding, -reparenting, and removing leaves. This page explains the extra decisions needed -when a biological growth model owns those operations. +Start with [Modify Plant Structure](@ref) for a runnable example of adding and +removing leaves, or changing which object they belong to. This page explains +how to connect those operations to a growth model and a carbon budget. ## Connect organ creation to a resource budget -Keep the plant carbon stock on the plant object and leaf production on the -leaves. The plant balance gathers only descendant production with -`Many(scale=:Leaf, within=Subtree())`. State whether production is a rate, -an interval amount, or a cumulative quantity before connecting it to a stock. -An accumulated source must be differenced or accounted for once; adding its -whole value repeatedly creates carbon. - -A growth kernel then follows this sequence: - -1. Read the plant's available carbon and the developmental condition. -2. If growth is permitted, construct the new organ's status with explicit - initial area, mass, and other required values. -3. Charge the construction cost once and register the organ with its stable - identity and correct parent. -4. Check that remaining reserves plus constructed material reproduce the - pre-growth budget, including any explicitly modelled respiration cost. - -Use `register_object!` when the caller has constructed a fully initialized -`Object`. For an MTG-backed model, use `add_organ!` so the MTG node, status, -and runtime object are created together. Neither operation chooses an -organogenesis hypothesis, construction cost, or carbon-to-dry-matter conversion -for you. +Store the plant's carbon reserves on the plant object and each leaf's carbon +production on that leaf. `Many(scale=:Leaf, within=Subtree())` lets the plant +collect production from its own leaves. Before adding production to reserves, +check what it represents: a rate, an amount produced during one interval, or +a total accumulated since the start of the simulation. For an accumulated +total, add only the increase since the last update. Adding the full total at +every step would count the same carbon repeatedly. + +The growth model's `run!` function then follows this sequence: + +1. Read the available carbon and check whether the plant is ready to grow. +2. If so, create the new organ's status with initial area, mass, and any + other values its models need. +3. Subtract the construction cost once. Add the organ to the simulation with + a unique identity and the correct parent object. +4. Check the carbon balance: the remaining reserves, the carbon in the new + organ, and any carbon spent on respiration must add up to the amount + available before growth. + +Use `register_object!` if your model has already created an `Object` with all +its initial values. If you represent the plant with a MultiScaleTreeGraph +(MTG), use `add_organ!` to create its MTG node, status, and simulation object +together. Your growth model must still decide when an organ appears, what it +costs, and how carbon is converted to dry matter. ## Know when a newborn can run -When a kernel changes topology, PlantSimEngine refreshes targets and bindings -**after that application**. A new leaf can run applications that remain later -in the same timestep. An application that already completed is not rerun. -If a newborn needs that application's initial calculation immediately, its -creator must declare an `Initializer` and call `run_initializer!` explicitly. +When a model adds, removes, or reparents an organ, PlantSimEngine updates +which objects each model runs on and where their inputs come from +**after that application finishes**. A new leaf can run models scheduled +later in the same timestep. Models that already ran are not repeated. +If the new leaf needs one of those earlier calculations immediately, the +model creating it must declare an `Initializer` and call `run_initializer!`. -When the caller changes topology between `step!` calls, the refresh happens -before the next step. These are two different entry points to the same -lifecycle mechanism; neither implies a rollback of biological state. +If you change the plant structure between `step!` calls, these connections +are updated before the next step. Neither case automatically undoes changes +to the plant's values if something goes wrong. -Check the organ's parent, required initial values, first retained sample, and -plant carbon budget after creation. Use the working example in -[Modify Plant Structure](@ref) to inspect the registry, then continue with +After creating an organ, check its parent, initial values, first saved result, +and the plant's carbon budget. Use the working example in +[Modify Plant Structure](@ref) to inspect the objects, then continue with [Adding Roots And Water](@ref) for a small resource-accounting example and [Debugging Growth And Resource Ordering](@ref) when execution order is unclear. diff --git a/docs/src/tutorials/growing_plant/part2_roots_water.md b/docs/src/tutorials/growing_plant/part2_roots_water.md index 51a957b3e..3669d1f81 100644 --- a/docs/src/tutorials/growing_plant/part2_roots_water.md +++ b/docs/src/tutorials/growing_plant/part2_roots_water.md @@ -1,10 +1,11 @@ # Adding Roots And Water -This example gathers two roots' **already accepted** uptake rates into one -plant water stock. It teaches the time and ownership boundary; the constant -rates are illustrative, not a root-uptake equation. It assumes an external -water supply has granted those rates and does not simulate soil competition, -transpiration, or a complete plant water balance. +This example adds the water taken up by two roots to one plant water stock. +It shows how to count each amount once when roots update hourly and the plant +updates daily. The uptake rates are constant teaching values, not predictions +from a root-uptake equation. We assume that the water supply can provide +these **already accepted** rates. The example does not calculate soil +competition, transpiration, or a complete plant water balance. ## Integrate each accepted interval once @@ -62,25 +63,27 @@ water_history = [ water_history ``` -The first daily call has only one hourly sample available: its partial window -adds `1.08` g. Each following daily call adds 24 new hourly intervals, or +The first daily calculation has only one hourly value available, so it adds +`1.08` g. Each following daily calculation adds 24 new hourly amounts, or `25.92` g. The stock is therefore `1.08`, `27.0`, and `52.92` g at base steps -1, 25, and 49. All 49 supplied hourly amounts are counted once. A daily cadence -does not by itself suppress this partial startup window. +1, 25, and 49. All 49 supplied hourly amounts are counted once. A model set +to run daily still runs at the start, before a full day of values is available. These totals describe uptake added to storage, not tissue hydration or growth. -## Extend the boundary to a shared soil +## Share a limited soil water supply -When several plants share finite soil water, give the soil stock one owner. -A collective soil/root controller must gather all demands, limit their sum to -the available water, subtract the accepted withdrawals once, and return the -accepted rates or amounts to each plant. Several roots independently reading -one soil stock do not provide that arbitration. `Updates` can order writers, -but it does not implement a resource-allocation rule. +When several plants share a limited soil water supply, use one model to +manage that stock. This model must collect all root demands, decide how much +water each receives, subtract the total withdrawal once, and return the +accepted rates or amounts to the plants. Simply letting each root read the +same soil stock does not prevent them from taking too much water together. +`Updates` can specify the order in which models change a value, but your +equations must decide how to share the water. -Keep rainfall as environmental forcing and state its units before converting -it to a soil-water amount. Add losses and exchanges explicitly when extending -the plant balance. When growth adds a root, initialize its state and register -it through the lifecycle API; the plant-local `Many` binding then refreshes -after the creating application. See [Growing A Plant CompositeModel](@ref). +Supply rainfall through the environment and state its units before converting +it to an amount of soil water. Add each loss and exchange when extending +the plant balance. When growth adds a root, provide its initial values and +add it with `register_object!` or, for an MTG, `add_organ!`. The plant's `Many` +selection then includes that root after the creating application finishes. +See [Growing A Plant CompositeModel](@ref). diff --git a/docs/src/tutorials/growing_plant/part3_debugging.md b/docs/src/tutorials/growing_plant/part3_debugging.md index c6aff4d74..8c9b488e9 100644 --- a/docs/src/tutorials/growing_plant/part3_debugging.md +++ b/docs/src/tutorials/growing_plant/part3_debugging.md @@ -6,45 +6,53 @@ twice, or appears in outputs at an unexpected time. Begin with the runnable ## Follow one timestep -First establish whether the organ was created inside a kernel or between -`step!` calls. Inside a kernel, targets and bindings refresh after the creating -application. The newborn can run applications that remain later in that step; -applications that already completed do not run again. Between steps, the -refresh happens before the next step. +First check whether a model created the organ during `run!`, or whether you +added it between `step!` calls. During a step, PlantSimEngine updates the list +of objects and their input connections after the creating application +finishes. The new organ can then run models scheduled later in that step; +models that already ran are not repeated. If you add the organ between steps, +these connections are updated before the next step. -An `Initializer` allows a creator to calculate one newborn's initial state -explicitly. It does not create an extra retained publication during the step. -Registration changes the live registry immediately; the refresh barrier is -not a general transaction or an automatic rollback of model state. +An `Initializer` lets a model calculate a new organ's initial values without +adding an extra saved result during the step. Adding an organ changes the +object registry immediately. The later update to input connections does not +automatically undo those changes, or changes to other model values, if a +calculation fails. -## Inspect the boundary that failed +## Find the cause of the unexpected result | Symptom | First check | What to verify | |---|---|---| -| Missing or invalid newborn state | `Diagnostics.explain_initialization(model)` | Every required input has a meaningful initial value. | -| One plant consumes another plant's leaves | `Diagnostics.explain_bindings(model)` | The source uses the intended `Subtree()` or other explicit scope. | -| A stock has two producers | `Diagnostics.explain_writers(model)` | One owner updates it, or intentional writers declare `Updates(:stock; after=:producer)`. | -| Child models run unexpectedly | `Diagnostics.explain_calls(model)` | The correct applications are call targets and the parent invokes them once for each intended trial or acceptance. | -| Daily uptake is added hourly | `Diagnostics.explain_schedule(model)` | Stock-update cadence matches the non-overlapping integration intervals. | -| Newborn output begins too early or too late | `Diagnostics.explain_outputs(simulation)` and `collect_outputs(simulation)` | First publication agrees with the creation point and remaining schedule. | - -For carbon or water, also write a balance independent of the execution graph: -initial stock plus accepted inputs equals final stock plus explicit losses -and transfers. Passing writer checks cannot establish this conservation law. +| A new organ has missing or invalid values | `Diagnostics.explain_initialization(model)` | Every required input has a meaningful initial value. | +| One plant reads values from another plant's leaves | `Diagnostics.explain_bindings(model)` | `Subtree()` or another selector limits the search to the intended plant. | +| Two models change the same stock | `Diagnostics.explain_writers(model)` | Only one model sets the value, or `Updates(:stock; after=:producer)` specifies the intended order. | +| A called model runs unexpectedly | `Diagnostics.explain_calls(model)` | The controller selects the intended models and calls them only for the intended trials and accepted calculations. | +| A day's uptake is added every hour | `Diagnostics.explain_schedule(model)` | The stock is updated only once for each amount of water taken up. | +| A new organ's results start too early or too late | `Diagnostics.explain_outputs(simulation)` and `collect_outputs(simulation)` | The first result matches when the organ was created and which models could still run in that step. | + +For carbon or water, also check the balance directly: initial stock plus +accepted inputs equals final stock plus losses and transfers. PlantSimEngine +can check which models change a stock, but that check does not prove that your +equations conserve the resource. [Adding Roots And Water](@ref) shows a two-day accounting check. ## Keep trial and accepted state distinct -For a numerical cycle, choose the scientific meaning: lag one input with -`PreviousTimeStep`, put convergence under a parent-owned hard call, or -reformulate the equations. Incidental application order is not a solver. - -Use `run_call!(context, name; publish=false)` for trial evaluations and publish -only the accepted result. This suppresses trial output publication; it does -not undo assignments to live status. The parent must prepare each trial's -state and keep irreversible stock updates out of rejected iterations. Commit -mutable environment changes only after accepting a solution. - -The parent owns the iteration limit, tolerance, and failure policy. The -[MAESPA-Style Synthesis](@ref) demonstrates accepted canopy-air commits and -leaf calls; its carbon accumulation happens after the solver accepts a state. +If two models each need the other's result, decide what the equations require. +Use `PreviousTimeStep` if one input should come from the preceding step. If +both must be solved together in the current step, use a controller to repeat +the calculations until the solution is acceptable. You may also need to +rewrite the equations. Changing the order of the `ModelSpec` declarations +does not solve this problem. + +Use `run_call!(context, name; publish=false)` for trials and `publish=true` +only for the accepted result. `publish=false` keeps the trial out of the +output history, but it does not undo changes to status values. The controller +must prepare the values for each new trial and avoid permanently spending +carbon or water on rejected trials. Save environment changes with +`commit_environment!` only after accepting a solution. + +The controller must set the maximum number of trials, how close the solution +must be, and what to do if no acceptable solution is found. The +[MAESPA-Style Synthesis](@ref) shows repeated leaf calculations followed by +an accepted canopy air update. It adds carbon only after accepting a solution. diff --git a/docs/src/working_with_data/fitting.md b/docs/src/working_with_data/fitting.md index 65f058af8..78c2b5417 100644 --- a/docs/src/working_with_data/fitting.md +++ b/docs/src/working_with_data/fitting.md @@ -1,11 +1,13 @@ # Parameter Fitting -`PlantSimEngine.Evaluation.fit` is the shared interface for model-specific -calibration. -Model packages implement a method whose first argument is the model type and -whose second argument is Tables.jl-compatible observations. +Parameter fitting means finding parameter values that make a model agree +with observations. PlantSimEngine provides `Evaluation.fit` as a common +function name; each model package supplies the fitting method for its own +models. Pass the model type first and a table of observations second. +The table can be a DataFrame or another format supported by Tables.jl. -The mathematical core of the Beer fit is: +For the `Beer` model, we can calculate the light-extinction coefficient `k` +from LAI, incoming light, and absorbed light: ```julia J_to_umol = PlantMeteo.Constants().J_to_umol @@ -14,16 +16,16 @@ f_abs = data.aPPFD ./ incident_ppfd k = Statistics.mean(-log1p.(-f_abs) ./ data.LAI) ``` -This snippet shows the inversion only. The implementation in +This snippet only shows how to rearrange Beer's equation to find `k`. The implementation in `examples/Beer.jl` validates every observation and returns `(k=k,)`; do not use the snippet alone as an unchecked fitting method. -The result should be a `NamedTuple` of fitted parameters. +A fitting method returns named parameter values, for example `(k=0.6,)`. -In this Beer example, `Ri_PAR_f` is an incident flux per unit ground area and -`aPPFD` is the flux absorbed by the whole canopy, also per unit ground area. +In this Beer example, `Ri_PAR_f` measures incoming light per unit ground area +and `aPPFD` measures light absorbed by the whole canopy per unit ground area. `LAI` is leaf area per unit ground area. Do not use a PPFD expressed per unit -leaf area in this inversion. The fit rejects empty data, non-finite or +leaf area in this calculation. The fit rejects empty data, non-finite or non-positive `LAI` and incident PAR, and absorbed fractions outside `[0, 1)`. ```@example fitting diff --git a/src/PlantSimEngine.jl b/src/PlantSimEngine.jl index 5888db4b0..a5112127f 100644 --- a/src/PlantSimEngine.jl +++ b/src/PlantSimEngine.jl @@ -49,6 +49,7 @@ include("component_models/TimeStepTable.jl") # Model application configuration: include("ModelSpec.jl") +include("composite_model/display.jl") # Model evaluation (statistics): include("evaluation/statistics.jl") diff --git a/src/composite_model/display.jl b/src/composite_model/display.jl new file mode 100644 index 000000000..03102e1de --- /dev/null +++ b/src/composite_model/display.jl @@ -0,0 +1,103 @@ +# Display authored configuration only. Inspecting a model must not compile it, +# initialize its status, sample the environment, or refresh lifecycle caches. +function Base.show(io::IO, model::CompositeModel) + print(io, "CompositeModel(objects=", length(model.registry.objects), + ", applications=", length(model.applications)) + isempty(model.instances) || print(io, ", instances=", length(model.instances)) + print(io, ")") +end + +function _model_display_line(io::IO, text::AbstractString) + width = max(1, displaysize(io)[2]) + if textwidth(text) <= width + print(io, text) + return + end + used = 0 + for character in text + used + textwidth(character) > width - 1 && break + print(io, character) + used += textwidth(character) + end + print(io, '…') +end + +_model_display_label(value) = escape_string(string(value)) + +function _model_display_scales(model::CompositeModel) + by_scale = model.registry.by_scale + scales = sort!(collect(keys(by_scale)); by=string) + labels = [ + string(_model_display_label(scale), ": ", length(by_scale[scale])) + for scale in Iterators.take(scales, 6) + ] + length(scales) > 6 && push!(labels, "…") + unlabelled = length(model.registry.objects) - sum(length, values(by_scale); init=0) + unlabelled > 0 && push!(labels, string("no scale label: ", unlabelled)) + return join(labels, ", ") +end + +function _model_display_environment(environment) + isnothing(environment) && return "none" + name = string(nameof(typeof(environment))) + if environment isa TimeStepTable + return string(name, " (", length(environment), " rows)") + end + return name +end + +function _model_display_application(application) + spec = application isa ModelSpec ? application : nothing + model = isnothing(spec) ? application : spec.model + base_model = model isa ObjectModelOverrides ? model.base : model + label = string(nameof(typeof(base_model))) + if !isnothing(spec) && !isnothing(spec.name) + label = string(_model_display_label(spec.name), ": ", label) + end + if model isa ObjectModelOverrides + count = length(model.overrides) + label *= string(" (", count, " object override", count == 1 ? "" : "s", ")") + end + if !isnothing(spec) && !isnothing(spec.timestep) + cadence = spec.timestep isa Dates.Period ? string(spec.timestep) : + string(nameof(typeof(spec.timestep))) + label *= string("; every ", cadence) + end + return label +end + +function Base.show(io::IO, ::MIME"text/plain", model::CompositeModel) + if get(io, :compact, false) + show(io, model) + return + end + _model_display_line(io, "CompositeModel") + scales = _model_display_scales(model) + objects = string(" Objects: ", length(model.registry.objects)) + isempty(scales) || (objects *= string(" (", scales, ")")) + print(io, '\n') + _model_display_line(io, objects) + if !isempty(model.instances) + print(io, '\n') + _model_display_line(io, string(" Template instances: ", length(model.instances))) + end + print(io, '\n') + _model_display_line(io, string(" Shared environment: ", _model_display_environment(model.environment))) + print(io, '\n') + _model_display_line(io, string(" Model applications: ", length(model.applications))) + + # This is a summary even when :limit is false. In a small terminal, leave + # room for the header and the number of omitted applications. + header_lines = isempty(model.instances) ? 4 : 5 + available = get(io, :limit, false) ? max(0, displaysize(io)[1] - header_lines - 1) : 8 + shown = min(length(model.applications), 8, available) + for application in Iterators.take(model.applications, shown) + print(io, '\n') + _model_display_line(io, string(" ", _model_display_application(application))) + end + remaining = length(model.applications) - shown + if remaining > 0 + print(io, '\n') + _model_display_line(io, string(" … ", remaining, " more")) + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 5a83d8f3d..6d52eabcd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -38,6 +38,10 @@ else include("test-model-object-id.jl") end + @testset "CompositeModel display" begin + include("test-model-display.jl") + end + @testset "Composite Model/Object API stabilization" begin include("test-model-api-stabilization.jl") end diff --git a/test/test-model-display.jl b/test/test-model-display.jl new file mode 100644 index 000000000..83265820b --- /dev/null +++ b/test/test-model-display.jl @@ -0,0 +1,109 @@ +using Dates + +PlantSimEngine.@process "display_probe" verbose = false + +struct DisplayProbeModel{T} <: AbstractDisplay_ProbeModel + payload::T +end + +PlantSimEngine.inputs_(::DisplayProbeModel) = NamedTuple() +PlantSimEngine.outputs_(::DisplayProbeModel) = (count=0,) +function PlantSimEngine.run!(::DisplayProbeModel, status, environment, constants, context) + status.count += 1 + return nothing +end + +# Neither model parameters nor user-defined environment displays belong in the +# summary. A raw model must not be normalized through ModelSpec just to show it. +struct DisplayUnreadable end +Base.show(::IO, ::DisplayUnreadable) = error("Display must not read this value") + +@testset "CompositeModel display does not prepare or run the model" begin + model = CompositeModel(DisplayProbeModel(DisplayUnreadable())) + status = model_status(model, :scene) + revision = model.revision + @test isnothing(model.binding_cache) + @test repr(model) == "CompositeModel(objects=1, applications=1)" + display = repr(MIME"text/plain"(), model) + @test occursin("Objects: 1 (Scene: 1)", display) + @test occursin("DisplayProbeModel", display) + @test occursin("Shared environment: none", display) + @test !occursin("DisplayUnreadable", display) + @test isnothing(model.binding_cache) + @test isnothing(model.environment_binding_cache) + @test model.bindings_dirty + @test model.revision == revision + @test model_status(model, :scene) === status + + simulation = run!(model; steps=1) + cache = model.binding_cache + environment_cache = model.environment_binding_cache + @test final_state(simulation).count == 1 + @test repr(MIME"text/plain"(), model) == display + @test final_state(simulation).count == 1 + @test model.binding_cache === cache + @test model.environment_binding_cache === environment_cache + @test !model.bindings_dirty + + register_object!(model, Object(:leaf; scale=:Leaf)) + dirty_cache = model.binding_cache + @test occursin("Objects: 2 (Leaf: 1, Scene: 1)", repr(MIME"text/plain"(), model)) + @test model.bindings_dirty + @test model.binding_cache === dirty_cache + + # This setup cannot compile (no selector), but should still be inspectable. + incomplete = CompositeModel(Object(:scene); + applications=(DisplayProbeModel(DisplayUnreadable()),), + environment=DisplayUnreadable()) + @test occursin("Shared environment: DisplayUnreadable", repr(MIME"text/plain"(), incomplete)) + @test isnothing(incomplete.binding_cache) + @test occursin("Objects: 0", repr(MIME"text/plain"(), CompositeModel())) +end + +@testset "CompositeModel display summarizes configuration" begin + weather = read_weather(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv")) + template = CompositeModelTemplate(( + ModelSpec(DisplayProbeModel(zeros(10_000)); name=:growth, + on=Many(scale=:Leaf), every=Hour(1)), + )) + plant = Object(:plant; scale=:Plant) + leaf = Object(:leaf; scale=:Leaf, parent=:plant) + model = CompositeModel( + ObjectInstance(:plant, template; root=plant, objects=(leaf,), + object_overrides=(Override(object=:leaf, application=:growth, + model=DisplayProbeModel(ones(10_000))),)); + environment=weather, + ) + display = repr(MIME"text/plain"(), model) + @test repr(model) == "CompositeModel(objects=2, applications=1, instances=1)" + @test occursin("Objects: 2 (Leaf: 1, Plant: 1)", display) + @test occursin("Template instances: 1", display) + @test occursin("TimeStepTable (365 rows)", display) + @test occursin("plant__growth: DisplayProbeModel (1 object override); every 1 hour", display) + @test !occursin("ObjectModelOverrides", display) + @test length(display) < 500 + @test repr(MIME"text/plain"(), model; context=:compact => true) == repr(model) + @test count(repr(model), repr([model, model])) == 2 +end + +@testset "CompositeModel display stays small" begin + applications = [ModelSpec(DisplayProbeModel(zeros(100)); + name=Symbol("growth_", index), on=Many(scale=:Leaf)) for index in 1:100] + model = CompositeModel((Object(index; scale=:Leaf) for index in 1:100)...; + applications=applications) + display = repr(MIME"text/plain"(), model) + @test occursin("Objects: 100 (Leaf: 100)", display) + @test occursin("Model applications: 100", display) + @test occursin("92 more", display) + @test length(split(display, '\n')) <= 13 + @test length(display) < 1000 + + pushfirst!(model.applications, ModelSpec(DisplayProbeModel(0); + name=Symbol(repeat("叶", 50), "\nnext line"), on=Many(scale=:Leaf))) + small = sprint(show, MIME"text/plain"(), model; + context=(:limit => true, :displaysize => (10, 32))) + @test length(split(small, '\n')) <= 10 + @test all(line -> textwidth(line) <= 32, split(small, '\n')) + @test occursin("…", small) + @test occursin("96 more", small) +end From 9e0c2c19cf9e6746f6a47e8f894f5c3a06b3ab02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Vezy?= Date: Sun, 6 Sep 2026 20:43:26 +0200 Subject: [PATCH 6/6] Add two-plant comparison with distinct carbon allocation models --- docs/src/journeys/users/maespa_synthesis.md | 7 +- docs/src/journeys/users/one_plant.md | 2 +- docs/src/journeys/users/several_plants.md | 269 +++++++---------- examples/two_plant_allocation.jl | 224 ++++++++++++++ test/runtests.jl | 4 + test/test-two-plant-allocation.jl | 306 ++++++++++++++++++++ 6 files changed, 652 insertions(+), 160 deletions(-) create mode 100644 examples/two_plant_allocation.jl create mode 100644 test/test-two-plant-allocation.jl diff --git a/docs/src/journeys/users/maespa_synthesis.md b/docs/src/journeys/users/maespa_synthesis.md index 029df0919..ec64280b6 100644 --- a/docs/src/journeys/users/maespa_synthesis.md +++ b/docs/src/journeys/users/maespa_synthesis.md @@ -39,8 +39,8 @@ nothing ``` The model contains one object for the whole scene, one soil object, and two -plants built from the same template. The plants have different species -parameters and numbers of leaves: +plants with the same model connections. Each plant has its own template, +species parameters, and number of leaves: ```@example journey_maespa_synthesis ( @@ -99,6 +99,9 @@ select( Each plant's allocation model reads carbon values from its own leaves. The scene model reads areas from all leaves and water potential from the soil. +Here `AllocA` and `AllocB` use the same fixed-fraction allocation equation +with different parameters. For two different allocation rules running on +two plants together, see [Instantiate Several Plants](several_plants.md). Inspect `source_ids` below to check where each input comes from. The values are shared by reference, meaning the reader sees the current source values without copying them: diff --git a/docs/src/journeys/users/one_plant.md b/docs/src/journeys/users/one_plant.md index a612fb49b..3bf3c8c11 100644 --- a/docs/src/journeys/users/one_plant.md +++ b/docs/src/journeys/users/one_plant.md @@ -201,7 +201,7 @@ leaf_states = final_state(computed_simulation, Many(scale=:Leaf)) The resulting plant surface should be 3 m² and the light contributions should still be 40 and 80 μmol m⁻² of reference ground s⁻¹. You can now -[reuse this configuration on several plants](several_plants.md). +[use different models on two plants in the same simulation](several_plants.md). The optional table below lets you check the connections. For `:leaf_surfaces`, the plant should read from both leaves. Its `RefVector` diff --git a/docs/src/journeys/users/several_plants.md b/docs/src/journeys/users/several_plants.md index c9bc26cef..6b1e5d0e8 100644 --- a/docs/src/journeys/users/several_plants.md +++ b/docs/src/journeys/users/several_plants.md @@ -1,90 +1,89 @@ # Instantiate Several Plants -Apply the configuration from [one multiscale plant](one_plant.md) to two plants, -then change the specific leaf area of a third. A `CompositeModelTemplate` -stores the models and their connections so you can reuse them. Each -`ObjectInstance` supplies a plant, its leaves, and their initial values. +Two plants can use different equations for the same process in one +simulation. Here both plants receive the same amount of carbon, but use +different rules to divide it between leaves, wood, and reserves. This lets +you compare the rules while keeping the inputs the same. -As in that teaching example, light values are contributions per m² of a -plant's reference ground area per second, in μmol of absorbed PAR. A plant's -leaf contributions share that basis and can be added within the plant. -They are not fluxes per unit leaf area. Combining plants with different -reference areas would require an explicit area conversion first. +We represent each whole plant as one object. For an example with individual +leaves, see [one multiscale plant](one_plant.md). + +## Choose two allocation rules + +Both models describe **carbon allocation**: deciding where available carbon +goes. They read the same inputs and produce the same outputs, so either can +fill the `:allocation` application in our plant template. Their equations +differ: + +- `FixedFractionAllocation(0.5, 0.3)` assigns up to 50% of the available + carbon to leaves and up to 30% to wood. Neither receives more than its + demand. Carbon left over goes into reserves. +- `DemandAllocation()` divides carbon in proportion to leaf and wood demand. + When there is enough carbon, both demands are met. Any excess goes into + reserves. + +You can see the difference in the equations. Let `C` be available carbon, +`L` leaf demand, and `W` wood demand. `min(a, b)` means the smaller of the +two amounts: + +| Carbon sent to | Fixed fractions | Proportional to demand | +|---|---|---| +| Leaves | `min(0.5 * C, L)` | `min(C, L + W) * L / (L + W)` | +| Wood | `min(0.3 * C, W)` | `min(C, L + W) * W / (L + W)` | +| Reserves | What remains after leaf and wood allocation | What remains after leaf and wood allocation | + +If both demands are zero, both models put all available carbon into reserves. + +These are teaching rules, not calibrated models of particular species. +All amounts are grams of elemental carbon (g C) per plant. The example does +not convert carbon into dry biomass or subtract respiration losses, and +neither rule withdraws carbon from reserves. +The carbon offer is the amount available after any respiration costs. Offers +and demands must be finite and nonnegative. + +Load the models from the example source included with PlantSimEngine: ```@example journey_several_plants -using PlantSimEngine, DataFrames -using PlantSimEngine.Examples +using PlantSimEngine, Dates, DataFrames + +include(joinpath(pkgdir(PlantSimEngine), "examples", "two_plant_allocation.jl")) +using .TwoPlantAllocationExample +``` +## Make a template and two plants + +A `CompositeModelTemplate` stores models and their configuration for reuse. +Our template uses the fixed-fraction rule by default. The application name +`:allocation` identifies the calculation we will replace on the second plant. + +```@example journey_several_plants plant_template = CompositeModelTemplate(( ModelSpec( - ToyLeafSurfaceModel(0.02); - name=:leaf_surface, - on=Many(scale=:Leaf), - ), - ModelSpec( - ToyPlantLeafSurfaceModel(); - name=:plant_surface, + FixedFractionAllocation(0.5, 0.3); + name=:allocation, on=One(scale=:Plant), - inputs=( - :leaf_surfaces => Many( - scale=:Leaf, - within=Subtree(), - application=:leaf_surface, - var=:surface, - ), - ), - ), - ModelSpec( - ToyLightPartitioningModel(); - name=:leaf_light, - on=Many(scale=:Leaf), - inputs=( - :aPPFD_larger_scale => One( - scale=:Plant, - within=SelfPlant(), - var=:aPPFD, - ), - :total_surface => One( - scale=:Plant, - within=SelfPlant(), - application=:plant_surface, - var=:surface, - ), - ), ), )) nothing # hide ``` -Create two plants from the template. Here `root` is the object representing -the whole plant, at the top of its structure; it does not mean a botanical -root. The plants have different absorbed light and initial leaf biomasses: +Each `ObjectInstance` supplies a plant and its initial values. Here `root` +means the object at the top of the plant's structure, not a botanical root. +Both plants have 10 g C available today, a leaf demand of 8 g C, and a wood +demand of 2 g C. + +Plant A keeps the template's model. Plant B uses `overrides` to replace that +model with `DemandAllocation()`: ```@example journey_several_plants plant_a = ObjectInstance( :plant_a, plant_template; root=Object( - :plant_a_root; + :plant_a; scale=:Plant, kind=:plant, - status=Status(aPPFD=120.0), - ), - objects=( - Object( - :plant_a_leaf_1; - scale=:Leaf, - kind=:leaf, - parent=:plant_a_root, - status=Status(carbon_biomass=50.0), - ), - Object( - :plant_a_leaf_2; - scale=:Leaf, - kind=:leaf, - parent=:plant_a_root, - status=Status(carbon_biomass=100.0), - ), + status=Status(carbon_offer=10.0, leaf_demand=8.0, wood_demand=2.0), ), ) @@ -92,113 +91,69 @@ plant_b = ObjectInstance( :plant_b, plant_template; root=Object( - :plant_b_root; + :plant_b; scale=:Plant, kind=:plant, - status=Status(aPPFD=200.0), - ), - objects=( - Object( - :plant_b_leaf_1; - scale=:Leaf, - kind=:leaf, - parent=:plant_b_root, - status=Status(carbon_biomass=50.0), - ), - Object( - :plant_b_leaf_2; - scale=:Leaf, - kind=:leaf, - parent=:plant_b_root, - status=Status(carbon_biomass=50.0), - ), + status=Status(carbon_offer=10.0, leaf_demand=8.0, wood_demand=2.0), ), + overrides=(allocation=DemandAllocation(),), ) - -model = CompositeModel(plant_a, plant_b) -simulation = run!(model; outputs=:all) -plant_states = final_state(simulation, Many(scale=:Plant)) -Dict(id => (surface=state.surface, aPPFD=state.aPPFD) for (id, state) in plant_states) +nothing # hide ``` -Plant A has `1 + 2 = 3 m²` of leaves; plant B has `1 + 1 = 2 m²`. -Each plant uses only its own leaves when calculating the total, because the -selector uses `Subtree()`. Likewise, each pair of leaf light contributions -adds up to the light supplied to its own plant, expressed per m² of that -plant's reference ground area: +The process is the same on both plants: `:carbon_allocation`. The model used +for that process is different. Each plant keeps its own inputs and carbon +pools; replacing Plant B's model does not change Plant A's model. -```@example journey_several_plants -leaf_states = final_state(simulation, Many(scale=:Leaf)) -( - plant_a_light=sum( - leaf_states[id].aPPFD - for id in (:plant_a_leaf_1, :plant_a_leaf_2) - ), - plant_b_light=sum( - leaf_states[id].aPPFD - for id in (:plant_b_leaf_1, :plant_b_leaf_2) - ), -) -``` +## Run both plants together -The table below lists the objects and model applications for each plant. -PlantSimEngine adds the plant instance name to each application name, which -lets you distinguish the two plants' calculations: +Put both plants in one `CompositeModel`. One step lasts one day here: ```@example journey_several_plants -select( - DataFrame(Diagnostics.explain_instances(model)), - :name, - :root_id, - :object_ids, - :application_ids, +model = CompositeModel(plant_a, plant_b; environment=(duration=Day(1),)) +simulation = run!(model; steps=1, outputs=:all) +results = collect_outputs(simulation; sink=DataFrame) + +allocation_rows = filter( + :variable => v -> v in (:leaf_growth, :wood_growth, :reserve_change), + results, ) +comparison = unstack(allocation_rows, :object_id, :variable, :value) +select(comparison, :object_id, :leaf_growth, :wood_growth, :reserve_change) ``` -## Override one instance +`collect_outputs` returns a table with one row per variable. We select the +three daily allocations, then use `unstack` to give each variable its own +column. Each row of the displayed table now describes one plant. -Now create a third plant with a larger specific leaf area. Set `overrides` -to replace the model used for `:leaf_surface` on this plant. The other models -and their connections stay as defined in the template: +Plant A sends **5 g C to leaves, 2 to wood, and 3 to reserves**. Its wood +fraction would give 3 g C, but wood only demands 2. The unused carbon stays +in reserves rather than being reassigned to leaves. -```@example journey_several_plants -plant_c = ObjectInstance( - :plant_c, - plant_template; - root=Object( - :plant_c_root; - scale=:Plant, - kind=:plant, - status=Status(aPPFD=120.0), - ), - objects=( - Object( - :plant_c_leaf_1; - scale=:Leaf, - kind=:leaf, - parent=:plant_c_root, - status=Status(carbon_biomass=50.0), - ), - Object( - :plant_c_leaf_2; - scale=:Leaf, - kind=:leaf, - parent=:plant_c_root, - status=Status(carbon_biomass=100.0), - ), - ), - overrides=(leaf_surface=ToyLeafSurfaceModel(0.04),), -) +Plant B sends **8 g C to leaves, 2 to wood, and 0 to reserves**. The available +10 g C is enough to meet both demands. In both cases, the three amounts add +up to the 10 g C supplied. -override_simulation = run!(CompositeModel(plant_c)) -override_state = final_state(override_simulation, One(scale=:Plant)) -override_state.surface -``` +The models also keep cumulative `leaf_carbon`, `wood_carbon`, and +`reserve_carbon` values. These pools start at zero here, so after one day +they equal the amounts just allocated. They record allocated carbon, not +predicted organ biomass. -The third plant has 6 m² of leaves: twice the area at the original specific -leaf area, for the same supplied carbon biomass. This is a parameter comparison -within the teaching model, not a calibrated species comparison. +```@example journey_several_plants +fixed = final_state(simulation, :plant_a) # hide +demand = final_state(simulation, :plant_b) # hide +@assert (fixed.leaf_growth, fixed.wood_growth, fixed.reserve_change) == (5.0, 2.0, 3.0) # hide +@assert (demand.leaf_growth, demand.wood_growth, demand.reserve_change) == (8.0, 2.0, 0.0) # hide +@assert fixed.leaf_carbon + fixed.wood_carbon + fixed.reserve_carbon == 10.0 # hide +@assert demand.leaf_carbon + demand.wood_carbon + demand.reserve_carbon == 10.0 # hide +nothing # hide +``` -The plants do not share any inputs in this example. If they need to read a -shared soil object, for example, use `within=SceneScope()` in that input's -selector. This allows it to look beyond the current plant. +To compare other conditions, change `carbon_offer`, `leaf_demand`, or +`wood_demand` when constructing the plants. These supplied values stay fixed +in this example: each new day supplies another 10 g C to each plant and +renews the demands. The carbon pools accumulate over those days. A longer +simulation could obtain changing daily values from +photosynthesis and organ-demand models. See +[Collect and plot results](../../guides/data/outputs_plotting.md) for comparing +the resulting time series. diff --git a/examples/two_plant_allocation.jl b/examples/two_plant_allocation.jl new file mode 100644 index 000000000..3eee398db --- /dev/null +++ b/examples/two_plant_allocation.jl @@ -0,0 +1,224 @@ +module TwoPlantAllocationExample + +using Dates +using PlantSimEngine +using PlantSimEngine.Examples: AbstractCarbon_AllocationModel + +export FixedFractionAllocation, DemandAllocation +export allocation_template, allocation_plant, build_allocation_comparison + +""" + FixedFractionAllocation(leaf_fraction, wood_fraction) + +Allocate fixed fractions of each day's available carbon to leaves and wood, +up to their respective demands. Keep unused carbon in reserve, without +redistributing one organ's unused share to the other organ. + +This is an uncalibrated teaching hypothesis. All inputs are nonnegative +daily amounts in g elemental C per plant, after any respiration costs. +Fractions must be nonnegative and sum to at most one. The example does not +model respiration, turnover, or withdrawal from existing carbon pools. +""" +struct FixedFractionAllocation{T<:Real} <: AbstractCarbon_AllocationModel + leaf_fraction::T + wood_fraction::T + + function FixedFractionAllocation(leaf_fraction::Real, wood_fraction::Real) + leaf, wood = promote(float(leaf_fraction), float(wood_fraction)) + all(x -> isfinite(x) && x >= zero(x), (leaf, wood)) || + throw(ArgumentError("Allocation fractions must be finite and nonnegative.")) + leaf + wood <= one(leaf) || + throw(ArgumentError("Allocation fractions must sum to at most one.")) + return new{typeof(leaf)}(leaf, wood) + end +end + +""" + DemandAllocation() + DemandAllocation{Float32}() + +Allocate available carbon in proportion to leaf and wood demands, up to +their combined demand. Keep any surplus in reserve. With no demand, keep +all available carbon in reserve. + +This is a second uncalibrated hypothesis for the same `carbon_allocation` +process as `FixedFractionAllocation`, with the same inputs, outputs, and +units. The type parameter selects the initial output number type. +""" +struct DemandAllocation{T<:Real} <: AbstractCarbon_AllocationModel end +DemandAllocation() = DemandAllocation{Float64}() + +const AllocationModel = Union{FixedFractionAllocation,DemandAllocation} + +PlantSimEngine.inputs_(::AllocationModel) = ( + carbon_offer=Required(Real), + leaf_demand=Required(Real), + wood_demand=Required(Real), +) + +function PlantSimEngine.outputs_(::Union{FixedFractionAllocation{T},DemandAllocation{T}}) where {T} + return ( + leaf_growth=zero(T), + wood_growth=zero(T), + reserve_change=zero(T), + leaf_carbon=zero(T), + wood_carbon=zero(T), + reserve_carbon=zero(T), + ) +end + +PlantSimEngine.environment_inputs_(::AllocationModel) = NamedTuple() +PlantSimEngine.environment_outputs_(::AllocationModel) = NamedTuple() +PlantSimEngine.timestep_hint(::AllocationModel) = Day(1) + +const DAILY_CARBON = VariableContract( + unit=:g_carbon, basis=:plant, temporal=:day, + aggregation=:total, extent=:extensive, +) +const STORED_CARBON = VariableContract( + unit=:g_carbon, basis=:plant, temporal=:instantaneous, + aggregation=:state, extent=:extensive, +) + +PlantSimEngine.variable_contracts_(::AllocationModel) = ( + carbon_offer=DAILY_CARBON, + leaf_demand=DAILY_CARBON, + wood_demand=DAILY_CARBON, + leaf_growth=DAILY_CARBON, + wood_growth=DAILY_CARBON, + reserve_change=DAILY_CARBON, + leaf_carbon=STORED_CARBON, + wood_carbon=STORED_CARBON, + reserve_carbon=STORED_CARBON, +) + +PlantSimEngine.Authoring.model_metadata(::FixedFractionAllocation) = ( + hypothesis="Fixed fractions, capped by each organ's demand; unused shares go to reserve.", + reference=nothing, + maturity=:pedagogical_non_calibrated, + validation=:structural_tests_only, +) +PlantSimEngine.Authoring.model_metadata(::DemandAllocation) = ( + hypothesis="Available carbon is shared in proportion to organ demands; surplus goes to reserve.", + reference=nothing, + maturity=:pedagogical_non_calibrated, + validation=:structural_tests_only, +) +PlantSimEngine.Authoring.parameter_metadata(::FixedFractionAllocation) = ( + leaf_fraction=( + description="Fraction of available carbon offered to leaves before the demand cap.", + unit=:fraction, domain=(minimum=0, maximum=1), + ), + wood_fraction=( + description="Fraction of available carbon offered to wood before the demand cap.", + unit=:fraction, domain=(minimum=0, maximum=1), + ), +) + +function _check_allocation_inputs(status) + for variable in (:carbon_offer, :leaf_demand, :wood_demand) + value = getproperty(status, variable) + isfinite(value) && value >= zero(value) || throw(ArgumentError( + "`$(variable)` must be finite and nonnegative in this teaching example.", + )) + end + return nothing +end + +# Fixed-fraction allocation kernel +function PlantSimEngine.run!( + model::FixedFractionAllocation, + status, + environment, + constants, + context, +) + _check_allocation_inputs(status) + status.leaf_growth = min(model.leaf_fraction * status.carbon_offer, status.leaf_demand) + # The last bound prevents roundoff from creating a negative reserve. + status.wood_growth = min( + model.wood_fraction * status.carbon_offer, + status.wood_demand, + status.carbon_offer - status.leaf_growth, + ) + status.reserve_change = status.carbon_offer - status.leaf_growth - status.wood_growth + + status.leaf_carbon += status.leaf_growth + status.wood_carbon += status.wood_growth + status.reserve_carbon += status.reserve_change + return nothing +end + +# Demand-based allocation kernel +function PlantSimEngine.run!( + ::DemandAllocation, + status, + environment, + constants, + context, +) + _check_allocation_inputs(status) + total_demand = status.leaf_demand + status.wood_demand + isfinite(total_demand) || throw(ArgumentError("Combined organ demand must be finite.")) + growth = min(status.carbon_offer, total_demand) + status.leaf_growth = iszero(total_demand) ? zero(growth) : + min(status.leaf_demand, growth, + growth * (status.leaf_demand / total_demand)) + # Keep the demand caps and carbon balance even after floating-point rounding. + status.wood_growth = min(status.wood_demand, growth - status.leaf_growth) + status.reserve_change = status.carbon_offer - status.leaf_growth - status.wood_growth + + status.leaf_carbon += status.leaf_growth + status.wood_carbon += status.wood_growth + status.reserve_carbon += status.reserve_change + return nothing +end + +"""Create a reusable configuration for one plant's allocation model.""" +allocation_template(model::AllocationModel) = CompositeModelTemplate(( + ModelSpec(model; name=:allocation, on=One(scale=:Plant)), +)) + +"""Create one plant, optionally replacing the template's allocation model.""" +function allocation_plant( + name, + template; + carbon_offer=10.0, + leaf_demand=8.0, + wood_demand=2.0, + allocation=nothing, +) + initial_values = Status(; carbon_offer, leaf_demand, wood_demand) + _check_allocation_inputs(initial_values) + return ObjectInstance( + name, + template; + root=Object(name; scale=:Plant, kind=:plant, status=initial_values), + overrides=isnothing(allocation) ? NamedTuple() : (allocation=allocation,), + ) +end + +""" + build_allocation_comparison(; fixed_model, demand_model, plant_a, plant_b) + +Build two plants in one `CompositeModel`. Both receive 10 g C per day and +demands of 8 g C for leaves and 2 g C for wood by default. Override `plant_a` +or `plant_b` with a named tuple of these inputs for an independent comparison. +Each day supplies a new carbon offer and new demands; previous reserves are +kept but cannot be withdrawn. Run with `run!(model; steps=1, outputs=:all)`. +""" +function build_allocation_comparison(; + fixed_model=FixedFractionAllocation(0.5, 0.3), + demand_model=DemandAllocation(), + plant_a=(carbon_offer=10.0, leaf_demand=8.0, wood_demand=2.0), + plant_b=(carbon_offer=10.0, leaf_demand=8.0, wood_demand=2.0), +) + template = allocation_template(fixed_model) + return CompositeModel( + allocation_plant(:plant_a, template; plant_a...), + allocation_plant(:plant_b, template; plant_b..., allocation=demand_model); + environment=(duration=Day(1),), + ) +end + +end # module TwoPlantAllocationExample diff --git a/test/runtests.jl b/test/runtests.jl index 6d52eabcd..7dfec9138 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -186,6 +186,10 @@ else include("test-maespa-model-example.jl") end + @testset "Two-plant allocation comparison" begin + include("test-two-plant-allocation.jl") + end + @testset "Status" begin include("test-Status.jl") end diff --git a/test/test-two-plant-allocation.jl b/test/test-two-plant-allocation.jl new file mode 100644 index 000000000..64720191e --- /dev/null +++ b/test/test-two-plant-allocation.jl @@ -0,0 +1,306 @@ +module TwoPlantAllocationTests + +using Test, PlantSimEngine, Dates + +include(joinpath(@__DIR__, "..", "examples", "two_plant_allocation.jl")) +using .TwoPlantAllocationExample + +function allocation_status(offer, leaf_demand, wood_demand; initial=(0, 0, 0)) + Status( + carbon_offer=offer, + leaf_demand=leaf_demand, + wood_demand=wood_demand, + leaf_growth=zero(offer), + wood_growth=zero(offer), + reserve_change=zero(offer), + leaf_carbon=oftype(offer, initial[1]), + wood_carbon=oftype(offer, initial[2]), + reserve_carbon=oftype(offer, initial[3]), + ) +end + +allocation_amounts(status) = + (status.leaf_growth, status.wood_growth, status.reserve_change) +carbon_stocks(status) = + (status.leaf_carbon, status.wood_carbon, status.reserve_carbon) + +function calculate!(model, status) + PlantSimEngine.run!(model, status, NamedTuple(), nothing, nothing) + return status +end + +@testset "Allocation equations and carbon conservation" begin + cases = ( + (name="balanced supply", offer=10.0, leaf=8.0, wood=2.0, + fixed=(5.0, 2.0, 3.0), demand=(8.0, 2.0, 0.0)), + (name="scarce supply", offer=4.0, leaf=8.0, wood=2.0, + fixed=(2.0, 1.2, 0.8), demand=(3.2, 0.8, 0.0)), + (name="supply exceeds both demands", offer=20.0, leaf=3.0, wood=2.0, + fixed=(3.0, 2.0, 15.0), demand=(3.0, 2.0, 15.0)), + (name="no supply", offer=0.0, leaf=8.0, wood=2.0, + fixed=(0.0, 0.0, 0.0), demand=(0.0, 0.0, 0.0)), + (name="no demand", offer=10.0, leaf=0.0, wood=0.0, + fixed=(0.0, 0.0, 10.0), demand=(0.0, 0.0, 10.0)), + (name="wood demand only", offer=10.0, leaf=0.0, wood=2.0, + fixed=(0.0, 2.0, 8.0), demand=(0.0, 2.0, 8.0)), + ) + + for case in cases + @testset "$(case.name)" begin + for (model, expected) in ( + (FixedFractionAllocation(0.5, 0.3), case.fixed), + (DemandAllocation(), case.demand), + ) + initial = (11.0, 17.0, 23.0) + status = allocation_status(case.offer, case.leaf, case.wood; initial) + calculate!(model, status) + + @test all(isapprox.(allocation_amounts(status), expected)) + @test all(isapprox.(carbon_stocks(status), initial .+ expected)) + @test sum(allocation_amounts(status)) ≈ case.offer + @test sum(carbon_stocks(status)) - sum(initial) ≈ case.offer + @test 0 <= status.leaf_growth <= case.leaf + @test 0 <= status.wood_growth <= case.wood + @test status.reserve_change >= 0 + end + end + end +end + +@testset "Allocation rejects invalid fractions and inputs" begin + for fractions in ((-0.1, 0.3), (0.5, -0.1), (0.8, 0.3), (1.1, 0.0), + (NaN, 0.3), (0.5, NaN), (Inf, 0.3), (0.5, Inf)) + @test_throws ArgumentError FixedFractionAllocation(fractions...) + end + + for model in (FixedFractionAllocation(0.5, 0.3), DemandAllocation()) + for input in (:carbon_offer, :leaf_demand, :wood_demand) + for invalid in (-1.0, NaN, Inf, -Inf) + status = allocation_status(10.0, 8.0, 2.0) + calculate!(model, status) + saved_amounts = allocation_amounts(status) + saved_stocks = carbon_stocks(status) + setproperty!(status, input, invalid) + @test_throws ArgumentError calculate!(model, status) + @test allocation_amounts(status) == saved_amounts + @test carbon_stocks(status) == saved_stocks + end + end + end + + overflow = allocation_status(10.0, floatmax(Float64), floatmax(Float64)) + @test_throws ArgumentError calculate!(DemandAllocation(), overflow) + @test carbon_stocks(overflow) == (0.0, 0.0, 0.0) + + template = allocation_template(FixedFractionAllocation(0.5, 0.3)) + @test_throws ArgumentError allocation_plant(:invalid, template; carbon_offer=-1.0) + @test_throws ArgumentError allocation_plant(:invalid, template; leaf_demand=NaN) + @test_throws ArgumentError allocation_plant(:invalid, template; wood_demand=Inf) +end + +@testset "Boundary allocation fractions remain valid" begin + for (fractions, expected) in ( + ((0.0, 0.0), (0.0, 0.0, 10.0)), + ((1.0, 0.0), (8.0, 0.0, 2.0)), + ((0.0, 1.0), (0.0, 2.0, 8.0)), + ) + status = allocation_status(10.0, 8.0, 2.0) + calculate!(FixedFractionAllocation(fractions...), status) + @test allocation_amounts(status) == expected + @test sum(carbon_stocks(status)) == 10.0 + end +end + +@testset "Roundoff preserves nonnegative allocation and demand caps" begin + cases = ( + (FixedFractionAllocation(0.01f0, 0.99f0), 0.7f0, 10.0f0, 10.0f0), + (DemandAllocation{Float32}(), 0.3f0, 0.1f0, 0.2f0), + (DemandAllocation(), 1.0, 0.2, 0.1), + ) + for (model, offer, leaf_demand, wood_demand) in cases + status = allocation_status(offer, leaf_demand, wood_demand) + calculate!(model, status) + # Exact inequalities catch a negative reserve or an allocation just + # above demand; approximate equality alone would hide these defects. + @test 0 <= status.leaf_growth <= leaf_demand + @test 0 <= status.wood_growth <= wood_demand + @test status.reserve_change >= 0 + @test sum(allocation_amounts(status)) ≈ offer + @test sum(carbon_stocks(status)) ≈ offer + end +end + +@testset "Repeated calculations replace step outputs and preserve stocks" begin + for model in (FixedFractionAllocation(0.5, 0.3), DemandAllocation()) + status = allocation_status(10.0, 8.0, 2.0) + calculate!(model, status) + saved_stocks = carbon_stocks(status) + + status.carbon_offer = 0.0 + calculate!(model, status) + @test allocation_amounts(status) == (0.0, 0.0, 0.0) + @test carbon_stocks(status) == saved_stocks + + status.carbon_offer = 4.0 + status.leaf_demand = 0.0 + status.wood_demand = 0.0 + calculate!(model, status) + @test allocation_amounts(status) == (0.0, 0.0, 4.0) + @test carbon_stocks(status) == saved_stocks .+ (0.0, 0.0, 4.0) + @test sum(carbon_stocks(status)) ≈ 14.0 + end +end + +@testset "Float32 allocation and alternative-model declarations" begin + fixed = FixedFractionAllocation(0.5f0, 0.3f0) + demand = DemandAllocation{Float32}() + @test fixed isa PlantSimEngine.Examples.AbstractCarbon_AllocationModel + @test demand isa PlantSimEngine.Examples.AbstractCarbon_AllocationModel + @test typeof(fixed) != typeof(demand) + @test process(fixed) == process(demand) == :carbon_allocation + + comparison = Authoring.compare_models(fixed, demand) + @test comparison.same_process + @test comparison.override_compatible + @test !comparison.requires_binding_changes + + for (model, expected) in ((fixed, (5.0f0, 2.0f0, 3.0f0)), + (demand, (8.0f0, 2.0f0, 0.0f0))) + status = allocation_status(10.0f0, 8.0f0, 2.0f0) + calculate!(model, status) + @test allocation_amounts(status) == expected + @test all(value -> value isa Float32, allocation_amounts(status)) + @test all(value -> value isa Float32, carbon_stocks(status)) + @test Authoring.validate_model(model; strict=true).valid + end +end + +@testset "Daily carbon offers cannot be reapplied every hour" begin + for model in (FixedFractionAllocation(0.5, 0.3), DemandAllocation()) + hourly = CompositeModel(model; + status=(carbon_offer=10.0, leaf_demand=8.0, wood_demand=2.0), + environment=(duration=Hour(1),)) + @test_throws "timestep_hint.required=1 day" run!(hourly; steps=1) + end +end + +@testset "Two plants compare different allocation models in one simulation" begin + scenario = build_allocation_comparison() + @test scenario isa CompositeModel + @test Set(object_ids(scenario)) == Set((ObjectId(:plant_a), ObjectId(:plant_b))) + @test Authoring.validate_scenario(scenario; strict=true).valid + + applications = Diagnostics.explain_applications(scenario) + application_a = only(row for row in applications if row.application_id == :plant_a__allocation) + application_b = only(row for row in applications if row.application_id == :plant_b__allocation) + @test length(applications) == 2 + @test application_a.process == application_b.process + @test application_a.model_type <: FixedFractionAllocation + @test application_b.model_type <: DemandAllocation + @test application_a.target_ids == [:plant_a] + @test application_b.target_ids == [:plant_b] + + simulation = run!(scenario; outputs=:all) + state_a = final_state(simulation, :plant_a) + state_b = final_state(simulation, :plant_b) + @test current_step(simulation) == 1 + @test allocation_amounts(state_a) == (5.0, 2.0, 3.0) + @test allocation_amounts(state_b) == (8.0, 2.0, 0.0) + @test carbon_stocks(state_a) == (5.0, 2.0, 3.0) + @test carbon_stocks(state_b) == (8.0, 2.0, 0.0) + @test sum(carbon_stocks(state_a)) + sum(carbon_stocks(state_b)) == 20.0 + + continue!(simulation; steps=2) + @test current_step(simulation) == 3 + @test carbon_stocks(final_state(simulation, :plant_a)) == (15.0, 6.0, 9.0) + @test carbon_stocks(final_state(simulation, :plant_b)) == (24.0, 6.0, 0.0) + + # With no new carbon, plant A must stop growing without reusing yesterday's + # outputs. Plant B still receives its own unchanged supply and demands. + status_a = model_status(scenario, :plant_a) + status_b = model_status(scenario, :plant_b) + status_a.carbon_offer = 0.0 + @test (status_b.carbon_offer, status_b.leaf_demand, status_b.wood_demand) == (10.0, 8.0, 2.0) + step!(simulation) + @test current_step(simulation) == 4 + @test allocation_amounts(final_state(simulation, :plant_a)) == (0.0, 0.0, 0.0) + @test carbon_stocks(final_state(simulation, :plant_a)) == (15.0, 6.0, 9.0) + @test carbon_stocks(final_state(simulation, :plant_b)) == (32.0, 8.0, 0.0) + + # Carbon offered to a plant with no growth demand goes entirely to reserve. + status_a.carbon_offer = 4.0 + status_a.leaf_demand = 0.0 + status_a.wood_demand = 0.0 + continue!(simulation; steps=2) + @test current_step(simulation) == 6 + @test allocation_amounts(final_state(simulation, :plant_a)) == (0.0, 0.0, 4.0) + @test allocation_amounts(final_state(simulation, :plant_b)) == (8.0, 2.0, 0.0) + @test carbon_stocks(final_state(simulation, :plant_a)) == (15.0, 6.0, 17.0) + @test carbon_stocks(final_state(simulation, :plant_b)) == (48.0, 12.0, 0.0) + @test (status_b.carbon_offer, status_b.leaf_demand, status_b.wood_demand) == (10.0, 8.0, 2.0) + + rows = collect_outputs(simulation; sink=nothing) + expected = ( + plant_a=( + leaf_growth=[5.0, 5.0, 5.0, 0.0, 0.0, 0.0], + wood_growth=[2.0, 2.0, 2.0, 0.0, 0.0, 0.0], + reserve_change=[3.0, 3.0, 3.0, 0.0, 4.0, 4.0], + leaf_carbon=[5.0, 10.0, 15.0, 15.0, 15.0, 15.0], + wood_carbon=[2.0, 4.0, 6.0, 6.0, 6.0, 6.0], + reserve_carbon=[3.0, 6.0, 9.0, 9.0, 13.0, 17.0], + ), + plant_b=( + leaf_growth=fill(8.0, 6), + wood_growth=fill(2.0, 6), + reserve_change=zeros(6), + leaf_carbon=[8.0, 16.0, 24.0, 32.0, 40.0, 48.0], + wood_carbon=[2.0, 4.0, 6.0, 8.0, 10.0, 12.0], + reserve_carbon=zeros(6), + ), + ) + @test length(rows) == 2 * 6 * 6 + for (plant_id, plant_expected) in pairs(expected) + application_id = Symbol(plant_id, "__allocation") + for (variable, expected_values) in pairs(plant_expected) + stream = filter(row -> row.object_id == plant_id && row.variable == variable, rows) + @test getproperty.(stream, :application_id) == fill(application_id, 6) + @test getproperty.(stream, :timestep) == collect(1:6) + @test getproperty.(stream, :value) ≈ expected_values + end + end + + cumulative_offers = ( + plant_a=[10.0, 20.0, 30.0, 30.0, 34.0, 38.0], + plant_b=[10.0, 20.0, 30.0, 40.0, 50.0, 60.0], + ) + stock_variables = (:leaf_carbon, :wood_carbon, :reserve_carbon) + for timestep in 1:6 + scene_carbon = 0.0 + for (plant_id, supplied) in pairs(cumulative_offers) + stored_carbon = sum( + row.value for row in rows + if row.object_id == plant_id && row.timestep == timestep && + row.variable in stock_variables + ) + @test stored_carbon ≈ supplied[timestep] + scene_carbon += stored_carbon + end + @test scene_carbon ≈ cumulative_offers.plant_a[timestep] + cumulative_offers.plant_b[timestep] + end +end + +@testset "Float32 survives plant templates and model replacement" begin + daily_inputs = (carbon_offer=10.0f0, leaf_demand=8.0f0, wood_demand=2.0f0) + scenario = build_allocation_comparison( + fixed_model=FixedFractionAllocation(0.5f0, 0.3f0), + demand_model=DemandAllocation{Float32}(), + plant_a=daily_inputs, + plant_b=daily_inputs, + ) + simulation = run!(scenario; steps=2, outputs=:all) + @test carbon_stocks(final_state(simulation, :plant_a)) == (10.0f0, 4.0f0, 6.0f0) + @test carbon_stocks(final_state(simulation, :plant_b)) == (16.0f0, 4.0f0, 0.0f0) + @test all(row -> row.value isa Float32, collect_outputs(simulation; sink=nothing)) +end + +end # module TwoPlantAllocationTests