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

library(plant)

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. Because TF24f is built on TF24, a scientific change to TF24 may also change TF24f’s output. Following the parent version ensures that archived TF24f results are reconsidered after such a change.
  • The minor component (approximation_revision) tracks changes to the approximation itself. These changes affect TF24f but not TF24.

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.

References

Falster, D. S., Brännström, Åke, Westoby, M., & Dieckmann, U. (2015). Multi-trait eco-evolutionary dynamics explain niche diversity and evolved neutrality in forests. bioRxiv, 014605. doi:10.1101/014605
Kohyama, T. (1993). Size-structured tree populations in gap-dynamic forest: The forest architecture hypothesis for the stable coexistence of species. Journal of Ecology, 81(1), 131–143.