Summary
Store the defaults edition as a property of each MizerParams object (rather than reading it only from the global mizer_defaults_edition option), and have all default-choice code consult the model's own edition. This would let us change the defaults mizer chooses for new models without changing the behaviour of existing models when they are recomputed.
Motivation
mizer already has the defaults_edition() mechanism for versioning the choices it makes for parameters the user does not supply. However, the edition is currently a global option, not stored in the model. Every default gate reads the current global edition:
R/species_params.R (get_gamma_default)
R/get_initial_n.R
R/project.R
R/setFishing.R (two sites: catchability and effort defaults)
R/setReproduction.R (the psi above w_repro_max choice)
R/MizerParams-class.R (default effort)
Because the edition is not recorded in the object, an existing model is not protected against a change of defaults. The given_species_params<- setter rebuilds species_params from given_species_params on every edit (params@species_params <- validSpeciesParams(value) and then setParams()), so any calculated value that is not stored in given_species_params is re-derived using whatever defaults are current. If a user bumps the global edition (or a future mizer release changes the current default edition) and then edits an existing model, its recompute silently picks up the new defaults.
This is the same underlying issue behind the n/p inconsistency (see below): the only reason a calculated default survives an edit today is if it happens to have a single, unchanging default source — or if it has been (arguably incorrectly) frozen into given_species_params.
Proposal
- Stamp the edition into the object at creation. When a
MizerParams object is built, record the edition it was built under (the current global defaults_edition()).
- Read the model's edition on recompute. Change the default gates listed above to consult the edition stored in the model rather than the global option, so that recomputing an existing model reproduces its original defaults.
- New models use the newest edition, so default choices can be changed freely for new models without affecting existing ones.
With this in place, evolving a default becomes: introduce a new edition, gate the new choice on the edition, and leave existing models (which carry their original edition) untouched.
Relationship to the n/p default leak
While investigating this I found a concrete instance of the problem. The species allometric exponents n and p have two disagreeing default sources:
newMultispeciesParams() arguments default to n = 2/3, p = 0.7 (this is what every constructed model gets, including the bundled NS_params).
validSpeciesParams() / species_params.data.frame() default to n = 3/4, p = 3/4 (the recompute path).
To stop a recompute from silently flipping n from 2/3 to 3/4, newMultispeciesParams() currently writes the n/p defaults into species_params before the given_species_params snapshot (R/newMultispeciesParams.R:179-181), so they end up baked into given_species_params even when the user never specified them. This has been the case since given_species_params was introduced in v2.5.0 (commit 0742df78).
That is a leak: given_species_params() should hold only values the user explicitly supplied. But we cannot simply drop n/p from given without either (a) unifying the two default sources, or (b) recording the edition in the model so the recompute reproduces the original choice. Per-model editions are the clean, general solution — n/p then become one edition-gated default among the others rather than a special case that has to be frozen into given.
Design questions to resolve
- Where to store the edition. A dedicated S4 slot
defaults_edition (parallels the existing mizer_version slot; needs a class change, an upgrade() path stamping legacy objects as edition 1, and regeneration of bundled test data) vs. an entry in the existing @metadata list (no class change, but semantically a squatter). A dedicated slot seems the more honest home.
- Threading the edition into the data-frame-level functions.
validSpeciesParams() (where the n/p default lives) and validGearParams() operate on bare data frames with no params in scope. The edition would need to ride along — e.g. as an attribute on the species_params / given_species_params S3 object — read by those functions and set from the model's slot. This is the bulk of the work.
- Legacy objects should map unambiguously to edition 1 on upgrade.
Benefits
given_species_params() can hold only user-specified values (fixes the n/p leak).
- Existing models remain reproducible under future changes to default choices, even across edits that trigger a recompute.
- The recompute semantics stay simple and safe ("recompute everything from
given"); only the choice of default formula becomes model-specific.
Summary
Store the defaults edition as a property of each
MizerParamsobject (rather than reading it only from the globalmizer_defaults_editionoption), and have all default-choice code consult the model's own edition. This would let us change the defaults mizer chooses for new models without changing the behaviour of existing models when they are recomputed.Motivation
mizer already has the
defaults_edition()mechanism for versioning the choices it makes for parameters the user does not supply. However, the edition is currently a global option, not stored in the model. Every default gate reads the current global edition:R/species_params.R(get_gamma_default)R/get_initial_n.RR/project.RR/setFishing.R(two sites:catchabilityand effort defaults)R/setReproduction.R(thepsiabovew_repro_maxchoice)R/MizerParams-class.R(default effort)Because the edition is not recorded in the object, an existing model is not protected against a change of defaults. The
given_species_params<-setter rebuildsspecies_paramsfromgiven_species_paramson every edit (params@species_params <- validSpeciesParams(value)and thensetParams()), so any calculated value that is not stored ingiven_species_paramsis re-derived using whatever defaults are current. If a user bumps the global edition (or a future mizer release changes the current default edition) and then edits an existing model, its recompute silently picks up the new defaults.This is the same underlying issue behind the
n/pinconsistency (see below): the only reason a calculated default survives an edit today is if it happens to have a single, unchanging default source — or if it has been (arguably incorrectly) frozen intogiven_species_params.Proposal
MizerParamsobject is built, record the edition it was built under (the current globaldefaults_edition()).With this in place, evolving a default becomes: introduce a new edition, gate the new choice on the edition, and leave existing models (which carry their original edition) untouched.
Relationship to the
n/pdefault leakWhile investigating this I found a concrete instance of the problem. The species allometric exponents
nandphave two disagreeing default sources:newMultispeciesParams()arguments default ton = 2/3,p = 0.7(this is what every constructed model gets, including the bundledNS_params).validSpeciesParams()/species_params.data.frame()default ton = 3/4,p = 3/4(the recompute path).To stop a recompute from silently flipping
nfrom 2/3 to 3/4,newMultispeciesParams()currently writes then/pdefaults intospecies_paramsbefore thegiven_species_paramssnapshot (R/newMultispeciesParams.R:179-181), so they end up baked intogiven_species_paramseven when the user never specified them. This has been the case sincegiven_species_paramswas introduced in v2.5.0 (commit0742df78).That is a leak:
given_species_params()should hold only values the user explicitly supplied. But we cannot simply dropn/pfromgivenwithout either (a) unifying the two default sources, or (b) recording the edition in the model so the recompute reproduces the original choice. Per-model editions are the clean, general solution —n/pthen become one edition-gated default among the others rather than a special case that has to be frozen intogiven.Design questions to resolve
defaults_edition(parallels the existingmizer_versionslot; needs a class change, anupgrade()path stamping legacy objects as edition 1, and regeneration of bundled test data) vs. an entry in the existing@metadatalist (no class change, but semantically a squatter). A dedicated slot seems the more honest home.validSpeciesParams()(where then/pdefault lives) andvalidGearParams()operate on bare data frames with noparamsin scope. The edition would need to ride along — e.g. as an attribute on thespecies_params/given_species_paramsS3 object — read by those functions and set from the model's slot. This is the bulk of the work.Benefits
given_species_params()can hold only user-specified values (fixes then/pleak).given"); only the choice of default formula becomes model-specific.