library(plant)Scientific versioning of models
Telling a scientific change apart from a software change
theory
There are two different kinds of change to keep track of when you use plant.
The first is a change to the software. The package has a Version, just like any other R package. That version may move because an argument was renamed, some code was made faster, a bug was fixed, or the model itself changed.
The second is a change to the scientific model. This happens when an equation or a default parameter changes in a way that changes the simulation result for the same inputs. FF16 and K93 encode established published models (Kohyama, 1993; Falster, Brännström, Westoby, & Dieckmann, 2015) and therefore change rarely. TF24 and its fast variant, TF24f, are still being developed more actively.
The package version alone cannot distinguish these cases. A performance improvement and a new growth equation may both produce a new release, even though only the second change should alter the scientific result.
This matters when simulations are expensive and their outputs are archived. If only the software implementation changes, an existing result can usually be reused. If the model’s science changes, that result may need to be recomputed. Re-running everything after every software release would waste time, but never re-running anything could leave you using results from an older model.
To make that decision explicit, each model has its own scientific version. This version is independent of the package Version. It advances only when a change to the model’s equations or default parameters changes its output for identical inputs. Refactors, performance work, and interface changes do not advance it.
Reading a model’s version
Use model_version() when you need only the scientific version. It returns a string because some models have compound versions. Use model_id() when you want the model name and version together in an identifier suitable for labelling an archived run:
model_version("FF16") # scientific version[1] "1"
model_id("FF16") # name-and-version identifier[1] "FF16@v1"
To inspect all four models in the installed copy of plant:
sapply(c("FF16", "K93", "TF24", "TF24f"), model_id) FF16 K93 TF24 TF24f
"FF16@v1" "K93@v1" "TF24@v3" "TF24f@v3.1"
FF16 and K93 are scientifically frozen at v1, meaning that scientific changes to them should be exceptional. TF24 began at v2 rather than v1 because a published result used the pre-versioning “v1” science. Its version has advanced as the model has developed. TF24f uses a compound version, explained below.
The two versions, and why they are separate
| Version | Changes when… | Example |
|---|---|---|
Package Version |
anything ships — bug fixes, refactors, performance, interface changes | renaming an argument |
| Scientific version | the simulation output changes for identical inputs — an equation or a default parameter moves | recalibrating a default trait value |
For example, suppose a refactor makes a simulation run faster but leaves every number unchanged. The package Version advances because new software was released. The model’s scientific version does not advance because its scientific meaning and output have not changed.
Now suppose a default mortality parameter is recalibrated. The package version advances, and so does the scientific version of the affected model. An archived run made with the old default can then be recognised as belonging to the earlier science.
Compound versions: TF24f
Most models need only one number. TF24f is different because it is defined relative to another model. It is a fast approximation of TF24: it shares the underlying biology but uses a cheaper way to track the leaf’s operating point.
Its version therefore has two components:
"<TF24 version>.<approximation revision>"
model_id("TF24f")[1] "TF24f@v3.1"
- The major component automatically follows the scientific version of
TF24. BecauseTF24fis built onTF24, a scientific change toTF24may also changeTF24f’s output. Following the parent version ensures that archivedTF24fresults are reconsidered after such a change. - The minor component (
approximation_revision) tracks changes to the approximation itself. These changes affectTF24fbut notTF24.
For example, a scientific change to TF24 advances the first component while leaving the second in place. A change only to the fast approximation advances the second component.
Where the number lives, and the bump rule
The scientific version has one source of truth. It is declared in C++, beside the strategy implementation that it describes, as the scientific_version constant on each strategy class:
// inst/include/plant/models/ff16_strategy.h
static constexpr int scientific_version = 1;TF24f instead declares approximation_revision. The code in src/strategy_version.cpp combines this revision with the current TF24 version. The R functions read the result from the compiled code, so there is no second copy of the version number to keep in sync.
The rule is deliberately narrow: increment the scientific version when, and only when, a changed equation or default parameter changes simulation output for identical inputs. Do not increment it for refactors, performance work, or interface changes.
When adding a new strategy, developers must add a scientific_version constant to its model header and a matching dispatch arm in src/strategy_version.cpp.
The drift guard
The version still has to be changed by a person, so plant includes a test to catch one common mistake: changing a default without changing the scientific version. tests/testthat/test-model-version.R records a snapshot of each model’s scientific surface. The snapshot contains the model_id and the default strategy, including biological parameters (pars), numerical settings (control), and birth-rate defaults. Numeric values are stored at full precision.
If a default changes while the version stays fixed, the snapshot changes but the model_id does not, and the test fails. The developer must then decide whether to bump the scientific version or, if the change is genuinely non-scientific, deliberately accept the new snapshot.
This guard has a limit: it cannot detect a change to an equation when all defaults remain unchanged. Those changes still depend on code review and the versioning rule documented beside the strategy.
How downstream tools use it
The version is machine-readable so downstream tools can treat it as part of a simulation’s identity. logpile, for example, archives plant simulations in a content-addressed cache. It hashes a resolved set of inputs, including the versioned model identifier.
- After a software-only change, the scientific version is unchanged. With the same inputs, the request identity is unchanged and the archived result can be reused.
- After a scientific change, the model identifier changes. The request is therefore different, so a result from the earlier scientific version is not mistaken for the new one.
At present, logpile expects an explicit versioned model identifier rather than a bare name such as "TF24". Generate that identifier from the installed copy of plant, then include it in the request:
tf24_id <- plant::model_id("TF24")
logpile::resolve_request(list(model_id = tf24_id))$model_id
#> "TF24@v3"This makes the link between the request and the installed scientific model explicit. If you are reproducing an older archived run, keep its older identifier with the archive rather than silently relabelling it as current.
For developers
See plant’s agents.md (§7) for the maintenance rules. Family-wide conventions live in plant-meta.