diff --git a/NAMESPACE b/NAMESPACE index e4c3d58f1..4abb4a64b 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -129,6 +129,11 @@ S3method(getYield,MizerParams) S3method(getYield,MizerSim) S3method(getYieldGear,MizerParams) S3method(getYieldGear,MizerSim) +S3method(get_f0_default,MizerParams) +S3method(get_gamma_default,MizerParams) +S3method(get_h_default,MizerParams) +S3method(get_h_default,default) +S3method(get_ks_default,MizerParams) S3method(given_species_params,MizerParams) S3method(given_species_params,MizerSim) S3method(given_species_params,data.frame) @@ -144,6 +149,7 @@ S3method(matchBiomasses,MizerParams) S3method(matchGrowth,MizerParams) S3method(matchNumbers,MizerParams) S3method(maturity,MizerParams) +S3method(measure_avail_energy,MizerParams) S3method(metab,MizerParams) S3method(plot,ArrayResourceBySize) S3method(plot,ArraySpeciesBySize) @@ -479,6 +485,7 @@ export(matchBiomasses) export(matchGrowth) export(matchNumbers) export(maturity) +export(measure_avail_energy) export(melt) export(metab) export(mizerDiffusion) diff --git a/NEWS.md b/NEWS.md index 375f19c52..5932ecdae 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,16 @@ # mizer 3.4.0 +- The functions that calculate species parameter defaults from the model — + `get_gamma_default()`, `get_f0_default()`, `get_h_default()` and + `get_ks_default()` — are now S3 generics, so an extension package can register + its own methods for them instead of reimplementing the calculation. The + measurement of the energy available in the power-law reference state, which is + where those defaults look at the prey in the model, is exported as the generic + `measure_avail_energy()`. An extension that replaces mizer's resource by a + resource system of its own only needs a method for that one function to have + the `gamma` and `f0` defaults derived against its resources. The guide to + creating an extension package explains when this is needed (#598). + - Fixed: `validSim()` passed the resource spectrum where the rate functions expect the list of other components, so diagnosing a failed simulation of a model with components created by `setComponent()` failed with `$ operator is diff --git a/R/species_params.R b/R/species_params.R index 09569d70f..02ce3a8a6 100644 --- a/R/species_params.R +++ b/R/species_params.R @@ -1582,16 +1582,31 @@ set_species_param_default <- function(object, parname, default, #' mathematical derivation. #' #' @param params A MizerParams object or a species parameter data frame +#' @param ... Unused. #' @return A vector with the values of h for all species #' @export #' @concept helper #' @family functions calculating defaults -get_h_default <- function(params) { - if (is(params, "MizerParams")) { - species_params <- params@species_params - } else { - species_params <- validSpeciesParams(params) - } +get_h_default <- function(params, ...) { + UseMethod("get_h_default") +} + +#' @export +get_h_default.MizerParams <- function(params, ...) { + h_from_species_params(params@species_params) +} + +#' @export +get_h_default.default <- function(params, ...) { + h_from_species_params(validSpeciesParams(params)) +} + +#' Calculate the default `h` from a species parameter data frame +#' +#' @param species_params A validated species parameter data frame +#' @return A vector with the values of h for all species +#' @noRd +h_from_species_params <- function(species_params) { assert_that("n" %in% names(species_params)) species_params <- set_species_param_default(species_params, "f0", 0.6) if (!("h" %in% colnames(species_params))) { @@ -1659,10 +1674,43 @@ get_h_default <- function(params) { #' The caller is responsible for having set the `q` and `gamma` species #' parameters and the `search_vol` slot that the measurement is to use. #' +#' # For extension authors +#' +#' This function is the hook by which an extension package that changes the +#' resource structure of the model keeps [get_gamma_default()] and +#' [get_f0_default()] working. Mizer's own method measures the energy available +#' from the single resource spectrum in the `initial_n_pp` slot. An extension +#' that replaces that resource by a system of its own registers a method for its +#' marker class, for example +#' +#' ``` +#' measure_avail_energy.mizerMR <- function(params, ...) { +#' # Put each of the extension's resources into the power-law +#' # reference state and measure the energy they make available. +#' } +#' ``` +#' +#' and both defaults then measure against that system without the extension +#' having to reimplement the rest of the calculation. A method must return the +#' coefficient \eqn{A_i} described above, not the encounter rate itself, and +#' should follow mizer in measuring a property of the species parameters: leave +#' out any dynamic modulation of the search volume, and any additive encounter +#' contribution, that your extension applies. Folding such a factor into the +#' measurement folds it into `gamma`, which then determines the search volume, +#' so the factor is re-applied on every rebuild (issues #577 and #586). +#' #' @param params A MizerParams object +#' @param ... Unused. #' @return A vector with one number for each species -#' @noRd -measure_avail_energy <- function(params) { +#' @export +#' @concept helper +#' @family functions calculating defaults +measure_avail_energy <- function(params, ...) { + UseMethod("measure_avail_energy") +} + +#' @export +measure_avail_energy.MizerParams <- function(params, ...) { params@initial_n[] <- 0 params@ext_encounter[] <- 0 params@other_encounter <- list() @@ -1700,13 +1748,23 @@ measure_avail_energy <- function(params) { #' deliberately does not go through the extension dispatch chain, nor through an #' encounter function registered with [setRateFunction()]. #' +#' This is an S3 generic. An extension package that replaces mizer's single +#' resource by a resource system of its own directs the measurement at that +#' system by registering a method for [measure_avail_energy()], which is where +#' the energy available in the reference state is measured. +#' #' @param params A MizerParams object +#' @param ... Unused. #' @return A vector with the values of gamma for all species #' @export #' @concept helper #' @family functions calculating defaults -get_gamma_default <- function(params) { - assert_that(is(params, "MizerParams")) +get_gamma_default <- function(params, ...) { + UseMethod("get_gamma_default") +} + +#' @export +get_gamma_default.MizerParams <- function(params, ...) { species_params <- params@species_params %>% set_species_param_default("f0", 0.6) if (!("gamma" %in% colnames(species_params))) { @@ -1782,13 +1840,23 @@ get_gamma_default <- function(params) { #' deliberately does not go through the extension dispatch chain, nor through an #' encounter function registered with [setRateFunction()]. #' +#' This is an S3 generic. An extension package that replaces mizer's single +#' resource by a resource system of its own directs the measurement at that +#' system by registering a method for [measure_avail_energy()], which is where +#' the energy available in the reference state is measured. +#' #' @param params A MizerParams object +#' @param ... Unused. #' @return A vector with the values of f0 for all species #' @export #' @concept helper #' @family functions calculating defaults -get_f0_default <- function(params) { - assert_that(is(params, "MizerParams")) +get_f0_default <- function(params, ...) { + UseMethod("get_f0_default") +} + +#' @export +get_f0_default.MizerParams <- function(params, ...) { species_params <- params@species_params %>% set_species_param_default("f0", 0.6) if (!("gamma" %in% colnames(species_params))) { @@ -1837,13 +1905,18 @@ get_f0_default <- function(params) { #' mathematical derivation. #' #' @param params A MizerParams object +#' @param ... Unused. #' @return A vector with the values of ks for all species #' @export #' @concept helper #' @family functions calculating defaults -get_ks_default <- function(params) { - assert_that(is(params, "MizerParams"), - "n" %in% names(params@species_params), +get_ks_default <- function(params, ...) { + UseMethod("get_ks_default") +} + +#' @export +get_ks_default.MizerParams <- function(params, ...) { + assert_that("n" %in% names(params@species_params), "p" %in% names(params@species_params)) if (!"h" %in% names(params@species_params) || any(is.na(params@species_params[["h"]]))) { diff --git a/docs/llms.txt b/docs/llms.txt index 477fd2c4c..c38213bed 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -1074,6 +1074,9 @@ users building extensions or working with model objects directly. [`w2l()`](https://sizespectrum.org/mizer/reference/l2w.md) : Length-weight conversion +- [`measure_avail_energy()`](https://sizespectrum.org/mizer/reference/measure_avail_energy.md) + : Measure the available energy in a power-law prey spectrum + - [`needs_upgrading()`](https://sizespectrum.org/mizer/reference/needs_upgrading.md) : Determine whether a MizerParams or MizerSim object needs to be upgraded diff --git a/inst/llms.txt b/inst/llms.txt index 058b92eec..d944f178b 100644 --- a/inst/llms.txt +++ b/inst/llms.txt @@ -878,6 +878,9 @@ users building extensions or working with model objects directly. [`w2l()`](https://sizespectrum.org/mizer/reference/l2w.md) : Length-weight conversion +- [`measure_avail_energy()`](https://sizespectrum.org/mizer/reference/measure_avail_energy.md) + : Measure the available energy in a power-law prey spectrum + - [`needs_upgrading()`](https://sizespectrum.org/mizer/reference/needs_upgrading.md) : Determine whether a MizerParams or MizerSim object needs to be upgraded diff --git a/inst/skills/create-extension-package/SKILL.md b/inst/skills/create-extension-package/SKILL.md index b5a935773..211f90f33 100644 --- a/inst/skills/create-extension-package/SKILL.md +++ b/inst/skills/create-extension-package/SKILL.md @@ -495,6 +495,59 @@ 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`. +#### But they do go through your `get_*_default()` method + +That non-dispatching measurement is right for an extension that *modulates* a +rate mizer already has. It is wrong for an extension that changes the model's +structure — most of all one that replaces the single resource spectrum by a +resource system of its own. The `gamma` values that +`newMultispeciesParams()` auto-derived at construction time were calibrated +against the resource that was there then. Swapping that resource out afterwards +does not re-derive them, so every species that did not come with a `gamma` of +its own is left calibrated against a resource the model no longer has. + +Nothing in core mizer can notice this for you, and re-calling +`get_gamma_default()` will not fix it, because its measurement deliberately does +not see your resource system. The five functions involved are therefore S3 +generics, so you can supply the part that mizer cannot know: + +| Generic | What a method should return | +|---|---| +| `measure_avail_energy()` | the coefficient $A_i$ in $E_i(w) = A_i w^{2 + q_i - \lambda}$, the energy the reference state makes available | +| `get_gamma_default()` | search volume coefficient `gamma` for each species | +| `get_f0_default()` | target feeding level `f0` for each species | +| `get_h_default()` | maximum intake rate coefficient `h` | +| `get_ks_default()` | standard metabolic rate coefficient `ks` | + +`measure_avail_energy()` is nearly always the only one you need. It is the +single point at which `get_gamma_default()` and `get_f0_default()` look at the +prey in the model, so a method for it redirects both defaults at your resource +system without your having to reimplement either: + +```r +#' @export +measure_avail_energy.mizerShelf <- function(params, ...) { + # Put the extension's own resources into the power-law reference state + # and return the energy they make available to each species. +} +``` + +Because the rate setters call the generics, an object of your class picks the +method up on every rebuild — a user who hands `gamma` back to mizer by removing +it from `given_species_params()` gets a `gamma` derived against your resources +from then on. + +Two rules for such a method: + +- Return a property of the *species parameters*, not of the model's dynamics. + Leave out any dynamic modulation of the search volume, and any additive + encounter contribution, that your extension applies. Folding such a factor in + folds it into `gamma`, and since `gamma` then determines the search volume, + the factor gets re-applied on every rebuild (issues #577 and #586). +- Call `NextMethod()` if what you are doing is adding to mizer's reference + state rather than replacing it, so that another extension in the chain still + gets its turn. + ### Creating objects: the two commands A constructor function that returns a `mizerShelf` object must end with these @@ -677,6 +730,10 @@ When building a dispatching extension package, verify the following: own with `other_mort()` or `other_encounter()`, never by assigning into `params@other_mort` or `params@other_encounter` directly. See [Metadata-only extensions: mizerStarvation]. +- [ ] If your package changes the resource structure of the model, define a + `measure_avail_energy()` method so that the `gamma` and `f0` defaults are + derived against your resources rather than against the one mizer replaced. + See [But they do go through your `get_*_default()` method]. - [ ] Report anything you tell the user with `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. diff --git a/inst/skills/extend-mizer/SKILL.md b/inst/skills/extend-mizer/SKILL.md index ba6dc6d1d..98b36e496 100644 --- a/inst/skills/extend-mizer/SKILL.md +++ b/inst/skills/extend-mizer/SKILL.md @@ -67,6 +67,14 @@ not enter the power-law reference state used by `get_gamma_default()` and `other_encounter()`, including a component's `encounter_fun`: they describe feeding on the reference resource alone, before extra food sources are added. +If instead of adding food you *replace* the resource — a package that installs a +resource system of its own — then any `gamma` mizer auto-derived at construction +time is calibrated against a resource that is no longer in the model, and it +will not be re-derived on its own. Give your class a `measure_avail_energy()` +method, which is the hook both defaults use to look at the prey in the model; +see the section "But they do go through your `get_*_default()` method" in +[Creating a mizer extension package](https://sizespectrum.org/mizer/articles/guide-create-extension-package.html). + ## Replacing a rate function Use when the model still follows mizer's standard flow but one step should be diff --git a/man/get_f0_default.Rd b/man/get_f0_default.Rd index 7edafaaa6..7fdf50bbb 100644 --- a/man/get_f0_default.Rd +++ b/man/get_f0_default.Rd @@ -4,10 +4,12 @@ \alias{get_f0_default} \title{Get default value for f0} \usage{ -get_f0_default(params) +get_f0_default(params, ...) } \arguments{ \item{params}{A MizerParams object} + +\item{...}{Unused.} } \value{ A vector with the values of f0 for all species @@ -35,12 +37,18 @@ so external encounter and contributions registered with \code{\link[=other_encou (including component encounter functions) are excluded. The measurement also deliberately does not go through the extension dispatch chain, nor through an encounter function registered with \code{\link[=setRateFunction]{setRateFunction()}}. + +This is an S3 generic. An extension package that replaces mizer's single +resource by a resource system of its own directs the measurement at that +system by registering a method for \code{\link[=measure_avail_energy]{measure_avail_energy()}}, which is where +the energy available in the reference state is measured. } \seealso{ Other functions calculating defaults: \code{\link[=get_gamma_default]{get_gamma_default()}}, \code{\link[=get_h_default]{get_h_default()}}, -\code{\link[=get_ks_default]{get_ks_default()}} +\code{\link[=get_ks_default]{get_ks_default()}}, +\code{\link[=measure_avail_energy]{measure_avail_energy()}} } \concept{functions calculating defaults} \concept{helper} diff --git a/man/get_gamma_default.Rd b/man/get_gamma_default.Rd index e94aeff66..50e8f1ffd 100644 --- a/man/get_gamma_default.Rd +++ b/man/get_gamma_default.Rd @@ -4,10 +4,12 @@ \alias{get_gamma_default} \title{Get default value for gamma} \usage{ -get_gamma_default(params) +get_gamma_default(params, ...) } \arguments{ \item{params}{A MizerParams object} + +\item{...}{Unused.} } \value{ A vector with the values of gamma for all species @@ -29,12 +31,18 @@ so external encounter and contributions registered with \code{\link[=other_encou (including component encounter functions) are excluded. The measurement also deliberately does not go through the extension dispatch chain, nor through an encounter function registered with \code{\link[=setRateFunction]{setRateFunction()}}. + +This is an S3 generic. An extension package that replaces mizer's single +resource by a resource system of its own directs the measurement at that +system by registering a method for \code{\link[=measure_avail_energy]{measure_avail_energy()}}, which is where +the energy available in the reference state is measured. } \seealso{ Other functions calculating defaults: \code{\link[=get_f0_default]{get_f0_default()}}, \code{\link[=get_h_default]{get_h_default()}}, -\code{\link[=get_ks_default]{get_ks_default()}} +\code{\link[=get_ks_default]{get_ks_default()}}, +\code{\link[=measure_avail_energy]{measure_avail_energy()}} } \concept{functions calculating defaults} \concept{helper} diff --git a/man/get_h_default.Rd b/man/get_h_default.Rd index 8edbb6d2f..b24c94f37 100644 --- a/man/get_h_default.Rd +++ b/man/get_h_default.Rd @@ -4,10 +4,12 @@ \alias{get_h_default} \title{Get default value for h} \usage{ -get_h_default(params) +get_h_default(params, ...) } \arguments{ \item{params}{A MizerParams object or a species parameter data frame} + +\item{...}{Unused.} } \value{ A vector with the values of h for all species @@ -33,7 +35,8 @@ mathematical derivation. Other functions calculating defaults: \code{\link[=get_f0_default]{get_f0_default()}}, \code{\link[=get_gamma_default]{get_gamma_default()}}, -\code{\link[=get_ks_default]{get_ks_default()}} +\code{\link[=get_ks_default]{get_ks_default()}}, +\code{\link[=measure_avail_energy]{measure_avail_energy()}} } \concept{functions calculating defaults} \concept{helper} diff --git a/man/get_ks_default.Rd b/man/get_ks_default.Rd index aefec0827..7692c75b5 100644 --- a/man/get_ks_default.Rd +++ b/man/get_ks_default.Rd @@ -4,10 +4,12 @@ \alias{get_ks_default} \title{Get default value for \code{ks}} \usage{ -get_ks_default(params) +get_ks_default(params, ...) } \arguments{ \item{params}{A MizerParams object} + +\item{...}{Unused.} } \value{ A vector with the values of ks for all species @@ -27,7 +29,8 @@ mathematical derivation. Other functions calculating defaults: \code{\link[=get_f0_default]{get_f0_default()}}, \code{\link[=get_gamma_default]{get_gamma_default()}}, -\code{\link[=get_h_default]{get_h_default()}} +\code{\link[=get_h_default]{get_h_default()}}, +\code{\link[=measure_avail_energy]{measure_avail_energy()}} } \concept{functions calculating defaults} \concept{helper} diff --git a/man/measure_avail_energy.Rd b/man/measure_avail_energy.Rd new file mode 100644 index 000000000..6690d51e3 --- /dev/null +++ b/man/measure_avail_energy.Rd @@ -0,0 +1,71 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/species_params.R +\name{measure_avail_energy} +\alias{measure_avail_energy} +\title{Measure the available energy in a power-law prey spectrum} +\usage{ +measure_avail_energy(params, ...) +} +\arguments{ +\item{params}{A MizerParams object} + +\item{...}{Unused.} +} +\value{ +A vector with one number for each species +} +\description{ +Puts the model into the reference state used for deriving the default +\code{gamma} and \code{f0}: no fish, and a resource spectrum given by the power law +\eqn{\kappa w^{-\lambda}}. It then measures the resulting encounter rate at +the largest size and divides out its power-law dependence on size, so that +the returned number for each species is the coefficient \eqn{A_i} in +\eqn{E_i(w) = A_i\, w^{2 + q_i - \lambda}}. +} +\details{ +The measurement uses \code{\link[=mizerEncounter]{mizerEncounter()}} rather than \code{\link[=getEncounter]{getEncounter()}}. What is +wanted here is a property of the species parameters, not of the model's +dynamics, so it must not go through the extension dispatch chain or through a +rate function that the user has registered with \code{\link[=setRateFunction]{setRateFunction()}}. An +extension that scales the search volume would otherwise fold its own factor +into mizer's \code{gamma} default, and because that default then determines the +search volume, the factor would be re-applied on every rebuild (issue #577). +Additive encounter contributions from \code{ext_encounter} and \code{other_encounter} +are excluded for the same reason (issue #586). + +The caller is responsible for having set the \code{q} and \code{gamma} species +parameters and the \code{search_vol} slot that the measurement is to use. +} +\section{For extension authors}{ +This function is the hook by which an extension package that changes the +resource structure of the model keeps \code{\link[=get_gamma_default]{get_gamma_default()}} and +\code{\link[=get_f0_default]{get_f0_default()}} working. Mizer's own method measures the energy available +from the single resource spectrum in the \code{initial_n_pp} slot. An extension +that replaces that resource by a system of its own registers a method for its +marker class, for example + +\if{html}{\out{
}}\preformatted{measure_avail_energy.mizerMR <- function(params, ...) \{ + # Put each of the extension's resources into the power-law + # reference state and measure the energy they make available. +\} +}\if{html}{\out{
}} + +and both defaults then measure against that system without the extension +having to reimplement the rest of the calculation. A method must return the +coefficient \eqn{A_i} described above, not the encounter rate itself, and +should follow mizer in measuring a property of the species parameters: leave +out any dynamic modulation of the search volume, and any additive encounter +contribution, that your extension applies. Folding such a factor into the +measurement folds it into \code{gamma}, which then determines the search volume, +so the factor is re-applied on every rebuild (issues #577 and #586). +} + +\seealso{ +Other functions calculating defaults: +\code{\link[=get_f0_default]{get_f0_default()}}, +\code{\link[=get_gamma_default]{get_gamma_default()}}, +\code{\link[=get_h_default]{get_h_default()}}, +\code{\link[=get_ks_default]{get_ks_default()}} +} +\concept{functions calculating defaults} +\concept{helper} diff --git a/tests/testthat/test-species_params.R b/tests/testthat/test-species_params.R index 91d293acb..2e354ce61 100644 --- a/tests/testthat/test-species_params.R +++ b/tests/testthat/test-species_params.R @@ -262,6 +262,88 @@ test_that("gamma and f0 defaults ignore additive encounters (#586)", { } }) +test_that("gamma and f0 defaults use the measure_avail_energy method (#598)", { + clearExtensionChain() + withr::defer(clearExtensionChain()) + + ext <- paste0("mizerTestEnergyExt", Sys.getpid()) + chain <- setNames(NA_character_, ext) + registerExtensions(chain) + # An extension that supplies more prey than mizer's single resource, the + # way mizerMR does with its several resource spectra. + registerS3method( + "measure_avail_energy", ext, + function(params, ...) 4 * NextMethod(), + envir = asNamespace("mizer") + ) + + params <- NS_params_small + ext_params <- params + ext_params@extensions <- chain + ext_params <- coerceToExtensionClass(ext_params) + no_sp <- nrow(species_params(params)) + + # Four times the available energy needs a quarter of the search volume + # to give the same feeding level. + no_gamma <- params + no_gamma@species_params$gamma[] <- NA + ext_no_gamma <- ext_params + ext_no_gamma@species_params$gamma[] <- NA + expect_equal( + unname(suppressMessages(get_gamma_default(ext_no_gamma)) / + suppressMessages(get_gamma_default(no_gamma))), + rep(0.25, no_sp) + ) + + # `get_f0_default()` is the inverse and so also goes through the method. + f0 <- get_f0_default(params) + expect_equal(unname(get_f0_default(ext_params)), + unname(1 / ((1 / f0 - 1) / 4 + 1))) +}) + +test_that("the default calculations are generics an extension can override (#598)", { + clearExtensionChain() + withr::defer(clearExtensionChain()) + + ext <- paste0("mizerTestDefaultsExt", Sys.getpid()) + chain <- setNames(NA_character_, ext) + registerExtensions(chain) + for (generic in c("get_h_default", "get_gamma_default", + "get_f0_default", "get_ks_default")) { + registerS3method(generic, ext, + function(params, ...) 2 * NextMethod(), + envir = asNamespace("mizer")) + } + + params <- NS_params_small + ext_params <- params + ext_params@extensions <- chain + ext_params <- coerceToExtensionClass(ext_params) + + expect_equal(get_h_default(ext_params), 2 * get_h_default(params)) + expect_equal(get_ks_default(ext_params), 2 * get_ks_default(params)) + expect_equal(get_f0_default(ext_params), 2 * get_f0_default(params)) + no_gamma <- params + no_gamma@species_params$gamma[] <- NA + ext_no_gamma <- ext_params + ext_no_gamma@species_params$gamma[] <- NA + expect_equal(unname(suppressMessages(get_gamma_default(ext_no_gamma)) / + suppressMessages(get_gamma_default(no_gamma))), + rep(2, nrow(species_params(params)))) + + # The rate setters go through the generic, so a rebuild of an extension + # object picks the method up. + p <- NS_params_small + suppressMessages(given_species_params(p)$h <- NULL) + h_base <- species_params(p)$h + pe <- p + pe@extensions <- chain + pe <- coerceToExtensionClass(pe) + suppressMessages(species_params(pe)$my_extension_note <- 1) + expect_equal(unname(species_params(pe)$h / h_base), + rep(2, length(h_base))) +}) + test_that("a gamma that cannot be defaulted names the species (#586)", { withr::local_options(list(mizer_defaults_edition = 2)) params <- NS_params_small diff --git a/vignettes/guide-create-extension-package.qmd b/vignettes/guide-create-extension-package.qmd index dc6e6fc8f..335f53c2d 100644 --- a/vignettes/guide-create-extension-package.qmd +++ b/vignettes/guide-create-extension-package.qmd @@ -512,6 +512,59 @@ 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`. +#### But they do go through your `get_*_default()` method + +That non-dispatching measurement is right for an extension that *modulates* a +rate mizer already has. It is wrong for an extension that changes the model's +structure — most of all one that replaces the single resource spectrum by a +resource system of its own. The `gamma` values that +[`newMultispeciesParams()`](../reference/newMultispeciesParams.html) auto-derived at construction time were calibrated +against the resource that was there then. Swapping that resource out afterwards +does not re-derive them, so every species that did not come with a `gamma` of +its own is left calibrated against a resource the model no longer has. + +Nothing in core mizer can notice this for you, and re-calling +`get_gamma_default()` will not fix it, because its measurement deliberately does +not see your resource system. The five functions involved are therefore S3 +generics, so you can supply the part that mizer cannot know: + +| Generic | What a method should return | +|---|---| +| [`measure_avail_energy()`](../reference/measure_avail_energy.html) | the coefficient $A_i$ in $E_i(w) = A_i w^{2 + q_i - \lambda}$, the energy the reference state makes available | +| [`get_gamma_default()`](../reference/get_gamma_default.html) | search volume coefficient `gamma` for each species | +| [`get_f0_default()`](../reference/get_f0_default.html) | target feeding level `f0` for each species | +| [`get_h_default()`](../reference/get_h_default.html) | maximum intake rate coefficient `h` | +| [`get_ks_default()`](../reference/get_ks_default.html) | standard metabolic rate coefficient `ks` | + +`measure_avail_energy()` is nearly always the only one you need. It is the +single point at which `get_gamma_default()` and `get_f0_default()` look at the +prey in the model, so a method for it redirects both defaults at your resource +system without your having to reimplement either: + +```{r eval=FALSE} +#' @export +measure_avail_energy.mizerShelf <- function(params, ...) { + # Put the extension's own resources into the power-law reference state + # and return the energy they make available to each species. +} +``` + +Because the rate setters call the generics, an object of your class picks the +method up on every rebuild — a user who hands `gamma` back to mizer by removing +it from `given_species_params()` gets a `gamma` derived against your resources +from then on. + +Two rules for such a method: + +- Return a property of the *species parameters*, not of the model's dynamics. + Leave out any dynamic modulation of the search volume, and any additive + encounter contribution, that your extension applies. Folding such a factor in + folds it into `gamma`, and since `gamma` then determines the search volume, + the factor gets re-applied on every rebuild (issues #577 and #586). +- Call `NextMethod()` if what you are doing is adding to mizer's reference + state rather than replacing it, so that another extension in the chain still + gets its turn. + ### Creating objects: the two commands A constructor function that returns a `mizerShelf` object must end with these @@ -698,6 +751,10 @@ When building a dispatching extension package, verify the following: own with `other_mort()` or `other_encounter()`, never by assigning into `params@other_mort` or `params@other_encounter` directly. See [Metadata-only extensions: mizerStarvation]. +- [ ] If your package changes the resource structure of the model, define a + `measure_avail_energy()` method so that the `gamma` and `f0` defaults are + derived against your resources rather than against the one mizer replaced. + See [But they do go through your `get_*_default()` method]. - [ ] Report anything you tell the user with `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. diff --git a/vignettes/guide-extend-mizer.qmd b/vignettes/guide-extend-mizer.qmd index 70377f106..15f61a6ba 100644 --- a/vignettes/guide-extend-mizer.qmd +++ b/vignettes/guide-extend-mizer.qmd @@ -77,6 +77,14 @@ not enter the power-law reference state used by [`get_gamma_default()`](../refer `other_encounter()`, including a component's `encounter_fun`: they describe feeding on the reference resource alone, before extra food sources are added. +If instead of adding food you *replace* the resource — a package that installs a +resource system of its own — then any `gamma` mizer auto-derived at construction +time is calibrated against a resource that is no longer in the model, and it +will not be re-derived on its own. Give your class a [`measure_avail_energy()`](../reference/measure_avail_energy.html) +method, which is the hook both defaults use to look at the prey in the model; +see the section "But they do go through your `get_*_default()` method" in +[Creating a mizer extension package](https://sizespectrum.org/mizer/articles/guide-create-extension-package.html). + --- ## Replacing a rate function