Skip to content

Per-model scientific versioning#560

Merged
dfalster merged 3 commits into
developfrom
feature/model-scientific-version
Jul 9, 2026
Merged

Per-model scientific versioning#560
dfalster merged 3 commits into
developfrom
feature/model-scientific-version

Conversation

@dfalster

@dfalster dfalster commented Jul 9, 2026

Copy link
Copy Markdown
Member

Resolves #561

Context

plant has a single package Version, but its scientific models evolve on a different clock than the code: FF16/K93 are scientifically frozen (only refactors/perf/interface changes), while TF24/TF24f change commit-to-commit. Software version is therefore a poor proxy for "did the science change". Downstream tools like logpile archive simulations and need to re-run only when the science changes.

What this adds

  • A scientific version per model: a static constexpr int scientific_version on each strategy class in inst/include/plant/models/*_strategy.h, co-located with the equations it versions. Bumped manually only when equations or default parameters change the simulation output for identical inputs — never for refactors, performance, interface, or serialisation changes.
  • R accessors model_version("FF16")1 and model_id("FF16")"FF16@v1", reading the C++ constant through the new src/strategy_version.cpp dispatcher (single source of truth; no duplicated number in R).
  • A drift-guard test (tests/testthat/test-model-version.R) that snapshots each model's default pars/control and fails when a default changes without a version bump (verified: it fails on a perturbed default). Pure equation changes are not auto-detected and rely on review — documented in the header comments and agents.md §7.

Starting versions: FF16@v1, K93@v1, TF24@v2 (a published result used the pre-versioning "v1" science). TF24f is a fast approximation of TF24 and inherits its code, so it carries a compound version TF24f@v2.1 — the major component auto-tracks TF24 (a TF24 change invalidates TF24f too, the safe direction), the minor is TF24f's own approximation revision. model_version() returns a version string ("1", "2.1").

Verification

  • make full_compile roxygen clean; model_version()/model_id() return expected values.
  • Full suite: 2377 pass, 0 fail (3 pre-existing AD-kernel skips).
  • Drift guard confirmed to fail on a changed default and pass when reverted.

🤖 Generated with Claude Code

Add a scientific version to each model, separate from the package Version, so
downstream tools can tell scientific changes apart from software changes.

The version is a `static constexpr int scientific_version` on each strategy
class (inst/include/plant/models/*_strategy.h), next to the equations it
versions. It is bumped manually only when equations or default parameters
change simulation output for identical inputs -- not for refactors, performance
or interface changes. Exposed to R via model_version() (integer) and model_id()
("FF16@v1"), reading the constant through the new
src/strategy_version.cpp dispatcher.

A drift-guard test (tests/testthat/test-model-version.R) snapshots each model's
default pars/control and fails when a default changes without a version bump.

FF16, K93, TF24 and TF24f all start at version 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dfalster and others added 2 commits July 9, 2026 13:02
TF24's scientific version starts at 2, since a published result used the
pre-versioning "v1" science.

TF24f is a fast approximation of TF24 and inherits its equations/parameters, so
it now carries a compound version "<TF24 version>.<approximation_revision>"
(e.g. "TF24f@v2.1"): the major component auto-tracks
TF24_Strategy::scientific_version -- so a TF24 scientific change also
invalidates TF24f (the safe direction) -- and approximation_revision (in
tf24f_strategy.h) tracks changes to the fast approximation itself.

model_version() now returns a version string ("1", "2.1") rather than an
integer; model_id() formats it as "Model@v<version>".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dfalster
dfalster merged commit f5fa2d2 into develop Jul 9, 2026
3 checks passed
@dfalster
dfalster deleted the feature/model-scientific-version branch July 9, 2026 23:42
dfalster added a commit to traitecoevo/logpile that referenced this pull request Jul 10, 2026
Resolves #4

## Context

`logpile` content-addresses simulations by the SHA-256 fingerprint of
their resolved inputs, and `model_id` (`"FF16@v1"`) is one of those
inputs. Until now the `@vN` scientific version was **hand-typed** on the
logpile side, and the `plant` package version was recorded only as inert
`build_hash` metadata that never triggered a rerun — so a behavioural
`plant` change silently produced stale cache hits (the known gotcha in
`AGENTS.md`).

Depends on traitecoevo/plant#560, which adds `plant::model_version()` /
`plant::model_id()`.

## What this changes

- `resolve_request()` now **re-derives** the model's scientific version
from `plant::model_version()` rather than trusting the request's `@vN`.
The authoritative version flows into the fingerprint, so:
- a `plant` scientific-version bump changes the fingerprint → reruns
happen;
- a software-only `plant` release leaves it untouched → cached results
are reused.
- `parse_model_id()` now accepts a **bare model name** (`"FF16"`), a
full id (`"FF16@v1"`), or a **compound** version (`"TF24f@v2.1"`, since
TF24f is a fast approximation of TF24). The version is handled as a
string.
- A **mismatch guard**: a pinned version that disagrees with the
installed `plant` is a hard error, not a silent stale hit.
- Bare and matching-pinned ids resolve identically, so **existing
archives stay valid** (`FF16` and `FF16@v1` produce the same
fingerprint).
- `AGENTS.md` gotcha updated to reflect the new behaviour.

## Verification

- Full suite: **225 pass, 0 fail** (1 pre-existing deferred-driver
skip), with `plant` #560 installed.
- Confirmed: bare/pinned/default all resolve to `FF16@v1`; bare and
pinned fingerprints identical; `FF16@v99` errors; bumping the resolved
version changes the fingerprint.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
dfalster added a commit that referenced this pull request Jul 10, 2026
Brings the latest develop (incl. #560 per-model scientific versioning, #548
hyperparameterisation harmonisation, #556 scenario framework, #562 Windows
fixes) into the TF24 NSC storage work (#517/#554).

Conflict resolution & reconciliation:
- tests/testthat/test-strategy-tf24.R:
  * Kept the #517 drought test as an assertion of COMPLETION (the NSC storage
    pool bounds mortality and fixes the #550 blow-up at the root cause); dropped
    develop's graceful-failure/skip_on_os("windows") variant since completion is
    now platform-independent.
  * Re-pinned offspring_production against the merged science (develop's #548
    hyperpar baseline + NSC): single species 25.4 -> 82.09; two-species
    dominant 16.9 -> 67.54, with the slower species still competitively excluded
    (< 0.5). Neither pre-merge branch value survived, so both were re-measured.
- R/RcppR6.R: regenerated (make RcppR6); hash reconciled.

Apply #560 versioning to the #554 science update:
- TF24_Strategy::scientific_version 2 -> 3 (NSC storage + reserve-gated growth
  and reserves-based mortality change output for identical inputs).
- TF24f auto-tracks the major component -> TF24f@v3.1 (approximation_revision
  unchanged); re-blessed the model-version drift-guard snapshot; added a NEWS
  entry. Invalidates the logpile cache for TF24/TF24f, as intended.

Full suite: 2413 pass, 0 fail (4 pre-existing/opt-in skips).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dfalster added a commit that referenced this pull request Jul 10, 2026
Brings the latest develop (incl. #554 NSC storage + #549 soil-numerics fixes,
#556 scenario framework, #560 versioning) into the layered soil-parameter work.

Conflicts in inst/include/plant/models/tf24_environment.h resolved by COMBINING
both changes (they touch the same retention/conductivity code):
- soil_K_from_soil_theta: keep #558 per-layer parameters (K_sat/soil_moist_sat/
  n_psi via soil_parameter_value, scalar fallback) AND apply develop's #549
  clamp of theta to [0, soil_moist_sat_layer].
- psi_from_soil_moist: keep #558 per-layer parameters AND apply develop's #549
  cap of psi at soil_psi_max_.
Both now read the per-layer value where develop used the scalar, so the layered
API and the blow-up guards coexist. R/RcppR6.R regenerated (hash reconciled).

This also clears the Windows CI failure: the non-portable
"blow-up fails gracefully (#550)" test that expected run_scm() to throw is
replaced by #554's completion assertion (NSC bounds mortality on all platforms).

No scientific_version bump: #558 is an interface addition with scalar-fallback
behaviour, so default simulation output is unchanged.

Full suite: 2430 pass, 0 fail (4 pre-existing/opt-in skips).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Per-model scientific versioning (independent of package Version)

1 participant