name: create-extension-package description: >- Turn a working mizer extension into a shareable R package and maintain it. Use for packaging custom rates or components; choosing metadata-only or dispatching extensions; chaining S3 methods with NextMethod(); recording versions with recordExtension(); coercing, bundling, testing and upgrading extension objects; or making user reports obey info_level. For implementing the underlying extension mechanisms use the extend-mizer skill; for using an existing package use the use-extension-packages skill.
This is how to turn a mizer extension into a proper R package. It assumes you
are already comfortable writing custom rate functions or adding components with
setComponent() — the extend-mizer skill covers those — and now want to share
your extension with others or use it across several projects.
There are two kinds of extension package:
A metadata-only extension records itself in the model metadata for
reproducibility, but does not change how any mizer generic function behaves.
mizerStarvation is an
example: it adds starvation mortality via the other_mort pipeline, but it
does not need to override any user-facing mizer functions.
A dispatching extension additionally defines a new object type so that
mizer's generic functions (such as getBiomass() or plotBiomass()) can be
made to behave differently for models built with that extension.
mizerShelf is an example: it
adds detritus and carrion components and overrides getBiomass() to include
their biomasses in the result.
Both kinds are covered here, working through concrete examples from each package.
To get started quickly, clone or fork mizerExtensionTemplate, a minimal working package that illustrates all the mechanisms described here with inline comments explaining each step.
A plain R script works fine for a single project. An R package becomes worthwhile when you want:
library() everywhere.NextMethod()] below).testthat tests, roxygen2 documentation and a pkgdown website.getMetadata(params)$extensions reports the version of
each extension package used to build a model. If a collaborator opens your saved
MizerParams object in a different session, mizer can warn them if the
required package is missing or outdated.mizerStarvation adds
starvation mortality — an extra per-capita mortality term that kicks in when a
fish's energy balance is negative. It does this through the other_mort
pipeline: setStarvation() registers the name of its rate function, and
getMort() calls every function registered there and adds the result to the
mortality rate at every time step. No mizer generic function needs to be
overridden.
That is the right mechanism for an extra mortality that depends on the state of
the model but carries no state of its own, and other_mort() is how you
register it:
setStarvation <- function(params, starv_coef = 10) {
# ... set up the species parameters the rate function needs ...
other_mort(params)[["starvation"]] <- "starvMort"
params
}
other_encounter() does the same for a contribution to the encounter rate. Two
neighbouring mechanisms are easy to confuse with these:
ext_mort() or ext_encounter() instead. Those are cheaper and
are what mizer's own external mortality and external encounter use.setComponent() and give the contribution as its mort_fun or
encounter_fun argument. That entry then belongs to the component:
getComponent() reports it and removeComponent() removes it, and
other_mort() deliberately does not list it.Do not assign into params@other_mort directly, even though older versions of
mizer left no alternative and mizerStarvation still does so. A bare slot
assignment skips the check that the name really is a function and that it does
not collide with a component's.
When your package creates or modifies a MizerParams object, record that your
extension has actually been applied. Stamp the installed package version when
the component is first created; on later modifications preserve the existing
stamp:
setStarvation <- function(params, starv_coef = 10) {
# ... set up the rate function, species parameters, etc. ...
extensions <- getMetadata(params)$extensions
version <- if ("mizerStarvation" %in% names(extensions)) {
NULL
} else {
as.character(utils::packageVersion("mizerStarvation"))
}
params <- mizer::recordExtension(
params, "mizerStarvation",
version = version,
requirement = "sizespectrum/mizerStarvation"
)
params
}
recordExtension() records the installation requirement (which mizer can use
to install missing packages automatically when opening saved models with
readParams()), preserves all existing entries and version stamps, and adds
this extension to the object.
Storing this record serves two purposes:
saveParams() and
later loaded with readParams(), mizer checks the recorded extensions and
warns if any required package is not installed or is too old.coerceToExtensionClass() uses the extension record to
build the correct S3 class vector, and mizer validation uses it to repair a
stale vector or an old file saved without its extension classes.An extension that adds a species parameter column usually needs to remove it
when the user switches the extension off. Assign a table without the column and
mizer takes it out of both species_params() and given_species_params():
setStarvation <- function(params, starv_coef = 10) {
if (all(starv_coef == 0)) {
species_params(params)$starv_coef <- NULL # withdraw the column
return(params)
}
species_params(params)$starv_coef <- starv_coef
# ... set up the rate function ...
}
Do not reach into params@species_params to do this. The rule is the same
for given_species_params(params)$starv_coef <- NULL: a column mizer knows how
to calculate comes back as a calculated value, and a column of your own — which
mizer has no way of recalculating — is gone. Before mizer 3.3.1 neither of these
worked, so a package that needs the behaviour has to require that version.
mizerShelf adds two dynamical
components — detritus and carrion — to a mizer model. Beyond just computing
them, it also needs to change what certain user-facing functions return: for
example, getBiomass() should include the detritus and carrion biomasses
alongside the species biomasses.
This section explains how to achieve that without breaking the standard mizer behaviour, and without preventing other extension packages from also modifying the same function.
Suppose you define a new getBiomass() function in your package that adds your
extra biomasses to the result. That works as long as your package is the only
one that modifies getBiomass(). But what if a second extension package also
wants to add its own extra components?
If both packages replace getBiomass(), whichever one was loaded last wins, and
the other's contribution is silently lost. There is no way for the two packages
to compose their changes.
R has a built-in mechanism for exactly this situation. Every object has a
class attribute — a character string (or a vector of strings) that labels
what kind of thing it is. When you call a function like getBiomass(params), R
looks at the class of params and searches for a version of getBiomass whose
name ends in . followed by that class label, such as getBiomass.mizerShelf.
If it finds one, it calls it. If not, it tries the next class in the vector,
and so on until it reaches the base class and calls the default version.
This mechanism is called S3 dispatch, but you do not need to know that term to use it. What matters practically is:
"mizerShelf").<genericname>.<classname> (e.g. getBiomass.mizerShelf).NextMethod() to pass control to the next class
in the chain before or after your own modifications.NextMethod()NextMethod() is what makes multiple extension packages compose gracefully.
Suppose the class of params is c("mizerFoo", "mizerShelf", "MizerParams"),
meaning params is simultaneously of type mizerFoo, type mizerShelf, and
the base type MizerParams. Then calling getBiomass(params) proceeds like
this:
getBiomass.mizerFoo and calls it.getBiomass.mizerFoo calls NextMethod().getBiomass.mizerShelf and calls it.getBiomass.mizerShelf calls NextMethod().getBiomass.MizerParams and calls it.mizerFoo's biomasses are added on top.Each extension in the chain sees and extends the result of all the extensions below it. The chain grows automatically as packages are loaded, so the user does not need to coordinate anything manually.
For this to work, every method must call NextMethod() so it does not
accidentally short-circuit the chain below it. The only exception is the base
mizer method at the bottom of the chain.
Your params objects carry an S3 class vector that prepends your class name to
"MizerParams" (e.g. c("mizerShelf", "MizerParams")). All extension-specific
data lives in other_params(params) or in component parameters; the class label
is simply used for S3 method dispatch.
Because MizerParams and MizerSim are S3 classes, you do not define classes
with setClass(). R's S3 dispatch mechanism works directly with class attribute
vectors. When your constructor finishes configuring the object, it calls
recordExtension() and coerceToExtensionClass():
params <- mizer::recordExtension(
params, "mizerShelf",
version = as.character(utils::packageVersion("mizerShelf")),
requirement = "sizespectrum/mizerShelf"
)
params <- mizer::coerceToExtensionClass(params)
So R/myextension-class.R holds documentation and no code:
#' mizerShelf extension classes
#'
#' S3 extension classes for MizerParams and MizerSim that enable S3 dispatch for
#' the methods defined in this package.
#'
#' @name mizerShelf-class
#' @keywords internal
NULL
The class name is your package/extension name, and the sim class is that name
with "Sim" appended. MizerSim objects are coerced to the sim class
automatically by project().
If your package ships a ready-made MizerParams or MizerSim object in its
data/ directory, create it using your package's setup function (which sets the
S3 class and records the extension metadata) and save it using
usethis::use_data(). Because MizerParams and MizerSim are S3 classes, R's
standard data lazy-loading delivers the object with its extension S3 class
intact, so users can use the bundled dataset immediately after loading your
package.
NextMethod()Here is getBiomass.mizerShelf from mizerShelf. It calls NextMethod() first
to get the standard mizer result, then appends the detritus and carrion
biomasses:
#' @method getBiomass mizerShelf
#' @export
getBiomass.mizerShelf <- function(object, ...) {
params <- object
b <- NextMethod() # standard species biomasses
d_biomass <- sum(params@initial_n_pp *
params@dw_full * params@w_full)
b <- c(b, Detritus = d_biomass)
other <- params@initial_n_other
scalar_other <- Filter(function(x) is.numeric(x) && length(x) == 1, other)
if (length(scalar_other) > 0) b <- c(b, unlist(scalar_other))
b
}
Because plotBiomass() calls getBiomass() internally, this single override
makes biomass plots include detritus and carrion without any further changes.
Always register S3 methods in your package's NAMESPACE file. The roxygen2
@method tag does this for you automatically:
#' @method getBiomass mizerShelf
#' @export
getBiomass.mizerShelf <- function(object, ...) { ... }
setRateFunction() with method dispatchUsers who write mizer extensions often start by replacing one of the built-in
rate functions with setRateFunction():
myEncounter <- function(params, n, n_pp, n_other, t = 0, ...) {
enc <- mizerEncounter(params, n = n, n_pp = n_pp, n_other = n_other, t = t, ...)
enc + extraEncounter(params, n, n_pp, n_other, t, ...)
}
params <- setRateFunction(params, "Encounter", "myEncounter")
This works well for a single user's workflow, but it is not composable: if two
extension packages both call setRateFunction(params, "Encounter", ...),
whichever runs last silently overwrites the other. When you turn your extension
into a package, replace setRateFunction() calls with project* methods
for your extension class. These methods participate in the daisy-chain via
NextMethod(), so two packages can both modify the same rate without conflict.
project* genericsEvery standard mizer rate function has a corresponding S3 generic that
extension-aware projections call during project(). Define a method for whichever
rate your extension modifies:
| setRateFunction() key | S3 generic to override |
|------------------------|------------------------|
| "Rates" | projectRates() |
| "Encounter" | projectEncounter() |
| "FeedingLevel" | projectFeedingLevel() |
| "EReproAndGrowth" | projectEReproAndGrowth() |
| "ERepro" | projectERepro() |
| "EGrowth" | projectEGrowth() |
| "Diffusion" | projectDiffusion() |
| "PredRate" | projectPredRate() |
| "PredMort" | projectPredMort() |
| "FMort" | projectFMort() |
| "Mort" | projectMort() |
| "RDI" | projectRDI() |
| "RDD" | projectRDD() |
| "ResourceMort" | projectResourceMort() |
Remove the setRateFunction() call from your constructor and define a method
for your extension class instead:
#' @method projectEncounter mizerMyExtension
#' @export
projectEncounter.mizerMyExtension <- function(params, n, n_pp, n_other,
t = 0, ...) {
enc <- NextMethod()
enc + extraEncounter(params, n, n_pp, n_other, t, ...)
}
NextMethod() replaces the explicit call to mizerEncounter(). It passes
control down the chain — first to any lower extension's projectEncounter
method, and ultimately to projectEncounter.MizerParams, which performs the
standard mizer calculation. Each extension in the chain adds its contribution
on top of the one below it, in load order.
Three rules:
NextMethod() — omitting it silently drops all contributions
from lower extensions in the chain.... so
extra arguments pass through.setRateFunction() in your constructor for any rate that
your package handles via a project* method. The two mechanisms are
separate and should not be mixed for the same rate within an extension
package.setRateFunction() and project* methods interactA user who calls setRateFunction(params, "Encounter", "myFn") is asking for
their function to completely replace the encounter calculation for that specific
params object. Mizer honours this: when myFn is set, projectEncounter()
is not called at all for the Encounter rate, so no extension package's
projectEncounter method will run for that rate either.
This means that if a user applies setRateFunction() to a rate that your
extension package modifies via projectEncounter.mizerMyExtension, your
method will be silently bypassed for that object. It is worth documenting this
limitation for your users.
Mizer calculates some species parameters by putting the model into a reference
state and measuring a rate in it. get_gamma_default() is the one to know
about: it gives each species a search volume coefficient of 1, puts a power-law
prey spectrum in front of it, and measures the available energy to find the
gamma that delivers the target feeding level f0. get_f0_default() does the
inverse.
Those measurements use mizerEncounter(), not getEncounter(), so they do not
dispatch through your projectEncounter method and are unaffected by a
setRateFunction() registration. They also exclude the ext_encounter array
and functions registered with other_encounter(), including a component's
encounter_fun. The gamma mizer calculates therefore describes the species'
baseline search volume on the reference resource — which is exactly what a
dynamic modulation such as a temperature scalar is meant to modulate. Do not
declare gamma as a given species parameter merely to protect it from your own
method or component; that is no longer necessary, and it costs the model the
ability to let gamma follow f0.
A constructor function that returns a mizerShelf object must end with these
two lines:
params <- mizer::recordExtension(
params, "mizerShelf",
version = as.character(utils::packageVersion("mizerShelf")))
params <- mizer::coerceToExtensionClass(params)
Here is how newDetritusCarrionParams() uses them in mizerShelf:
newDetritusCarrionParams <- function(species_params, ...) {
params <- newMultispeciesParams(species_params, ...,
resource_dynamics = "detritus_dynamics")
# ... set up rate functions, components, colours ...
params <- mizer::recordExtension(
params, "mizerShelf",
version = as.character(utils::packageVersion("mizerShelf")))
params <- mizer::coerceToExtensionClass(params)
}
recordExtension() doesrecordExtension() adds the extension that created or modified this object,
taking its installation requirement from the session registry. Existing
entries keep their position and version stamps; a new entry is prepended so the
object's chain stays ordered outermost first. The result is a bill of materials
for the extensions actually applied to this model, not every extension package
that happened to be loaded when it was created.
The version stamp says which package version's object layout the new component
conforms to. A constructor supplies it because it has just created that
component. An ordinary modifier calls recordExtension(params, "mizerShelf")
without version, preserving the stamp already on the object. If several
extension setup functions are applied, their calls accumulate the full object
chain.
When the object is later loaded from disk with readParams(), mizer reads
the extension record to check that all the recorded extensions are installed in
the current session and warns the user if any are missing or outdated.
coerceToExtensionClass(params) doesAt this point params is still a plain MizerParams object as far as R is
concerned. If you called getBiomass(params) now, R would call the standard
mizer getBiomass.MizerParams rather than getBiomass.mizerShelf.
coerceToExtensionClass() reads the object's extension record, finds the
registered dispatch extensions in its recorded chain, and sets the S3 class
vector. In our example, params becomes c("mizerShelf", "MizerParams") and
R will dispatch to getBiomass.mizerShelf automatically. validParams() also
normalises the vector as part of validation, which keeps old saved objects
compatible.
Note that coercion is driven by the object's recorded chain, not by what
extensions happen to be loaded in the current session. An object created with
only mizerShelf registered will remain a mizerShelf object even if
mizerOuter is also loaded.
MizerSim objects?You do not need to call coerceToExtensionClass() yourself for MizerSim
objects. When project() creates its output it calls MizerSim(), which
in turn calls coerceToExtensionClass() on the new sim object. Because the
params object inside the sim already has the extension metadata, mizer knows to
promote the sim to mizerShelfSim automatically.
This means that after:
sim <- project(NWMed_params, t_max = 3)
sim is already of class mizerShelfSim, and any method you have defined
for that class — such as getBiomass.mizerShelfSim — will be dispatched
automatically.
If your extension modifies rates during projection using project* methods (such as projectMort.mizerStarvation or projectEncounter.mizerMyExtension), you do not need to write any methods for MizerSim.
Mizer's own simulation rate accessors — getMort(sim), getEncounter(sim), getRates(sim), and projection functions like project() — evaluate the rate step-by-step using the simulation's params object, which carries your extension class. At each time step, mizer dispatches through your project* method via S3, so your rate modifications automatically apply throughout the simulation without writing a ...Sim method.
MizerSim methodThere are two situations where writing a method for your ...Sim class is necessary:
Overriding non-rate mizer generics. Functions like getBiomass() or getN() are summary accessors rather than step-by-step projection rates. When an extension adds external dynamical components (e.g. Detritus and Carrion in mizerShelf), you define both getBiomass.mizerShelf (for MizerParams) and getBiomass.mizerShelfSim (for MizerSim) so that getBiomass(sim) and plotBiomass(sim) include the component biomasses across time.
Providing new extension-specific get* rate accessors. If your package introduces its own rate diagnostic function (such as getStarvMort() in mizerStarvation), core mizer does not define that function. To let users evaluate the diagnostic on both parameters and simulations (e.g. getStarvMort(params) and getStarvMort(sim)), define your function as an S3 generic with methods for both MizerParams and MizerSim:
#' Starvation mortality rate
#'
#' Calculates the starvation mortality rate (1/year) by species and size.
#'
#' @param object A `MizerParams` or `MizerSim` object.
#' @param ... Arguments passed on to methods.
#' @return An array (species by size for `MizerParams`, or time by species by
#' size for `MizerSim`).
#' @export
getStarvMort <- function(object, ...) {
UseMethod("getStarvMort")
}
#' @rdname getStarvMort
#' @export
getStarvMort.MizerParams <- function(object, n = initialN(object),
n_pp = initialNResource(object),
n_other = initialNOther(object),
t = 0, ...) {
starvMort(object, n = n, n_pp = n_pp, n_other = n_other, t = t, ...)
}
#' @rdname getStarvMort
#' @export
getStarvMort.MizerSim <- function(object, time_range, drop = FALSE, ...) {
if (missing(time_range)) {
time_range <- dimnames(object@n)$time
}
time_elements <- get_time_elements(object, time_range)
# Loop over time_elements and compute rate matrix for each time step
}
When your package makes a choice on the user's behalf — filling in a default,
adjusting an input, declining to carry out an instruction — report it through
mizer's own mechanism rather than with a plain message() or warning(). A
plain message() ignores info_level, is not collected with the other reports,
and is swallowed on the species_params<-() path. Four functions are exported
for this:
| Call | For |
|---|---|
| signal_info() | Any report about a choice you made or an input you adjusted |
| with_info_level() | Wrapping your entry point, so the reports raised inside it are collected and given together |
| signal_not_recalculated() | Your setter left a hand-set array alone |
| default_info_level() | The default for your own info_level argument |
Give every entry point that reports an info_level argument, forward it, and
wrap the body:
newFooParams <- function(species_params, ...,
info_level = default_info_level()) {
with_info_level(info_level = info_level, {
params <- newMultispeciesParams(species_params,
info_level = info_level, ...)
if (is.null(species_params$foo_rate)) {
signal_info("foo_rate", "No `foo_rate` provided, using 0.1.",
level = 1)
params <- setComponent(params, ...)
}
params
})
}
Handlers nest by themselves — an inner one steps aside and lets the outermost do the reporting — so wrap without checking what your caller did.
Two arguments of signal_info() are worth getting right, because both wrong
choices are invisible at the call site:
severity is not a judgement about how serious the report is, it is a fact
about whether the report has to survive suppressMessages(). Use
"warning" when the user asked for something that is not happening, and the
default "info" when you are telling them about a choice you made.
species_params<-() suppresses messages over its recalculation, so an
"info" report raised under it never reaches the user.unhandled decides what happens when nothing is collecting, for instance when
your setter is called directly. "drop" says nothing, which suits chatter that
only makes sense as part of a report about a whole model. "show" reports it
there and then, which is right when this may be all the user hears.Do not hard-code the level in the call — newMultispeciesParams(sp,
info_level = 0, ...) looks like a way to keep your constructor quiet, but a
user who passes info_level themselves then gets formal argument "info_level"
matched by multiple actual arguments, because their value arrives through ...
alongside yours.
Progress reports are the exception to all of this: they have to appear while the
work is happening, and collected reports are given at the end, so use a plain
message() for those.
When building a dispatching extension package, verify the following:
setClass() — mizer uses S3 class vectors (c("<myExtension>", "MizerParams")),
and coerceToExtensionClass() manages the class order automatically. See [S3 extension classes].recordExtension() with the installed
package version and requirement, then coerceToExtensionClass(params).NAMESPACE (via @method + @export).NextMethod() in every method override.project* method (e.g. projectEncounter.mizerMyExtension) rather than
calling setRateFunction(). See [Replacing setRateFunction() with method dispatch].other_params(params) or in
new components created with setComponent(), never as extra top-level list elements or new slots.species_params(params)$my_col <- value and
species_params(params)$my_col <- NULL, never by writing into the
@species_params slot. See
[Taking a species parameter column away again].other_mort() or other_encounter(), never by assigning into
params@other_mort or params@other_encounter directly. See
[Metadata-only extensions: mizerStarvation].signal_info() inside a
with_info_level(), never a bare message() or warning(), and give every
entry point an info_level = default_info_level() argument that it forwards.
See [Telling the user what your package decided].For metadata-only packages, only the storage item and the reporting item apply,
and coerceToExtensionClass() is not needed. Their setup functions should still
call recordExtension(), stamping the package version and requirement when
their component is first created.
A dispatching extension overrides mizer generics, so it is easy to
accidentally break some core mizer behaviour that you did not mean to change. A
powerful way to catch this is to run mizer's own test suite with the shared
test fixture replaced by an object of your subclass. If your overrides are
faithful extensions of the base behaviour, the great majority of mizer's tests
should still pass; the failures that remain pinpoint exactly where your class
diverges from a plain MizerParams object (and are often legitimate — e.g. a
test that hard-codes the single-resource object structure).
Most of mizer's tests build on a small shared fixture, NS_params_small (and a
simulation NS_sim_small derived from it), defined in
tests/testthat/helper.R. The idea is to turn that fixture into an object of
your subclass and then run the suite.
The test files are not part of the installed package, so clone the mizer source and check out the tag or commit that matches your installed version:
packageVersion("mizer") # note this, then `git checkout` the matching tag
Edit tests/testthat/helper.R and, immediately after NS_params_small has
been fully built (just before NS_sim_small is created), insert code that
replaces it with an object of your class. Because devtools::load_all() sources
the helper inside mizer's own namespace, attached packages are not on the lookup
path there, so:
library(yourPackage), andyourPackage::.For example, mizerMR (which adds multiple resources) converts the single resource into two resources like this:
suppressMessages(library(mizerMR))
local({
p1 <- NS_params_small
rp <- data.frame(resource = c("Res A", "Res B"),
kappa = p1@resource_params$kappa / 2,
lambda = p1@resource_params$lambda, r_pp = 4,
n = p1@resource_params$n, w_min = min(p1@w_full),
w_max = p1@resource_params$w_pp_cutoff)
strip <- function(m) { dimnames(m) <- NULL; m }
ir <- p1@species_params$interaction_resource
NS_params_small <<- suppressMessages(mizerMR::setMultipleResources(
p1, resource_params = rp,
resource_interaction = strip(cbind(ir, ir)),
resource_capacity = strip(rbind(p1@cc_pp / 2, p1@cc_pp / 2)),
resource_rate = strip(rbind(p1@rr_pp, p1@rr_pp)),
initial_resource = strip(rbind(p1@initial_n_pp / 2, p1@initial_n_pp / 2))))
})
For your own extension, replace this block with a call to your constructor or
conversion function, so that NS_params_small becomes an object of your S3
extension class. NS_sim_small is built from it on the next line and will then be of your
...Sim class automatically.
Run the tests from the root of the mizer source tree. Use devtools::test()
rather than testthat::test_dir(): it calls load_all(), which is required
because some mizer tests use mizer's internal (unexported) functions.
devtools::test()
The edits above are only for this experiment. Undo them with:
# from the mizer source root
system("git checkout tests/testthat")
The only failures left are genuine differences between your subclass and a
plain MizerParams object. Typical, expected ones include tests that assert the
exact structure of a single-resource object, or that index a resource array as
if it were one-dimensional. Any other failure — especially in a generic you
override — is worth investigating, as it usually means your method does not
faithfully extend the base behaviour.
As your extension evolves you may change where or how it stores its data in a
MizerParams object. Users who saved a model with an older version of your
package then need that model migrated to the new layout. mizer upgrades the
core slots itself (see ?validParams), and it lets your extension hook into
the same machinery so that your migration runs automatically.
getMetadata(params)$extensions reports, for each extension in the chain, both
the installation requirement and the version of the extension package that the
object conforms to. Write entries with recordExtension(); do not modify the
returned metadata directly. Stamp the installed version only when you create
your component (or when you upgrade the object); for ordinary modifications
call recordExtension() without a version, so the existing stamp is
preserved:
# in your setup function, when the component is first created:
params <- recordExtension(params, "myExtension",
version = as.character(packageVersion("myExtension")))
# on later modifications, preserve the stamp:
params <- recordExtension(params, "myExtension")
Stamping only on create/upgrade is important: if an ordinary modification re-stamped to the installed version, an object could claim to be current and skip a migration it actually needs.
upgrade methodmizer registers its core upgrades as methods of the S3 generic
utils::upgrade(). Register your own method for your subclass and have it
perform only your migration. It must be idempotent, must not call
NextMethod(), and must not touch the version stamp — mizer's orchestrator
re-stamps the object after calling your method.
#' @exportS3Method utils::upgrade
upgrade.myExtension <- function(object, ...) {
# Detect the old layout structurally and migrate it. Safe to run twice.
if (!is.null(object@other_params$old_location)) {
object@other_params$new_location <- object@other_params$old_location
object@other_params$old_location <- NULL
}
object
}
needs_upgrading() returns TRUE when the core mizer version is out of date
or when any extension's recorded stamp is missing or older than the
installed package version. When a user runs the object through validParams()
(directly, or via readParams(), project(), a setter, …) mizer's orchestrator
runs the core upgrade if needed and then calls each out-of-date extension's
upgrade method in turn, re-stamping each afterwards. A missing stamp counts as
out of date, so objects created before you adopted version tracking are migrated
and stamped on first use. Because your method is idempotent this is always safe.
Note that calling upgrade() on an object directly only dispatches to a single
method and does not run the full chain — validParams(params) (or
readParams()) is the entry point users should rely on.
extend-mizer skill for the full menu of extension mechanisms — custom
rate functions, external encounter and mortality, components, and subclassing.upgrade-extension-package skill, for bringing an existing package to
the state described here — one written against the S4 mizer, or one that has
fallen behind a later release. It lists the changes to make in order and the
symptoms that point at each one; this page stays the description of the
finished package.?coerceToExtensionClass?recordExtensionAny scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.