Skip to content

Commit 23899a9

Browse files
committed
Merge branch 'main' into compile-models
2 parents e832ad8 + 8e601d5 commit 23899a9

4 files changed

Lines changed: 285 additions & 5 deletions

File tree

.github/workflows/Benchmarks.yml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
name: Benchmarks
22
on:
33
pull_request_target:
4-
branches: [main]
4+
branches: [ main ]
55
workflow_dispatch:
66
permissions:
77
pull-requests: write
88
jobs:
99
bench:
10-
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }}
10+
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{
11+
github.event_name }}
1112
runs-on: ${{ matrix.os }}
1213
timeout-minutes: 60
1314
strategy:
@@ -24,6 +25,4 @@ jobs:
2425
with:
2526
julia-version: ${{ matrix.version }}
2627
bench-on: ${{ github.event.pull_request.head.sha }}
27-
extra-pkgs: |
28-
https://github.com/PalmStudio/XPalm.jl
29-
https://github.com/VEZY/PlantBiophysics.jl
28+
extra-pkgs: https://github.com/PalmStudio/XPalm.jl,https://github.com/VEZY/PlantBiophysics.jl

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ makedocs(;
4343
],
4444
"Execution" => "model_execution.md",
4545
"Model traits" => "model_traits.md",
46+
"AI agent skill" => "agent_skill.md",
4647
"Working with data" => [
4748
"Reducing DoF" => "./working_with_data/reducing_dof.md",
4849
"Fitting" => "./working_with_data/fitting.md",

docs/src/agent_skill.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# AI agent skill
2+
3+
PlantSimEngine includes an optional Codex/OpenAI-style skill for users who want an AI agent to help write simulations or implement models.
4+
5+
The skill file is stored in the repository at:
6+
7+
```text
8+
skills/plantsimengine/SKILL.md
9+
```
10+
11+
Users can download the `skills/plantsimengine` folder and tell their agent to use the `plantsimengine` skill when working with PlantSimEngine.jl. The skill gives agents the package-specific conventions they need for:
12+
13+
- composing existing models with `ModelMapping`;
14+
- declaring spatial multiscale mappings with scale symbols and `MultiScaleModel`;
15+
- configuring multirate simulations with `ModelSpec`, `TimeStepModel`, `InputBindings`, and temporal policies;
16+
- implementing or wrapping models with `@process`, `inputs_`, `outputs_`, `run!`, hard dependencies, and model traits.
17+
18+
The canonical source is [`skills/plantsimengine/SKILL.md`](https://github.com/VirtualPlantLab/PlantSimEngine.jl/blob/main/skills/plantsimengine/SKILL.md).
19+
20+
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/`.

skills/plantsimengine/SKILL.md

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
---
2+
name: plantsimengine
3+
description: Use PlantSimEngine.jl to compose existing process models with ModelMapping, spatial multiscale MTG mappings, multirate ModelSpec configuration, and to implement or wrap new models by defining processes, inputs_, outputs_, run!, hard dependencies, and model traits.
4+
---
5+
6+
# PlantSimEngine Skill
7+
8+
Use this skill when helping with PlantSimEngine.jl simulations, model mappings, multiscale MTG coupling, multirate execution, or implementing/wrapping models.
9+
10+
PlantSimEngine has two main user roles:
11+
12+
- **Users** compose existing models. They mostly need `ModelMapping`, `MultiScaleModel`/`ModelSpec`, status initialization, variable mappings, spatial scale symbols, and multirate policies.
13+
- **Modelers** implement or wrap models. They need process identity, `inputs_`, `outputs_`, `run!`, hard dependencies, model traits, and tests that prove the model composes correctly.
14+
15+
Prefer current APIs: `ModelMapping`, `ModelSpec`, `MultiScaleModel`, `Status`, `PreviousTimeStep`, and `run!`. Treat legacy `ModelList` as compatibility plumbing unless the user is working on legacy code.
16+
17+
## First Steps
18+
19+
1. Identify whether the request is user-side mapping work or modeler-side implementation work.
20+
2. Inspect existing model declarations before inventing names:
21+
- Search for process definitions with `rg "@process|abstract type Abstract.*Model" src examples docs test`.
22+
- Search for model APIs with `rg "inputs_\\(|outputs_\\(|PlantSimEngine.run!|dep\\(" src examples test`.
23+
3. Check model IO with `inputs(model)`, `outputs(model)`, `variables(model)`, and process identity with `process(model)` when available.
24+
4. Validate mappings early with `dep(mapping)`, `to_initialize(mapping[, mtg])`, `resolved_model_specs(mapping)`, and `explain_model_specs(mapping)` when relevant.
25+
26+
## User Workflow: Existing Models
27+
28+
### Single-scale mapping
29+
30+
Use `ModelMapping` when all models share one status.
31+
32+
```julia
33+
mapping = ModelMapping(
34+
ModelA(args...),
35+
ModelB(args...);
36+
status=(x=1.0, y=0.0),
37+
)
38+
39+
out = run!(mapping, meteo)
40+
```
41+
42+
Rules:
43+
44+
- Inputs are matched to outputs by variable name after model declarations are flattened.
45+
- Variables not produced by another model must be initialized through `status`, model defaults, meteo, or another supported input source.
46+
- If two models produce the same canonical variable, inspect the graph and disambiguate before relying on incidental order.
47+
- `Status` values are reference-backed. Writing `status.x = value` mutates the cell used by coupled models.
48+
49+
### Spatial multiscale mapping
50+
51+
Use `ModelMapping` keyed by scale symbols when running on an MTG. Prefer symbol scales such as `:Scene`, `:Plant`, `:Leaf`, `:Internode`; string scale names are deprecated.
52+
53+
```julia
54+
mapping = ModelMapping(
55+
:Scene => (
56+
SceneModel(),
57+
),
58+
:Plant => (
59+
MultiScaleModel(
60+
PlantModel(),
61+
[:TT_cu => (:Scene => :TT_cu)],
62+
),
63+
),
64+
:Leaf => (
65+
LeafModel(),
66+
Status(carbon_biomass=1.0),
67+
),
68+
)
69+
70+
out = run!(mtg, mapping, meteo)
71+
```
72+
73+
Each scale tuple can contain models, `ModelSpec`s, `MultiScaleModel`s, and optional `Status(...)` initializers for variables local to that scale. A model only sees variables in its local status unless they are mapped from another scale or supplied by runtime input binding.
74+
75+
### Variable mapping forms
76+
77+
Use `MultiScaleModel(model, mapped_variables)` or pipe through `ModelSpec(model) |> MultiScaleModel(mapped_variables)`.
78+
79+
Common forms:
80+
81+
```julia
82+
:x => :Plant # scalar read from :Plant, same variable name
83+
:x => (:Plant => :y) # scalar read from :Plant variable :y
84+
:x => [:Leaf] # vector read from all :Leaf nodes
85+
:x => [:Leaf, :Internode] # vector read from several scales
86+
:x => [:Leaf => :a, :Internode => :b] # vector read with per-scale renaming
87+
:x => (Symbol("") => :y) # same-scale rename or alias
88+
PreviousTimeStep(:x) # break current-step dependency inference
89+
PreviousTimeStep(:x) => (:Plant => :y)
90+
```
91+
92+
Semantics:
93+
94+
- Scalar cross-scale mappings share a `Ref`; the source scale is expected to be unique at runtime.
95+
- Vector mappings create a `RefVector`; models must handle vector inputs, and order follows MTG traversal order.
96+
- Same-scale renaming creates a per-status alias, not a graph-wide shared variable.
97+
- `PreviousTimeStep` prevents same-step dependency edges and is the standard way to break cycles.
98+
99+
### Multirate configuration
100+
101+
Use `ModelSpec` when models run at different clocks, consume streams with temporal policies, aggregate meteo, or need scoped streams.
102+
103+
```julia
104+
daily = ClockSpec(24.0, 1.0)
105+
106+
plant_spec =
107+
ModelSpec(PlantDailyModel()) |>
108+
MultiScaleModel([:leaf_assim => [:Leaf => :A]]) |>
109+
TimeStepModel(daily) |>
110+
InputBindings(;
111+
leaf_assim=(process=:leafassimilation, scale=:Leaf, var=:A, policy=Integrate()),
112+
) |>
113+
ScopeModel(:plant)
114+
```
115+
116+
Policies:
117+
118+
- `HoldLast()` uses the latest producer value.
119+
- `Interpolate()` interpolates or holds/extrapolates producer streams.
120+
- `Integrate()` reduces over the consumer window, usually for fluxes or accumulations.
121+
- `Aggregate()` reduces over the consumer window, usually for means, extrema, or summaries.
122+
123+
Precedence:
124+
125+
1. Input policy: explicit `InputBindings(..., policy=...)` > producer `output_policy` > `HoldLast()`.
126+
2. Timestep: `TimeStepModel(...)` > non-default `timespec(model)` > meteo base step.
127+
3. Meteo sampling: explicit `MeteoBindings(...)`/`MeteoWindow(...)` > `meteo_hint(...)` > runtime defaults.
128+
129+
Use explicit `InputBindings` when several models/scales can produce the same variable, names differ, or the default temporal policy is not correct. Use `OutputRouting(; x=:stream_only)` when a producer should publish a stream without becoming the canonical status owner for `x`.
130+
131+
## Modeler Workflow: New Or Wrapped Models
132+
133+
### Choose or create the process
134+
135+
Process identity is the abstract process type, not the concrete model name. Before adding a process, search for an existing one with the same biological or physical meaning. Reuse it when the new model is an alternative implementation of the same process.
136+
137+
Create a new process only when the simulated process is genuinely new:
138+
139+
```julia
140+
PlantSimEngine.@process "maintenance_respiration" verbose=false
141+
```
142+
143+
This creates an abstract process type such as `AbstractMaintenance_RespirationModel`. Concrete implementations subtype that abstract process.
144+
145+
### Implement the model contract
146+
147+
```julia
148+
struct MyModel{T} <: AbstractSome_ProcessModel
149+
p::T
150+
end
151+
152+
PlantSimEngine.inputs_(::MyModel) = (x=0.0, y=-Inf)
153+
PlantSimEngine.outputs_(::MyModel) = (z=-Inf,)
154+
155+
function PlantSimEngine.run!(m::MyModel, models, status, meteo, constants, extra=nothing)
156+
status.z = f(status.x, status.y, meteo.T, m.p)
157+
return nothing
158+
end
159+
```
160+
161+
Rules:
162+
163+
- `inputs_` and `outputs_` are authoritative. Defaults are also initialization hints.
164+
- Use `NamedTuple()` for no inputs or no outputs.
165+
- Read and write model state through `status`. Do not store timestep-varying state in the model object.
166+
- Read weather through `meteo` and physical constants through `constants`.
167+
- In MTG runs, `extra` is the `GraphSimulation`; do not use user-defined `extra` arguments for MTG APIs.
168+
- If a variable appears in both `inputs_` and `outputs_` with the same name, remember that `variables(model)` merges declarations and later output declarations win.
169+
170+
### Wrapping existing code
171+
172+
When wrapping an external or existing model:
173+
174+
1. Identify its true inputs, outputs, parameters, weather needs, and mutable state.
175+
2. Put fixed parameters in the struct.
176+
3. Put timestep-varying inputs and outputs in `status`.
177+
4. Convert internal side effects into explicit `status` assignments.
178+
5. Keep units and timestep assumptions in docstrings and traits.
179+
6. If the external model computes several processes internally, split it into several PlantSimEngine models when users need to couple or replace those subprocesses independently. Keep it as one model only when the subprocesses are inseparable implementation details.
180+
181+
### Hard dependencies
182+
183+
Use hard dependencies when a parent model directly calls a required submodel inside its own `run!`. The runtime records the dependency but does not automatically execute it for the parent.
184+
185+
```julia
186+
PlantSimEngine.dep(::ParentModel) = (
187+
child_process=AbstractChild_ProcessModel,
188+
)
189+
190+
function PlantSimEngine.run!(m::ParentModel, models, status, meteo, constants, extra=nothing)
191+
run!(models.child_process, models, status, meteo, constants, extra)
192+
status.parent_output = g(status.child_output)
193+
end
194+
```
195+
196+
For multiscale hard dependencies, declare the target scale:
197+
198+
```julia
199+
PlantSimEngine.dep(::ParentModel) = (
200+
child_process=AbstractChild_ProcessModel => (:Leaf,),
201+
)
202+
```
203+
204+
Then call the child model explicitly on the correct target status, usually via `extra.statuses[:Leaf]` and `extra.models[:Leaf]`. Be careful: hard-dependency IO still participates in graph compilation through the owning soft node.
205+
206+
### Model traits
207+
208+
Add traits only when they are true for the model implementation, not merely convenient for one scenario.
209+
210+
```julia
211+
PlantSimEngine.TimeStepDependencyTrait(::Type{<:MyModel}) =
212+
PlantSimEngine.IsTimeStepIndependent()
213+
214+
PlantSimEngine.ObjectDependencyTrait(::Type{<:MyModel}) =
215+
PlantSimEngine.IsObjectIndependent()
216+
217+
PlantSimEngine.timespec(::Type{<:MyDailyModel}) = ClockSpec(24.0, 1.0)
218+
219+
PlantSimEngine.output_policy(::Type{<:MyFluxModel}) = (
220+
assimilation=Integrate(),
221+
)
222+
223+
PlantSimEngine.timestep_hint(::Type{<:MyModel}) =
224+
(; required=(Dates.Hour(1), Dates.Hour(6)), preferred=Dates.Hour(1))
225+
226+
PlantSimEngine.meteo_hint(::Type{<:MyModel}) = (
227+
bindings=(T=MeanReducer(),),
228+
window=RollingWindow(),
229+
)
230+
```
231+
232+
Parallel traits are mainly for single-scale execution. Multirate MTG runs are currently sequential.
233+
234+
## Validation Checklist
235+
236+
For user mappings:
237+
238+
- `to_initialize(mapping)` or `to_initialize(mapping, mtg)` lists only variables the user should really provide.
239+
- `dep(mapping)` succeeds and the dependency graph matches the expected coupling.
240+
- `explain_model_specs(mapping)` is sensible for multirate runs.
241+
- Cycles are either absent or intentionally broken with `PreviousTimeStep`.
242+
- Ambiguous multirate producers are resolved with `InputBindings`.
243+
244+
For model implementations:
245+
246+
- Unit-test `inputs_`, `outputs_`, and a direct `run!` call with a minimal `Status`.
247+
- Test single-scale composition when the model is meant to couple by variable name.
248+
- Test MTG/multiscale mapping when the model expects scalar refs, `RefVector` inputs, or cross-scale writes.
249+
- Test multirate behavior when traits, `InputBindings`, `MeteoBindings`, or `OutputRouting` matter.
250+
- Check hard dependencies by proving the parent actually calls the child and uses the child's outputs.
251+
252+
## Common Pitfalls
253+
254+
- Do not confuse hard dependencies with soft dependency scheduling. Hard dependencies are manual calls.
255+
- Do not rely on MTG topology for model execution order. Soft dependency order controls model order.
256+
- Do not assume `RefVector` order has biological meaning.
257+
- Do not map scalar reads from a scale that can have several runtime nodes unless the model really expects the chosen unique source behavior.
258+
- Do not use strings for new scale declarations. Use symbols.
259+
- Do not mutate MTG topology after status initialization unless you reinitialize or use supported dynamic helpers.
260+
- Do not use `PreviousTimeStep` as a numerical lag unless the initial value and expected temporal semantics are explicit.

0 commit comments

Comments
 (0)