From d7a3a139f2bf3a972434c4b586b57308e1de72f3 Mon Sep 17 00:00:00 2001 From: Gustav Delius Date: Sat, 15 Aug 2026 18:58:33 +0100 Subject: [PATCH 1/3] Improvements to documentation --- R/indicator_functions.R | 8 +- dev_scripts/build_cheatsheets.R | 32 +++++++- inst/skills/analyse-and-plot/SKILL.md | 65 ++++++++++++---- .../analyse-and-plot/quick-reference.md | 6 ++ inst/skills/analyse-stability/SKILL.md | 16 +++- inst/skills/build-multispecies-model/SKILL.md | 16 +++- inst/skills/calibrate-model/SKILL.md | 27 ++++++- .../skills/calibrate-model/quick-reference.md | 4 + inst/skills/change-parameters/SKILL.md | 70 ++++++++++++++--- inst/skills/run-simulation/SKILL.md | 5 +- inst/skills/upgrade-mizer-code/SKILL.md | 44 ++++++++++- .../cheatsheet-analysis-and-plotting.Rmd | 78 ++++++++++++++----- vignettes/cheatsheet-calibration.Rmd | 26 +++++++ vignettes/cheatsheet-changing-parameters.Rmd | 74 +++++++++++++++--- vignettes/cheatsheet-fishing.Rmd | 4 +- vignettes/cheatsheet-model-setup.Rmd | 16 +++- vignettes/cheatsheet-running-simulations.Rmd | 5 +- vignettes/cheatsheet-stability.Rmd | 22 ++++-- vignettes/upgrading.Rmd | 43 ++++++++-- 19 files changed, 464 insertions(+), 97 deletions(-) diff --git a/R/indicator_functions.R b/R/indicator_functions.R index 23142cf5a..aa75d9035 100644 --- a/R/indicator_functions.R +++ b/R/indicator_functions.R @@ -127,10 +127,10 @@ getProportionOfLargeFish.MizerParams <- function(object, #' Calculate the mean weight of the community #' -#' Calculates the mean weight of the community. This is simply the total -#' biomass of the community divided by the abundance in numbers. You can -#' specify minimum and maximum weight or length range for the species. Lengths -#' take precedence over weights (i.e. if both min_l and min_w are supplied, only +#' Calculates the mean weight of the community. This is simply the total biomass +#' of the community divided by the abundance in numbers. You can specify minimum +#' and maximum weight or length for the included size range. Lengths take +#' precedence over weights (i.e. if both min_l and min_w are supplied, only #' min_l will be used). You can also specify the species to be used in the #' calculation. #' diff --git a/dev_scripts/build_cheatsheets.R b/dev_scripts/build_cheatsheets.R index 02d8acdf5..b88027a35 100644 --- a/dev_scripts/build_cheatsheets.R +++ b/dev_scripts/build_cheatsheets.R @@ -14,7 +14,9 @@ # title from `cheatsheet_topics` below) # * turns ```r into ```{r eval=FALSE} # * auto-links the first mention of each documented function to its pkgdown -# reference page, using the alias -> .Rd mapping read from man/ +# reference page, using the alias -> .Rd mapping read from man/. In the +# first column of a table the link is repeated on every row, since a table +# is read as a lookup rather than in order. # * rewrites "the `foo` skill" cross-references into links to the matching # cheatsheet # * drops ... blocks @@ -181,8 +183,13 @@ rd_alias_map <- function(man_dir = "man") { #' characters such as `gear_params`. Short bare names (`w`, `N`) read as prose #' rather than as cross-references and are skipped, as are the bare names listed #' in `nolink`, which double as data-frame column names. +#' +#' `force_to` suspends the first-mention rule up to that character position: a +#' name is linked there even if it was linked earlier in the article. It is used +#' for the first column of a table, which is read as a lookup, out of order and +#' without the prose that introduced the name. link_first_mentions <- function(line, map, seen, nolink = character(0), - min_bare = 3L) { + min_bare = 3L, force_to = 0L) { m <- gregexpr("`[^`]+`", line, perl = TRUE)[[1]] if (m[1] == -1) return(list(line = line, seen = seen)) starts <- as.integer(m) @@ -212,7 +219,8 @@ link_first_mentions <- function(line, map, seen, nolink = character(0), if (nchar(name) < min_bare) next # e.g. `w`, `N` } - if (is.null(map[[name]]) || isTRUE(seen[[name]])) next + if (is.null(map[[name]])) next + if (isTRUE(seen[[name]]) && en > force_to) next seen[[name]] <- TRUE todo <- c(todo, i) } @@ -233,6 +241,21 @@ link_first_mentions <- function(line, map, seen, nolink = character(0), } +#' Character position at which a table row's first cell ends +#' +#' Returns 0 for anything that is not a markdown table row, so the caller can +#' pass the result straight to `link_first_mentions(force_to = )`. The +#' delimiter row is excluded; a header row needs no special case, as it names +#' the column ("Function") in prose rather than holding a code span. +table_key_cell <- function(line) { + if (!grepl("^\\s*\\|", line)) return(0L) + if (grepl("^\\s*\\|[-: |]+\\|\\s*$", line)) return(0L) + close <- regexpr("\\|[^|]*\\|", line, perl = TRUE) + if (close == -1L) return(0L) + close + attr(close, "match.length") - 1L +} + + # Main conversion -------------------------------------------------------------- #' Read one SKILL.md and return its body: frontmatter, H1 and agent-only @@ -303,7 +326,8 @@ skill_to_cheatsheet <- function(vignette, spec, map, pkg_root = ".") { ln, fixed = TRUE) } res <- link_first_mentions(ln, map, seen, - nolink = spec$nolink %||% character(0)) + nolink = spec$nolink %||% character(0), + force_to = table_key_cell(ln)) seen <- res$seen out <- c(out, res$line) } diff --git a/inst/skills/analyse-and-plot/SKILL.md b/inst/skills/analyse-and-plot/SKILL.md index c6e0f8fbf..b913379e6 100644 --- a/inst/skills/analyse-and-plot/SKILL.md +++ b/inst/skills/analyse-and-plot/SKILL.md @@ -49,18 +49,20 @@ These extract raw arrays from a `MizerSim` object. | `finalN(sim)` | species abundance at last time | species × size | | `finalNResource(sim)` | resource abundance at last time | size | | `getEffort(sim)` | fishing effort | time × gear | -| `getTimes(sim)` | saved time steps | vector | +| `getTimes(sim)` | saved time steps | time | ```r -N(sim)[, "Cod", ] # time × size for Cod +N(sim)[, , 1] # time × species in smallest size class N(sim)["2010", "Cod", ] # size vector for Cod in year 2010 finalN(sim)["Cod", ] # size vector for Cod at the final time step ``` ## Summary functions -These compute derived quantities from abundances. All accept `MizerSim` or -`MizerParams`. See `?summary_functions` for the full list. +These functions compute derived quantities from abundances. All accept `MizerSim` or +`MizerParams`. +The result is a classed array that can be plotted directly with `plot()` — see +below. | Function | Returns | Dimensions | |---|---|---| @@ -86,9 +88,6 @@ getBiomass(sim, min_w = 10, max_w = 1e4) # biomass of 10g–10kg fish getYield(sim)["2010", ] # yield in year 2010 ``` -The result is a classed array that can be plotted directly with `plot()` — see -below. - ## Indicator functions These compute community-level indicators. All accept `MizerSim` (time series) or @@ -189,8 +188,7 @@ plot(getResourceMort(params)) # plankton resource mortality vs size ``` The array plots come with a small toolkit for combining and comparing them. -Every one of these has a method for every array class in the table above, -including the resource classes: +Every one of these has a method for every array class in the table above: | Function | What it does | |---|---| @@ -226,6 +224,7 @@ dedicated `plot…()` functions below — share these optional arguments: | `highlight` | character vector — draw named species with thicker lines | | `total` | logical — add a line for the community total | | `log_x`, `log_y` | logical — log-scale the x or y axis | +| `size_axis` | `"w"` (default) or `"l"` — plot against weight or against length | `tlim` replaces the deprecated `start_time`/`end_time`, and `log_x`/`log_y` replace the older single `log`. @@ -234,14 +233,44 @@ replace the older single `log`. window**: data outside the range is hidden but nothing is recomputed. To change the underlying numbers — for example the size range that a biomass is summed over — pass `min_w`/`max_w` (or `min_l`/`max_l`) to the `get…()` function -instead, e.g. `plotBiomass(sim, min_w = 10)`. +instead, e.g. `plot(getBiomass(sim, min_w = 10))`. + +`size_axis = "l"` converts the axis with the length–weight parameters `a` and +`b`, so it is unavailable for the resource, which has no species to take them +from. For a *density* it converts the y-axis too, via the appropriate Jacobian — +see the next section. + +### Which density a spectrum plot shows + +`plotSpectra()`, `plotSpectra2()`, `plotCDF()`, `plotCDF2()` and `animate()` +describe the plotted quantity with two independent logical arguments, each of +which contributes one factor of the weight: + +| | `per_log_size = FALSE` | `per_log_size = TRUE` | +|---|---|---| +| `biomass = FALSE` | number density | number density per log size | +| `biomass = TRUE` | biomass density | biomass density per log size | + +The older single `power` argument is the sum of the two (0, 1, 1, 2 across that +table) and is still accepted, but it cannot tell the two `power = 1` cells +apart — it is read as the biomass density with respect to weight, which is what +picks the y-axis label and the length-axis Jacobian. Supplying `power` together +with a flag that contradicts it is an error, so express the choice with the +flags. `plotCDF()` accepts only `per_log_size = FALSE`: a cumulative total does +not depend on the density it was accumulated from. + +**`log_x` does not change the y-axis.** Showing weight on a logarithmic axis is +a display choice; converting a density per unit weight into a density per +logarithmic weight interval is `per_log_size`. Conflating the two is the usual +reason a spectrum looks like it has the wrong slope. Which arguments apply depends on the array's shape: - `plot()` accepts `species`, `tlim`, `total`, `background`, `highlight`, `log_x`, `log_y`, `ylim`. - `plot()` accepts `species`, `highlight`, `log_x`, `log_y`, - `wlim`, `ylim`, `all.sizes`. + `wlim`, `llim`, `ylim`, `size_axis`, `all.sizes`. `size_axis` and `llim` + belong to this shape only — a plot against time has no size axis to convert. ## Dedicated plot functions @@ -249,7 +278,7 @@ Each dedicated `plot…()` function is essentially `plot()` applied to the match `get…()` array, so `plotBiomass(sim)` is `plot(getBiomass(sim))`. They accept the common arguments above, and each has a `plotly…()` counterpart (e.g. `plotlyBiomass()`) for interactive use — the array `plot()`s use `plotHover()` -instead. See `?plotting_functions`. +instead. **Against time:** @@ -267,13 +296,15 @@ instead. See `?plotting_functions`. | `plotFeedingLevel(sim)` | same as `plot(getFeedingLevel(sim))` | | `plotPredMort(sim)` | same as `plot(getPredMort(sim))` | | `plotFMort(sim)` | same as `plot(getFMort(sim))` | -| `plotSpectra(sim)` | abundance/biomass spectra: additionally overlays the resource spectrum and background species, and `biomass`/`per_log_size` choose the plotted density | +| `plotSpectra(sim)` | abundance/biomass spectra: additionally overlays the resource spectrum and background species, and `biomass`/`per_log_size` choose the plotted density (see [above](#which-density-a-spectrum-plot-shows)) | | `plotCDF(sim)` | cumulative version of the spectrum (`normalise` for proportion vs total) | | `plotGrowthCurves(sim)` | a distinct plot: size at age rather than a size spectrum | | `plotDiet(params)` | a distinct plot: stacked diet composition by prey | -**Calibration:** `plotBiomassObservedVsModel(params)`, -`plotYieldObservedVsModel(params)`. +**Calibration:** `plotBiomassObservedVsModel(params)` and +`plotYieldObservedVsModel(params)`; the latter takes a `gear` argument that +restricts both the modelled and the observed catch to the named gears. See the +`calibrate-model` skill. ```r plotBiomass(sim, species = c("Cod", "Herring"), total = TRUE) @@ -291,8 +322,8 @@ panels for a model's steady state (without the biomass-through-time panel). biomass over size — steadier than a density spectrum for eyeballing where biomass sits. `biomass = TRUE` (default) accumulates biomass, `biomass = FALSE` accumulates numbers; `normalise = FALSE` plots the cumulative total rather than -the proportion. Unlike in `plotSpectra()`, `per_log_size` is not accepted: the -integral does not depend on it. +the proportion. Unlike in `plotSpectra()`, only `per_log_size = FALSE` is +accepted: the integral does not depend on it. ```r plotCDF(NS_params, species = c("Cod", "Herring")) diff --git a/inst/skills/analyse-and-plot/quick-reference.md b/inst/skills/analyse-and-plot/quick-reference.md index da2a80322..a0e68c766 100644 --- a/inst/skills/analyse-and-plot/quick-reference.md +++ b/inst/skills/analyse-and-plot/quick-reference.md @@ -53,6 +53,12 @@ plotGrowthCurves(sim) # size vs age plotDiet(params, species = "Cod") # diet composition vs size plotCDF(sim) # cumulative biomass/abundance over size +# ── Choosing what a spectrum plot shows ─────────────────────────────────────── +plotSpectra(sim, biomass = TRUE) # biomass rather than number +plotSpectra(sim, per_log_size = TRUE) # density per log size +plotSpectra(sim, size_axis = "l") # x axis in length, not weight +plotSpectra(sim, log_x = TRUE) # display only: does NOT change the y density + # ── Plot any array directly, plus combine / compare tools ───────────────────── plot(getResourceMort(params)) # any get*() array plots directly p <- plot(getBiomass(sim), species = "Cod") diff --git a/inst/skills/analyse-stability/SKILL.md b/inst/skills/analyse-stability/SKILL.md index 4d0513b09..617a924f8 100644 --- a/inst/skills/analyse-stability/SKILL.md +++ b/inst/skills/analyse-stability/SKILL.md @@ -14,9 +14,12 @@ description: >- Tools for asking whether a mizer steady state is dynamically **stable** — and, when it is not, characterising the **limit cycle** that replaces it. These are -**experimental**: their interface may still change. All treat the resource as a -quasi-static fast variable by default (valid for the standard semichemostat -resource dynamics). +**experimental**: their interface may still change. All assume the standard +semichemostat resource dynamics. `steadyNewton()` solves for the resource +alongside the fish, so the resource density and the feeding levels it implies +are self-consistent even where consumers are satiated; `getStability()` instead +treats the resource as a quasi-static fast variable, and +`include_resource = TRUE` turns that approximation off. Distinct from calibration: the `calibrate-model` skill gets you *onto* a fixed point; what follows analyses the *dynamics around* it. The natural entry point @@ -47,6 +50,13 @@ stab # stable/unstable, spectral radius, cyc - `effort` / `reproduction` set the fishing effort and reproduction handling used when forming the map. +**Both `getStability()` and `getLimitCycleSim()` linearise at the state stored in +the object**, so a model that is not on a fixed point gets eigenvalues for the +neighbourhood of a point it is not sitting at. Both now warn when handed one; +the fix is to run `steadyNewton()` (or `steady()`) first, not to ignore the +warning. `plot(getSteadyResidual(params))` shows how far off it is — see the +`calibrate-model` skill. + `steady()` and `projectToSteady()` attach a related `"convergence"` attribute recording whether the run settled on a **steady state, a limit cycle, or neither**, together with the cycle period and relative amplitude when a cycle is diff --git a/inst/skills/build-multispecies-model/SKILL.md b/inst/skills/build-multispecies-model/SKILL.md index 39e263d4b..8875b8870 100644 --- a/inst/skills/build-multispecies-model/SKILL.md +++ b/inst/skills/build-multispecies-model/SKILL.md @@ -37,17 +37,25 @@ are truly required: | Column | Meaning | |---|---| | `species` | the species name | -| `w_inf` | von Bertalanffy asymptotic weight (g) — the required maximum-size parameter | +| `w_inf` | von Bertalanffy asymptotic weight (g) — the maximum-size parameter | Mizer derives defaults for `w_max` (the computational grid boundary, default `1.5 * w_inf`), `w_repro_max` and `w_mat` from `w_inf`. Everything else has a -sensible default or is calculated. Commonly supplied: +sensible default or is calculated. + +`w_inf` is the parameter to supply, but it need not be the one you *have*: a +table giving only `w_max`, or only lengths (`l_inf` or `l_max`, with the +length–weight parameters `a` and `b`), is accepted, and mizer fills `w_inf` in +from it and says so. Note that when `w_inf` is taken from `w_max` the two are +equal, so the `1.5 *` headroom above the asymptotic size is not there. + +Commonly supplied: | Column | Meaning | |---|---| | `w_mat` | Maturity weight (g) | -| `beta` | Preferred predator/prey mass ratio (default ~100) | -| `sigma` | Width of the lognormal predation kernel (default ~1.3) | +| `beta` | Preferred predator/prey mass ratio (default 30) | +| `sigma` | Width of the lognormal predation kernel (default 2) | | `k_vb` | von Bertalanffy K — used to derive `h` (and then `gamma`) if `h`/`gamma` absent | | `h`, `gamma` | Max intake coefficient and search-volume coefficient (alternative to `k_vb`) | | `alpha` | Assimilation efficiency (default 0.6) | diff --git a/inst/skills/calibrate-model/SKILL.md b/inst/skills/calibrate-model/SKILL.md index 962e73c79..e4ce4d96c 100644 --- a/inst/skills/calibrate-model/SKILL.md +++ b/inst/skills/calibrate-model/SKILL.md @@ -3,8 +3,9 @@ name: calibrate-model description: >- Bring a mizer model to steady state and calibrate it to observed data. Use whenever the user wants to find the steady state (steady, projectToSteady, - steadySingleSpecies, steadyNewton), match modelled biomass, yield, or growth to - observations (calibrateBiomass, matchBiomasses, matchGrowth), + steadySingleSpecies, steadyNewton), match modelled biomass, abundance, yield, + or growth to observations (calibrateBiomass, matchBiomasses, calibrateNumber, + matchNumbers, matchGrowth), set the level of density-dependent reproduction (reproduction_level<-), check whether a model is at its steady state (getSteadyResidual), or diagnose why a model will not settle or reproduce observed values. @@ -86,6 +87,28 @@ plot(getSteadyResidual(params)) # which species, and at which sizes them, re-running `steady()` between, until both are satisfied — usually a few passes. +**If your observations are numbers rather than weights**, use `calibrateNumber()` +and `matchNumbers()` in place of the two biomass functions. They are the same +functions with the factor of the weight taken out of the size integral, and they +read `number_observed` and `number_cutoff` instead of `biomass_observed` and +`biomass_cutoff`. Mixing the two — matching some species on biomass and others +on numbers — works, because each function ignores the species for which its own +observation column is `NA`. + +```r +species_params(params)$number_observed <- c(Cod = 1e6, Herring = 4e8, ...) +params <- calibrateNumber(params) +params <- matchNumbers(params) +params <- steady(params) +``` + +Whichever pair you use, a `_cutoff` value is the **smallest size the +observation counts**, in grams: a survey that misses fish under 10 g is +`biomass_cutoff = 10`, and the model is then integrated over the same range +rather than over the whole spectrum. Leave it out and the whole size range is +counted, which is the usual reason a model looks like it over-predicts a +species by a wide margin. + **Yields** are not a calibration target in mizer itself: put the observed annual yield of each gear-species pair into the `yield_observed` column of `gear_params()` and compare it with the model using diff --git a/inst/skills/calibrate-model/quick-reference.md b/inst/skills/calibrate-model/quick-reference.md index cf4669157..de4b556d5 100644 --- a/inst/skills/calibrate-model/quick-reference.md +++ b/inst/skills/calibrate-model/quick-reference.md @@ -7,6 +7,8 @@ params <- steadyNewton(params) # direct solve (experimental) # ── Calibrate to data (re-run steady() after each) ──────────────────────────── params <- calibrateBiomass(params) # total biomass → kappa params <- matchBiomasses(params) # per-species biomass +params <- calibrateNumber(params) # same, for `number_observed` instead +params <- matchNumbers(params) # of `biomass_observed` params <- matchGrowth(params) # growth → h, gamma, ks, k params <- steady(params) # re-converge @@ -15,6 +17,8 @@ reproduction_level(params) <- 0.25 reproduction_level(params) # what the model is currently tuned to # ── Verify ──────────────────────────────────────────────────────────────────── +summary(params) # includes the biomass-drift verdict +plot(getSteadyResidual(params)) # which species and sizes are still moving plotSpectra(params) plotGrowthCurves(params) plotBiomassObservedVsModel(params) diff --git a/inst/skills/change-parameters/SKILL.md b/inst/skills/change-parameters/SKILL.md index 0546d7abd..3df8aa517 100644 --- a/inst/skills/change-parameters/SKILL.md +++ b/inst/skills/change-parameters/SKILL.md @@ -73,6 +73,24 @@ have already given, or it feeds a rate array you set by hand, or it is a gear parameter that mizer reads from `gear_params()`. `species_params(params) <-` stays quiet about all three, which is what makes it the better one for scripts. +**Turning the commentary up or down.** Mizer reports the choices it makes — +defaults it filled in, inputs it adjusted, instructions it could not carry out — +at a level set by `info_level`. Most `set…()` and `new…()` functions take it as +an argument; for the ones that do not, including `species_params(params) <-` and +the rate setters, set the option instead: + +```r +options(mizer_info_level = 1) # only what matters: warnings and adjustments +options(mizer_info_level = 0) # complete silence +params <- setExtMort(params, info_level = 0) # just this one call +``` + +The default is 3 — every default mizer filled in. Level 1 keeps the reports that +tell you something went differently from how you asked, such as the "has not +taken effect" warnings above. **Level 0 is silence, not "warnings only"**: it +drops those too, so reach for 1 rather than 0 while you are still finding out +what a model does. + When you edit a whole table rather than a single column, read it back from the same accessor you assign to: @@ -119,6 +137,20 @@ the relevant setter automatically: | `beta`, `sigma`, `pred_kernel_type` | predation kernel | `setPredKernel()` | | `w_mat`, `w_mat25`, `w_repro_max`, `m` | reproduction allocation | `setReproduction()` | +`pred_kernel_type` chooses the *shape* of the kernel, and each shape reads its +own parameter columns. Changing it therefore also changes which columns matter: + +| `pred_kernel_type` | Parameter columns | +|---|---| +| `"lognormal"` (default) | `beta`, `sigma` | +| `"truncated_lognormal"` | `beta`, `sigma` (cut off at `beta * exp(3 * sigma)`) | +| `"box"` | `ppmr_min`, `ppmr_max` | +| `"power_law"` | `kernel_exp`, `kernel_l_l`, `kernel_u_l`, `kernel_l_r`, `kernel_u_r` | +| `"gaussian_mixture"` | `kernel_p`, `kernel_mean`, `kernel_sd` (multimodal preferences) | + +Any function `_pred_kernel(ppmr, ...)` you define yourself can be named in +`pred_kernel_type` too; its arguments become the required columns. + Other species parameters are used **directly** and build no array (changing them just changes the model): `alpha` (assimilation), `w_min` (egg size), `erepro` and `R_max` (reproduction), `interaction_resource`, and the length–weight @@ -192,19 +224,33 @@ forced to 1. So `f0` is the feeding level a species *would* have in that world. - Setting `interaction_resource` to anything other than 1 leaves `gamma` untouched under edition 1, so the reduction falls straight through to the realised feeding level and can starve a species outright. -- The resource scalars that enter the calibration do **not** trigger a - re-derivation. Changing `kappa` or `lambda` with `resource_params(params) <-` - rebuilds the resource arrays but leaves `gamma` and `q` alone, so the - feeding level moves, and the condition `q = n + lambda - 2` that makes the - feeding level size-independent silently breaks. After changing `lambda`, - reset `q` and `gamma` so they are derived afresh: +- The resource scalars that enter the calibration **do** trigger a + re-derivation, but only of the values mizer owns. Since mizer 3.3, changing + `lambda` recalculates every `q` and `gamma` that mizer calculated, and + changing `kappa` recalculates every calculated `gamma`; the condition + `q = n + lambda - 2` that makes the feeding level size-independent is + maintained for you. A value you **gave** is protected, as everywhere else — + so on a model that supplies `gamma` in its species parameters (`NS_params` + does) nothing moves, and the realised feeding level shifts with the new + resource. To let mizer re-derive a given value, clear it first: ```r -resource_params(params)$lambda <- 2.2 -given_species_params(params)$q <- NA # let mizer re-derive q ... -given_species_params(params)$gamma <- NA # ... and gamma with it +resource_params(params)$lambda <- 2.2 # calculated q and gamma follow +given_species_params(params)$gamma <- NA # hand a *given* gamma back to mizer ``` +Conversely, to hold calculated values against a resource change, record them as +given before you make it: + +```r +given_species_params(params)$q <- species_params(params)$q +resource_params(params)$lambda <- 2.2 # q now stays put +``` + +On mizer **< 3.3** neither refresh happened: the resource arrays were rebuilt but +`gamma` and `q` were left at the values for the old resource, and had to be set +to `NA` by hand. + ### Whether a value is given determines whether it is recalculated For derived parameters, `given_species_params()` distinguishes fixed input from @@ -289,6 +335,12 @@ resource_params(params)$r_pp <- 10 # rebuilds the replenishment rate (rr |---|---| | `kappa`, `lambda`, `w_pp_cutoff` | resource carrying capacity (`cc_pp`) | | `r_pp`, `n` | resource replenishment rate (`rr_pp`) | +| `kappa`, `lambda` | *also* the calculated `gamma` (and, for `lambda`, `q`) and hence the search volume | + +That last row is easy to miss: the resource power law is the reference spectrum +against which the search volume is calibrated, so a resource change reaches the +species too. See ["`f0` and `fc` are calibration targets"](#f0-and-fc-are-calibration-targets-not-model-outputs) +above. The size-resolved resource arrays themselves can also be set directly; that is a level-2 change and is covered under diff --git a/inst/skills/run-simulation/SKILL.md b/inst/skills/run-simulation/SKILL.md index 2b04f3283..7682da9ba 100644 --- a/inst/skills/run-simulation/SKILL.md +++ b/inst/skills/run-simulation/SKILL.md @@ -146,7 +146,10 @@ second_order_w(params) # inspect: flux and bin_average The scheme lives in the `MizerParams`, so a `MizerSim` carries it: comparing a run made under one setting with a run made under the other compares two -discretisations as well as two scenarios. Recalibrate after switching it on. +discretisations as well as two scenarios. Recalibrate after switching it on — +`steady()` handles the `van_leer` flux from mizer 3.3 onwards. On earlier +versions it fell into a limit cycle there and never converged, so a model built +with `second_order_w = TRUE` had to be settled under the default flux first. **Isolating a feedback loop.** To switch off the resource → growth feedback (the "phantom jam") while keeping everything else — for example to separate an diff --git a/inst/skills/upgrade-mizer-code/SKILL.md b/inst/skills/upgrade-mizer-code/SKILL.md index 5385861b1..a78fb352c 100644 --- a/inst/skills/upgrade-mizer-code/SKILL.md +++ b/inst/skills/upgrade-mizer-code/SKILL.md @@ -76,6 +76,9 @@ plots) are in the changelog and are not repeated here. | `getStability()` or `getLimitCycleSim()` now warns about the steady state | they linearise at the stored state | `getStability()` checks that it was given a steady state (3.3) | | `summary(params)` has an extra "Steady state" block | new steadiness verdict | `summary()` reports the steady state (3.3) | | `compareParams()` now reports differences it used to miss | relative tolerance for species parameters | `compareParams()` compares small parameters (3.3) | +| `steady()` now converges on a `van_leer` model where it used to report a limit cycle, or never settle | the flux limiter is relaxed between iterations | `steady()` converges under the `van_leer` flux scheme (3.3) | +| A steady state found with `steadyNewton()` moved, in a model whose consumers are satiated | the resource is now solved for alongside the fish | `steadyNewton()` solves for the resource (3.3) | +| `getStability()` eigenvalues, a limit-cycle period or a bifurcation diagram shifted | they inherit the corrected `steadyNewton()` fixed point | `steadyNewton()` solves for the resource (3.3) | | `gamma`, `q` or feeding levels change after setting resource `kappa` or `lambda` | calculated search-volume parameters now follow the resource power law | Resource scalars refresh calculated `gamma` and `q` (3.3) | | A recalculated `gamma` or `f0` is wildly different, in a model whose `search_vol` was set by hand | the frozen array used to block mizer's own unit-gamma calculation | Defaults for `gamma` and `f0` ignore a hand-set search volume (3.3) | | Setting `f0 = 1` now errors even though `gamma` is supplied | every supplied target feeding level is now validated | `f0` is always validated (3.3) | @@ -139,10 +142,11 @@ and are not repeated here. ## Upgrading from mizer 3.2 to 3.3 Most of the changes in this release are corrections. Results move only for -models that had opted in to second-order bin-averaging, that set `min_w` below -the default, that specify sizes as lengths, or that change the resource power -law after constructing the model. The one change to an interface is in the -spectrum plots. +models that had opted in to second-order bin-averaging or to the `van_leer` +flux, that set `min_w` below the default, that specify sizes as lengths, that +change the resource power law after constructing the model, or that were brought +to steady state with `steadyNewton()` while their consumers were satiated. The +one change to an interface is in the spectrum plots. ### `biomass` and `per_log_size` replace `power` @@ -494,6 +498,38 @@ the neighbourhood of a point the model is not sitting at and the verdict on stability is meaningless. Both now warn in that case. Run `steadyNewton()` first, or silence with `options(mizer_info_level = 0)` if you know what you are doing. +### `steady()` converges under the `van_leer` flux scheme + +On a model whose `second_order_w()` selects the `"van_leer"` flux, `steady()` +used to fall into a limit cycle instead of converging: the flux limiter weights +flipped from one cell to the next between iterations, and the iteration chased +itself. The limiter is now relaxed with an exponential moving average, and the +run converges (#522). + +Code that worked around this — a `steady()` call wrapped in `try()`, a hand-set +`t_max`, a fall-back to the default upwind flux, or a `steadyNewton()` +substituted for `steady()` — is no longer needed. The steady state it now +reaches is the one the `van_leer` discretisation actually has, so it differs +from the upwind steady state the workaround was settling on; recalibrate rather +than treat the difference as a regression. + +### `steadyNewton()` solves for the resource + +`steadyNewton()`'s analytic substitution for the semichemostat resource assumed +that consumer feeding levels were fixed while the resource adjusted, which is not +self-consistent once consumers are satiated: the resource density and the feeding +level it produces determine each other. The resource is now carried among the +solver's unknowns, so the two are updated together (#521). + +The fixed point this converges on is the correct one, so **steady states found +with `steadyNewton()` on a model with satiated consumers move**, and anything +downstream of them — `getStability()`'s spectral radius, `getLimitCycleSim()`'s +period, a `plotBifurcation()` diagram — moves with them. Models whose consumers +are far from satiation are unaffected. `getStability()`'s quasi-static +approximation gained a fixed iteration for the same reason, which also makes its +numerical Jacobian smoother; small changes in the reported eigenvalues are +expected. + ### `compareParams()` compares small parameters properly `compareParams()` now uses a relative tolerance for species parameters, so diff --git a/vignettes/cheatsheet-analysis-and-plotting.Rmd b/vignettes/cheatsheet-analysis-and-plotting.Rmd index 380376929..afa94b941 100644 --- a/vignettes/cheatsheet-analysis-and-plotting.Rmd +++ b/vignettes/cheatsheet-analysis-and-plotting.Rmd @@ -54,10 +54,10 @@ These extract raw arrays from a `MizerSim` object. | [`finalN(sim)`](../reference/finalN.html) | species abundance at last time | species × size | | [`finalNResource(sim)`](../reference/finalN.html) | resource abundance at last time | size | | [`getEffort(sim)`](../reference/getEffort.html) | fishing effort | time × gear | -| [`getTimes(sim)`](../reference/getTimes.html) | saved time steps | vector | +| [`getTimes(sim)`](../reference/getTimes.html) | saved time steps | time | ```{r eval=FALSE} -N(sim)[, "Cod", ] # time × size for Cod +N(sim)[, , 1] # time × species in smallest size class N(sim)["2010", "Cod", ] # size vector for Cod in year 2010 finalN(sim)["Cod", ] # size vector for Cod at the final time step ``` @@ -66,12 +66,14 @@ finalN(sim)["Cod", ] # size vector for Cod at the final time step ## Summary functions -These compute derived quantities from abundances. All accept `MizerSim` or -`MizerParams`. See `?summary_functions` for the full list. +These functions compute derived quantities from abundances. All accept `MizerSim` or +`MizerParams`. +The result is a classed array that can be plotted directly with [`plot()`](../reference/plot.html) — see +below. | Function | Returns | Dimensions | |---|---|---| -| `getBiomass(sim, min_w, max_w)` | total biomass | time × species | +| [`getBiomass(sim, min_w, max_w)`](../reference/getBiomass.html) | total biomass | time × species | | [`getSSB(sim)`](../reference/getSSB.html) | spawning stock biomass | time × species | | [`getN(sim, min_w, max_w)`](../reference/getN.html) | total abundance | time × species | | [`getYield(sim)`](../reference/getYield.html) | total yield across gears | time × species | @@ -93,9 +95,6 @@ getBiomass(sim, min_w = 10, max_w = 1e4) # biomass of 10g–10kg fish getYield(sim)["2010", ] # yield in year 2010 ``` -The result is a classed array that can be plotted directly with [`plot()`](../reference/plot.html) — see -below. - --- ## Indicator functions @@ -190,7 +189,7 @@ without a dedicated plot function or custom ggplot code. | Class | Typical source | `plot()` shows | |---|---|---| -| `ArrayTimeBySpecies` | `getBiomass(sim)`, `getSSB(sim)`, `getYield(sim)`, `getN(sim)` | value vs time, one line per species | +| [`ArrayTimeBySpecies`](../reference/ArrayTimeBySpecies.html) | `getBiomass(sim)`, `getSSB(sim)`, `getYield(sim)`, `getN(sim)` | value vs time, one line per species | | [`ArraySpeciesBySize`](../reference/ArraySpeciesBySize.html) | `getFeedingLevel(params)`, `getPredMort(params)`, `getEncounter(params)` | value vs size, one line per species | | [`ArrayTimeBySpeciesBySize`](../reference/ArrayTimeBySpeciesBySize.html) | `getFMort(sim)`, `getPredMort(sim)` | one time slice vs size (set with `time`) | | [`ArrayResourceBySize`](../reference/ArrayResourceBySize.html) | `NResource(params)`, [`getResourceMort(params)`](../reference/getResourceMort.html), [`resource_rate(params)`](../reference/setResource.html), [`resource_capacity(params)`](../reference/setResource.html) | resource quantity vs size | @@ -207,9 +206,9 @@ including the resource classes: | Function | What it does | |---|---| -| `addPlot()` | adds a compatible array as extra lines on an existing plot | -| `plot2()` | compares two compatible arrays (colour = species, linetype = which object) | -| `plotRelative()` | shows the relative difference `2 (y - x) / (x + y)` between two compatible arrays | +| [`addPlot()`](../reference/addPlot.html) | adds a compatible array as extra lines on an existing plot | +| [`plot2()`](../reference/plot2.html) | compares two compatible arrays (colour = species, linetype = which object) | +| [`plotRelative()`](../reference/plotRelative.html) | shows the relative difference `2 (y - x) / (x + y)` between two compatible arrays | | [`plotHover()`](../reference/plotHover.html) | turns any of these ggplots into a hover-enabled plotly plot | ```{r eval=FALSE} @@ -241,6 +240,7 @@ dedicated `plot…()` functions below — share these optional arguments: | `highlight` | character vector — draw named species with thicker lines | | `total` | logical — add a line for the community total | | `log_x`, `log_y` | logical — log-scale the x or y axis | +| `size_axis` | `"w"` (default) or `"l"` — plot against weight or against length | `tlim` replaces the deprecated `start_time`/`end_time`, and `log_x`/`log_y` replace the older single `log`. @@ -251,12 +251,42 @@ the underlying numbers — for example the size range that a biomass is summed over — pass `min_w`/`max_w` (or `min_l`/`max_l`) to the `get…()` function instead, e.g. [`plotBiomass(sim, min_w = 10)`](../reference/plotBiomass.html). +`size_axis = "l"` converts the axis with the length–weight parameters `a` and +`b`, so it is unavailable for the resource, which has no species to take them +from. For a *density* it converts the y-axis too, via the appropriate Jacobian — +see the next section. + +### Which density a spectrum plot shows + +[`plotSpectra()`](../reference/plotSpectra.html), [`plotSpectra2()`](../reference/plotSpectra2.html), [`plotCDF()`](../reference/plotCDF.html), [`plotCDF2()`](../reference/plotCDF2.html) and [`animate()`](../reference/animate.html) +describe the plotted quantity with two independent logical arguments, each of +which contributes one factor of the weight: + +| | `per_log_size = FALSE` | `per_log_size = TRUE` | +|---|---|---| +| `biomass = FALSE` | number density | number density per log size | +| `biomass = TRUE` | biomass density | biomass density per log size | + +The older single `power` argument is the sum of the two (0, 1, 1, 2 across that +table) and is still accepted, but it cannot tell the two `power = 1` cells +apart — it is read as the biomass density with respect to weight, which is what +picks the y-axis label and the length-axis Jacobian. Supplying `power` together +with a flag that contradicts it is an error, so express the choice with the +flags. `plotCDF()` accepts only `per_log_size = FALSE`: a cumulative total does +not depend on the density it was accumulated from. + +**`log_x` does not change the y-axis.** Showing weight on a logarithmic axis is +a display choice; converting a density per unit weight into a density per +logarithmic weight interval is `per_log_size`. Conflating the two is the usual +reason a spectrum looks like it has the wrong slope. + Which arguments apply depends on the array's shape: - `plot()` accepts `species`, `tlim`, `total`, `background`, `highlight`, `log_x`, `log_y`, `ylim`. - `plot()` accepts `species`, `highlight`, `log_x`, `log_y`, - `wlim`, `ylim`, `all.sizes`. + `wlim`, `llim`, `ylim`, `size_axis`, `all.sizes`. `size_axis` and `llim` + belong to this shape only — a plot against time has no size axis to convert. --- @@ -272,7 +302,7 @@ instead. See `?plotting_functions`. | Function | How it relates to plotting the array directly | |---|---| -| `plotBiomass(sim)` | same as `plot(getBiomass(sim))` | +| [`plotBiomass(sim)`](../reference/plotBiomass.html) | same as `plot(getBiomass(sim))` | | [`plotYield(sim)`](../reference/plotYield.html) | same as `plot(getYield(sim))` | | [`plotYieldGear(sim)`](../reference/plotYieldGear.html) | like `plotYield()` but keeps the gear dimension, one panel per gear | @@ -284,13 +314,15 @@ instead. See `?plotting_functions`. | [`plotFeedingLevel(sim)`](../reference/plotFeedingLevel.html) | same as `plot(getFeedingLevel(sim))` | | [`plotPredMort(sim)`](../reference/plotPredMort.html) | same as `plot(getPredMort(sim))` | | [`plotFMort(sim)`](../reference/plotFMort.html) | same as `plot(getFMort(sim))` | -| [`plotSpectra(sim)`](../reference/plotSpectra.html) | abundance/biomass spectra: additionally overlays the resource spectrum and background species, and `biomass`/`per_log_size` choose the plotted density | +| [`plotSpectra(sim)`](../reference/plotSpectra.html) | abundance/biomass spectra: additionally overlays the resource spectrum and background species, and `biomass`/`per_log_size` choose the plotted density (see [above](#which-density-a-spectrum-plot-shows)) | | [`plotCDF(sim)`](../reference/plotCDF.html) | cumulative version of the spectrum (`normalise` for proportion vs total) | | [`plotGrowthCurves(sim)`](../reference/plotGrowthCurves.html) | a distinct plot: size at age rather than a size spectrum | | [`plotDiet(params)`](../reference/plotDiet.html) | a distinct plot: stacked diet composition by prey | -**Calibration:** [`plotBiomassObservedVsModel(params)`](../reference/plotBiomassObservedVsModel.html), -[`plotYieldObservedVsModel(params)`](../reference/plotYieldObservedVsModel.html). +**Calibration:** [`plotBiomassObservedVsModel(params)`](../reference/plotBiomassObservedVsModel.html) and +[`plotYieldObservedVsModel(params)`](../reference/plotYieldObservedVsModel.html); the latter takes a `gear` argument that +restricts both the modelled and the observed catch to the named gears. See the +[calibration cheatsheet](cheatsheet-calibration.html). ```{r eval=FALSE} plotBiomass(sim, species = c("Cod", "Herring"), total = TRUE) @@ -310,8 +342,8 @@ panels for a model's steady state (without the biomass-through-time panel). biomass over size — steadier than a density spectrum for eyeballing where biomass sits. `biomass = TRUE` (default) accumulates biomass, `biomass = FALSE` accumulates numbers; `normalise = FALSE` plots the cumulative total rather than -the proportion. Unlike in `plotSpectra()`, `per_log_size` is not accepted: the -integral does not depend on it. +the proportion. Unlike in `plotSpectra()`, only `per_log_size = FALSE` is +accepted: the integral does not depend on it. ```{r eval=FALSE} plotCDF(NS_params, species = c("Cod", "Herring")) @@ -341,7 +373,7 @@ plotCDF2(sim, sim2, "Unfished", "Fished") ## Animating spectra through time -[`animate()`](../reference/animate.html) plays a spectrum or rate array through the course of a simulation +`animate()` plays a spectrum or rate array through the course of a simulation ([`animateSpectra()`](../reference/animate.html) is a retained alias). ```{r eval=FALSE} @@ -471,6 +503,12 @@ plotGrowthCurves(sim) # size vs age plotDiet(params, species = "Cod") # diet composition vs size plotCDF(sim) # cumulative biomass/abundance over size +# ── Choosing what a spectrum plot shows ─────────────────────────────────────── +plotSpectra(sim, biomass = TRUE) # biomass rather than number +plotSpectra(sim, per_log_size = TRUE) # density per log size +plotSpectra(sim, size_axis = "l") # x axis in length, not weight +plotSpectra(sim, log_x = TRUE) # display only: does NOT change the y density + # ── Plot any array directly, plus combine / compare tools ───────────────────── plot(getResourceMort(params)) # any get*() array plots directly p <- plot(getBiomass(sim), species = "Cod") diff --git a/vignettes/cheatsheet-calibration.Rmd b/vignettes/cheatsheet-calibration.Rmd index 4285aa0db..6c80ed051 100644 --- a/vignettes/cheatsheet-calibration.Rmd +++ b/vignettes/cheatsheet-calibration.Rmd @@ -98,6 +98,28 @@ plot(getSteadyResidual(params)) # which species, and at which sizes them, re-running `steady()` between, until both are satisfied — usually a few passes. +**If your observations are numbers rather than weights**, use [`calibrateNumber()`](../reference/calibrateNumber.html) +and [`matchNumbers()`](../reference/matchNumbers.html) in place of the two biomass functions. They are the same +functions with the factor of the weight taken out of the size integral, and they +read `number_observed` and `number_cutoff` instead of `biomass_observed` and +`biomass_cutoff`. Mixing the two — matching some species on biomass and others +on numbers — works, because each function ignores the species for which its own +observation column is `NA`. + +```{r eval=FALSE} +species_params(params)$number_observed <- c(Cod = 1e6, Herring = 4e8, ...) +params <- calibrateNumber(params) +params <- matchNumbers(params) +params <- steady(params) +``` + +Whichever pair you use, a `_cutoff` value is the **smallest size the +observation counts**, in grams: a survey that misses fish under 10 g is +`biomass_cutoff = 10`, and the model is then integrated over the same range +rather than over the whole spectrum. Leave it out and the whole size range is +counted, which is the usual reason a model looks like it over-predicts a +species by a wide margin. + **Yields** are not a calibration target in mizer itself: put the observed annual yield of each gear-species pair into the `yield_observed` column of [`gear_params()`](../reference/gear_params.html) and compare it with the model using @@ -190,6 +212,8 @@ params <- steadyNewton(params) # direct solve (experimental) # ── Calibrate to data (re-run steady() after each) ──────────────────────────── params <- calibrateBiomass(params) # total biomass → kappa params <- matchBiomasses(params) # per-species biomass +params <- calibrateNumber(params) # same, for `number_observed` instead +params <- matchNumbers(params) # of `biomass_observed` params <- matchGrowth(params) # growth → h, gamma, ks, k params <- steady(params) # re-converge @@ -198,6 +222,8 @@ reproduction_level(params) <- 0.25 reproduction_level(params) # what the model is currently tuned to # ── Verify ──────────────────────────────────────────────────────────────────── +summary(params) # includes the biomass-drift verdict +plot(getSteadyResidual(params)) # which species and sizes are still moving plotSpectra(params) plotGrowthCurves(params) plotBiomassObservedVsModel(params) diff --git a/vignettes/cheatsheet-changing-parameters.Rmd b/vignettes/cheatsheet-changing-parameters.Rmd index 865ec3a12..2fd59ceaa 100644 --- a/vignettes/cheatsheet-changing-parameters.Rmd +++ b/vignettes/cheatsheet-changing-parameters.Rmd @@ -65,7 +65,7 @@ track of which values you **gave** and which it **calculated**. |---|---| | [`given_species_params(params)`](../reference/species_params.html) | only the parameters you supplied explicitly | | [`calculated_species_params(params)`](../reference/species_params.html) | the parameters mizer derived or defaulted | -| `species_params(params)` | everything (given, with calculated filling the gaps) | +| [`species_params(params)`](../reference/species_params.html) | everything (given, with calculated filling the gaps) | **Rule (mizer ≥ 3.2): change species parameters with `species_params(params) <-`.** It detects what you changed, records it as *given* (so defaults can no longer @@ -83,6 +83,24 @@ have already given, or it feeds a rate array you set by hand, or it is a gear parameter that mizer reads from `gear_params()`. `species_params(params) <-` stays quiet about all three, which is what makes it the better one for scripts. +**Turning the commentary up or down.** Mizer reports the choices it makes — +defaults it filled in, inputs it adjusted, instructions it could not carry out — +at a level set by `info_level`. Most `set…()` and `new…()` functions take it as +an argument; for the ones that do not, including `species_params(params) <-` and +the rate setters, set the option instead: + +```{r eval=FALSE} +options(mizer_info_level = 1) # only what matters: warnings and adjustments +options(mizer_info_level = 0) # complete silence +params <- setExtMort(params, info_level = 0) # just this one call +``` + +The default is 3 — every default mizer filled in. Level 1 keeps the reports that +tell you something went differently from how you asked, such as the "has not +taken effect" warnings above. **Level 0 is silence, not "warnings only"**: it +drops those too, so reach for 1 rather than 0 while you are still finding out +what a model does. + When you edit a whole table rather than a single column, read it back from the same accessor you assign to: @@ -131,6 +149,20 @@ the relevant setter automatically: | `beta`, `sigma`, `pred_kernel_type` | predation kernel | [`setPredKernel()`](../reference/setPredKernel.html) | | `w_mat`, `w_mat25`, `w_repro_max`, `m` | reproduction allocation | [`setReproduction()`](../reference/setReproduction.html) | +`pred_kernel_type` chooses the *shape* of the kernel, and each shape reads its +own parameter columns. Changing it therefore also changes which columns matter: + +| `pred_kernel_type` | Parameter columns | +|---|---| +| `"lognormal"` (default) | `beta`, `sigma` | +| `"truncated_lognormal"` | `beta`, `sigma` (cut off at `beta * exp(3 * sigma)`) | +| `"box"` | `ppmr_min`, `ppmr_max` | +| `"power_law"` | `kernel_exp`, `kernel_l_l`, `kernel_u_l`, `kernel_l_r`, `kernel_u_r` | +| `"gaussian_mixture"` | `kernel_p`, `kernel_mean`, `kernel_sd` (multimodal preferences) | + +Any function `_pred_kernel(ppmr, ...)` you define yourself can be named in +`pred_kernel_type` too; its arguments become the required columns. + Other species parameters are used **directly** and build no array (changing them just changes the model): `alpha` (assimilation), `w_min` (egg size), `erepro` and `R_max` (reproduction), `interaction_resource`, and the length–weight @@ -206,19 +238,33 @@ forced to 1. So `f0` is the feeding level a species *would* have in that world. - Setting `interaction_resource` to anything other than 1 leaves `gamma` untouched under edition 1, so the reduction falls straight through to the realised feeding level and can starve a species outright. -- The resource scalars that enter the calibration do **not** trigger a - re-derivation. Changing `kappa` or `lambda` with `resource_params(params) <-` - rebuilds the resource arrays but leaves `gamma` and `q` alone, so the - feeding level moves, and the condition `q = n + lambda - 2` that makes the - feeding level size-independent silently breaks. After changing `lambda`, - reset `q` and `gamma` so they are derived afresh: +- The resource scalars that enter the calibration **do** trigger a + re-derivation, but only of the values mizer owns. Since mizer 3.3, changing + `lambda` recalculates every `q` and `gamma` that mizer calculated, and + changing `kappa` recalculates every calculated `gamma`; the condition + `q = n + lambda - 2` that makes the feeding level size-independent is + maintained for you. A value you **gave** is protected, as everywhere else — + so on a model that supplies `gamma` in its species parameters (`NS_params` + does) nothing moves, and the realised feeding level shifts with the new + resource. To let mizer re-derive a given value, clear it first: ```{r eval=FALSE} -resource_params(params)$lambda <- 2.2 -given_species_params(params)$q <- NA # let mizer re-derive q ... -given_species_params(params)$gamma <- NA # ... and gamma with it +resource_params(params)$lambda <- 2.2 # calculated q and gamma follow +given_species_params(params)$gamma <- NA # hand a *given* gamma back to mizer ``` +Conversely, to hold calculated values against a resource change, record them as +given before you make it: + +```{r eval=FALSE} +given_species_params(params)$q <- species_params(params)$q +resource_params(params)$lambda <- 2.2 # q now stays put +``` + +On mizer **< 3.3** neither refresh happened: the resource arrays were rebuilt but +`gamma` and `q` were left at the values for the old resource, and had to be set +to `NA` by hand. + ### Whether a value is given determines whether it is recalculated For derived parameters, `given_species_params()` distinguishes fixed input from @@ -307,6 +353,12 @@ resource_params(params)$r_pp <- 10 # rebuilds the replenishment rate (rr |---|---| | `kappa`, `lambda`, `w_pp_cutoff` | resource carrying capacity (`cc_pp`) | | `r_pp`, `n` | resource replenishment rate (`rr_pp`) | +| `kappa`, `lambda` | *also* the calculated `gamma` (and, for `lambda`, `q`) and hence the search volume | + +That last row is easy to miss: the resource power law is the reference spectrum +against which the search volume is calibrated, so a resource change reaches the +species too. See ["`f0` and `fc` are calibration targets"](#f0-and-fc-are-calibration-targets-not-model-outputs) +above. The size-resolved resource arrays themselves can also be set directly; that is a level-2 change and is covered under @@ -429,7 +481,7 @@ no longer touches them. | [`resource_capacity(params) <-`](../reference/setResource.html) | the carrying capacity over size | | [`resource_rate(params) <-`](../reference/setResource.html) | the replenishment (regeneration) rate over size | | [`resource_level(params) <-`](../reference/setResource.html) | the resource level (fraction of capacity) | -| `setResource(params, …)` | any of the above, plus `lambda`, `n`, `w_pp_cutoff`, [`resource_dynamics`](../reference/setResource.html) | +| [`setResource(params, …)`](../reference/setResource.html) | any of the above, plus `lambda`, `n`, `w_pp_cutoff`, [`resource_dynamics`](../reference/setResource.html) | ```{r eval=FALSE} resource_capacity(params) <- my_capacity # array over size; now frozen diff --git a/vignettes/cheatsheet-fishing.Rmd b/vignettes/cheatsheet-fishing.Rmd index 44caf5727..001bfa33d 100644 --- a/vignettes/cheatsheet-fishing.Rmd +++ b/vignettes/cheatsheet-fishing.Rmd @@ -103,7 +103,7 @@ in `[0, 1]` at each size. Its other arguments must appear as columns in | `sel_func` | Parameter column(s) | Shape | |---|---|---| -| `knife_edge` (default) | `knife_edge_size` | step from 0 to 1 at that **weight** (default `w_mat`) | +| [`knife_edge`](../reference/knife_edge.html) (default) | `knife_edge_size` | step from 0 to 1 at that **weight** (default `w_mat`) | | [`knife_edge_length`](../reference/knife_edge_length.html) | `knife_edge_length` | step from 0 to 1 at that **length** | | [`sigmoid_length`](../reference/sigmoid_length.html) | `l50`, `l25` | smooth; lengths (cm) at 50% and 25% selection | | [`double_sigmoid_length`](../reference/double_sigmoid_length.html) | `l50`, `l25`, `l50_right`, `l25_right` | dome-shaped (selects a length band) | @@ -180,7 +180,7 @@ without an explicit `effort` argument. | Function | Use | |---|---| | [`initial_effort(params)`](../reference/initial_effort.html) | read baseline effort (a named vector) | -| `initial_effort(params) <-` | set baseline effort | +| [`initial_effort(params) <-`](../reference/initial_effort.html) | set baseline effort | | [`getEffort(sim)`](../reference/getEffort.html) | effort actually used over time in a simulation | ```{r eval=FALSE} diff --git a/vignettes/cheatsheet-model-setup.Rmd b/vignettes/cheatsheet-model-setup.Rmd index 767cdfc46..5f19c7578 100644 --- a/vignettes/cheatsheet-model-setup.Rmd +++ b/vignettes/cheatsheet-model-setup.Rmd @@ -49,17 +49,25 @@ are truly required: | Column | Meaning | |---|---| | `species` | the species name | -| `w_inf` | von Bertalanffy asymptotic weight (g) — the required maximum-size parameter | +| `w_inf` | von Bertalanffy asymptotic weight (g) — the maximum-size parameter | Mizer derives defaults for `w_max` (the computational grid boundary, default `1.5 * w_inf`), `w_repro_max` and `w_mat` from `w_inf`. Everything else has a -sensible default or is calculated. Commonly supplied: +sensible default or is calculated. + +`w_inf` is the parameter to supply, but it need not be the one you *have*: a +table giving only `w_max`, or only lengths (`l_inf` or `l_max`, with the +length–weight parameters `a` and `b`), is accepted, and mizer fills `w_inf` in +from it and says so. Note that when `w_inf` is taken from `w_max` the two are +equal, so the `1.5 *` headroom above the asymptotic size is not there. + +Commonly supplied: | Column | Meaning | |---|---| | `w_mat` | Maturity weight (g) | -| `beta` | Preferred predator/prey mass ratio (default ~100) | -| `sigma` | Width of the lognormal predation kernel (default ~1.3) | +| `beta` | Preferred predator/prey mass ratio (default 30) | +| `sigma` | Width of the lognormal predation kernel (default 2) | | `k_vb` | von Bertalanffy K — used to derive `h` (and then `gamma`) if `h`/`gamma` absent | | `h`, `gamma` | Max intake coefficient and search-volume coefficient (alternative to `k_vb`) | | `alpha` | Assimilation efficiency (default 0.6) | diff --git a/vignettes/cheatsheet-running-simulations.Rmd b/vignettes/cheatsheet-running-simulations.Rmd index 7f898eaea..044a9d4dd 100644 --- a/vignettes/cheatsheet-running-simulations.Rmd +++ b/vignettes/cheatsheet-running-simulations.Rmd @@ -164,7 +164,10 @@ second_order_w(params) # inspect: flux and bin_average The scheme lives in the `MizerParams`, so a `MizerSim` carries it: comparing a run made under one setting with a run made under the other compares two -discretisations as well as two scenarios. Recalibrate after switching it on. +discretisations as well as two scenarios. Recalibrate after switching it on — +`steady()` handles the `van_leer` flux from mizer 3.3 onwards. On earlier +versions it fell into a limit cycle there and never converged, so a model built +with `second_order_w = TRUE` had to be settled under the default flux first. **Isolating a feedback loop.** To switch off the resource → growth feedback (the "phantom jam") while keeping everything else — for example to separate an diff --git a/vignettes/cheatsheet-stability.Rmd b/vignettes/cheatsheet-stability.Rmd index ef72d354f..ee526cea0 100644 --- a/vignettes/cheatsheet-stability.Rmd +++ b/vignettes/cheatsheet-stability.Rmd @@ -23,20 +23,23 @@ function, follow the links. Tools for asking whether a mizer steady state is dynamically **stable** — and, when it is not, characterising the **limit cycle** that replaces it. These are -**experimental**: their interface may still change. All treat the resource as a -quasi-static fast variable by default (valid for the standard semichemostat -resource dynamics). +**experimental**: their interface may still change. All assume the standard +semichemostat resource dynamics. [`steadyNewton()`](../reference/steadyNewton.html) solves for the resource +alongside the fish, so the resource density and the feeding levels it implies +are self-consistent even where consumers are satiated; [`getStability()`](../reference/getStability.html) instead +treats the resource as a quasi-static fast variable, and +`include_resource = TRUE` turns that approximation off. Distinct from calibration: the [calibration cheatsheet](cheatsheet-calibration.html) gets you *onto* a fixed point; what follows analyses the *dynamics around* it. The natural entry point -is [`steadyNewton()`](../reference/steadyNewton.html), which converges even when the fixed point is dynamically +is `steadyNewton()`, which converges even when the fixed point is dynamically unstable, unlike [`steady()`](../reference/steady.html). --- ## Is the steady state stable? — `getStability()` -[`getStability(params)`](../reference/getStability.html) linearises the one-step-ahead map at the fixed point and +`getStability(params)` linearises the one-step-ahead map at the fixed point and returns its leading eigenvalues. The result reports whether the state is stable or unstable, the **spectral radius** (modulus of the leading eigenvalue; > 1 means unstable), and — when the system is near a Hopf bifurcation — the **period** of @@ -58,6 +61,13 @@ stab # stable/unstable, spectral radius, cyc - `effort` / `reproduction` set the fishing effort and reproduction handling used when forming the map. +**Both `getStability()` and [`getLimitCycleSim()`](../reference/getLimitCycleSim.html) linearise at the state stored in +the object**, so a model that is not on a fixed point gets eigenvalues for the +neighbourhood of a point it is not sitting at. Both now warn when handed one; +the fix is to run `steadyNewton()` (or `steady()`) first, not to ignore the +warning. [`plot(getSteadyResidual(params))`](../reference/plot.html) shows how far off it is — see the +[calibration cheatsheet](cheatsheet-calibration.html). + `steady()` and [`projectToSteady()`](../reference/projectToSteady.html) attach a related `"convergence"` attribute recording whether the run settled on a **steady state, a limit cycle, or neither**, together with the cycle period and relative amplitude when a cycle is @@ -70,7 +80,7 @@ tolerance `tol`. ## Visualising the limit cycle — `getLimitCycleSim()` -[`getLimitCycleSim(params)`](../reference/getLimitCycleSim.html) takes the output of `steadyNewton()` and builds a +`getLimitCycleSim(params)` takes the output of `steadyNewton()` and builds a [`MizerSim`](../reference/MizerSim.html) covering **one period** of the limit cycle in the linear approximation, \[ N(t) = N^* + A\,\mathrm{Re}\!\left[e^{i\theta t}\,\mathbf v\right], \] diff --git a/vignettes/upgrading.Rmd b/vignettes/upgrading.Rmd index 7477116f8..8b26555ca 100644 --- a/vignettes/upgrading.Rmd +++ b/vignettes/upgrading.Rmd @@ -33,10 +33,11 @@ and are not repeated here. ## Upgrading from mizer 3.2 to 3.3 Most of the changes in this release are corrections. Results move only for -models that had opted in to second-order bin-averaging, that set `min_w` below -the default, that specify sizes as lengths, or that change the resource power -law after constructing the model. The one change to an interface is in the -spectrum plots. +models that had opted in to second-order bin-averaging or to the `van_leer` +flux, that set `min_w` below the default, that specify sizes as lengths, that +change the resource power law after constructing the model, or that were brought +to steady state with [`steadyNewton()`](../reference/steadyNewton.html) while their consumers were satiated. The +one change to an interface is in the spectrum plots. ### `biomass` and `per_log_size` replace `power` @@ -385,9 +386,41 @@ are still moving. It is a message, not a warning, because convergence at the Both [`getStability()`](../reference/getStability.html) and [`getLimitCycleSim()`](../reference/getLimitCycleSim.html) linearise the dynamics *at* [`initialN(params)`](../reference/initialN-set.html). If that state is not a fixed point, the eigenvalues describe the neighbourhood of a point the model is not sitting at and the verdict on -stability is meaningless. Both now warn in that case. Run [`steadyNewton()`](../reference/steadyNewton.html) first, +stability is meaningless. Both now warn in that case. Run `steadyNewton()` first, or silence with `options(mizer_info_level = 0)` if you know what you are doing. +### `steady()` converges under the `van_leer` flux scheme + +On a model whose `second_order_w()` selects the `"van_leer"` flux, `steady()` +used to fall into a limit cycle instead of converging: the flux limiter weights +flipped from one cell to the next between iterations, and the iteration chased +itself. The limiter is now relaxed with an exponential moving average, and the +run converges (#522). + +Code that worked around this — a `steady()` call wrapped in `try()`, a hand-set +`t_max`, a fall-back to the default upwind flux, or a `steadyNewton()` +substituted for `steady()` — is no longer needed. The steady state it now +reaches is the one the `van_leer` discretisation actually has, so it differs +from the upwind steady state the workaround was settling on; recalibrate rather +than treat the difference as a regression. + +### `steadyNewton()` solves for the resource + +`steadyNewton()`'s analytic substitution for the semichemostat resource assumed +that consumer feeding levels were fixed while the resource adjusted, which is not +self-consistent once consumers are satiated: the resource density and the feeding +level it produces determine each other. The resource is now carried among the +solver's unknowns, so the two are updated together (#521). + +The fixed point this converges on is the correct one, so **steady states found +with `steadyNewton()` on a model with satiated consumers move**, and anything +downstream of them — `getStability()`'s spectral radius, `getLimitCycleSim()`'s +period, a [`plotBifurcation()`](../reference/plotBifurcation.html) diagram — moves with them. Models whose consumers +are far from satiation are unaffected. `getStability()`'s quasi-static +approximation gained a fixed iteration for the same reason, which also makes its +numerical Jacobian smoother; small changes in the reported eigenvalues are +expected. + ### `compareParams()` compares small parameters properly [`compareParams()`](../reference/compareParams.html) now uses a relative tolerance for species parameters, so From a2cd297f6df5341b68032c10866e3520c1ebcdb0 Mon Sep 17 00:00:00 2001 From: Gustav Delius Date: Sat, 15 Aug 2026 22:00:35 +0100 Subject: [PATCH 2/3] Let mizer arrays say what kind of value they hold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mizer arrays now carry a `type` attribute — `"value"` (the default), `"density"` or `"proportion"` — replacing the `density_wrt` attribute added earlier in this development cycle. `density_wrt` could name any of four density measures, but three of them were dead as an array attribute: every mizer array is indexed by the model's weight grid, so a stored spectrum is per-gram by construction, and per-log-size quantities are computed on the fly inside plot data frames rather than stored. Carrying four values where two occur repeated the mistake the attribute was introduced to fix. Meanwhile a distinction that does earn its keep was handled ad hoc: `plotFeedingLevel()` hardcoded a [0, 1] clamp while `plot(getFeedingLevel(params))` — the array method for the same numbers — had none, and five other arrays are proportions that no plot knew about. - A `"density"` is multiplied by the appropriate Jacobian on a length axis and has its units restated from `1/g` to `1/cm`, as before. The density-measure vocabulary stays as internal plumbing, because `plotSpectra(per_log_size = TRUE, size_axis = "l")` still needs the `log_w` measure. - A `"proportion"` is plotted on a linear y axis covering the whole of [0, 1]. The range is only ever widened to include the data, never narrowed to that interval: the critical feeding level and the resource level can both legitimately exceed 1. - Arrays that declare no type still fall back to the old test on `value_name` and `units`, so existing code and saved objects are unaffected. Fixes `plotFeedingLevel(include_critical = TRUE)` drawing a critical feeding level above 1 off the top of the plot. Co-Authored-By: Claude Opus 5 --- NEWS.md | 32 ++ R/ArrayResourceBySize-class.R | 21 +- R/ArraySpeciesBySize-class.R | 145 ++++++-- R/ArrayTimeBySpecies-class.R | 10 +- R/ArrayTimeBySpeciesBySize-class.R | 31 +- R/MizerSim-class.R | 10 +- R/animateSpectra.R | 18 +- R/get_initial_n.R | 4 +- R/plots.R | 321 +++++++++++++++--- R/rate_functions.R | 13 +- R/setInitialValues.R | 8 +- R/setReproduction.R | 6 +- R/setResource.R | 4 +- R/sim_rates.R | 11 +- inst/skills/analyse-and-plot/SKILL.md | 261 ++++++++------ .../analyse-and-plot/quick-reference.md | 44 +-- inst/skills/upgrade-mizer-code/SKILL.md | 58 ++++ man/ArrayResourceBySize.Rd | 11 +- man/ArraySpeciesBySize.Rd | 14 +- man/ArrayTimeByResourceBySize.Rd | 11 +- man/ArrayTimeBySpecies.Rd | 11 +- man/ArrayTimeBySpeciesBySize.Rd | 5 + man/array_density_wrt.Rd | 22 ++ man/array_log_y.Rd | 26 ++ man/array_type.Rd | 18 + man/array_types.Rd | 33 ++ man/array_ylim.Rd | 25 ++ man/convert_density_units.Rd | 22 ++ man/convert_plot_density_axis.Rd | 49 +++ man/convert_plot_spectrum_axis.Rd | 8 +- man/density_measure_jacobian.Rd | 22 ++ man/density_measure_weight.Rd | 25 ++ man/density_measures.Rd | 33 ++ man/density_size_unit.Rd | 19 ++ man/density_target_measure.Rd | 26 ++ man/getMeanWeight.Rd | 8 +- man/get_species_size_rate_from_sim.Rd | 4 + man/plotComparisonDataFrame.Rd | 12 +- man/proportion_ylim.Rd | 31 ++ man/resolve_array_type.Rd | 27 ++ man/sim_size_rate.Rd | 1 + man/spectrum_density_wrt.Rd | 19 ++ man/validate_array_type.Rd | 18 + man/validate_density_wrt.Rd | 20 ++ tests/testthat/_snaps/diffusion.md | 15 +- tests/testthat/_snaps/rate_functions.md | 80 +++-- .../testthat/test-ArraySpeciesBySize-class.R | 135 ++++++++ .../test-ArrayTimeBySpeciesBySize-class.R | 29 ++ tests/testthat/test-plots.R | 186 ++++++++++ tests/testthat/test-rate_functions.R | 17 +- .../cheatsheet-analysis-and-plotting.Rmd | 316 ++++++++++------- vignettes/upgrading.Rmd | 61 +++- 52 files changed, 1931 insertions(+), 425 deletions(-) create mode 100644 man/array_density_wrt.Rd create mode 100644 man/array_log_y.Rd create mode 100644 man/array_type.Rd create mode 100644 man/array_types.Rd create mode 100644 man/array_ylim.Rd create mode 100644 man/convert_density_units.Rd create mode 100644 man/convert_plot_density_axis.Rd create mode 100644 man/density_measure_jacobian.Rd create mode 100644 man/density_measure_weight.Rd create mode 100644 man/density_measures.Rd create mode 100644 man/density_size_unit.Rd create mode 100644 man/density_target_measure.Rd create mode 100644 man/proportion_ylim.Rd create mode 100644 man/resolve_array_type.Rd create mode 100644 man/spectrum_density_wrt.Rd create mode 100644 man/validate_array_type.Rd create mode 100644 man/validate_density_wrt.Rd diff --git a/NEWS.md b/NEWS.md index b83182a7c..50ae155eb 100644 --- a/NEWS.md +++ b/NEWS.md @@ -5,6 +5,16 @@ stability of steady states. ## Bug fixes +- `plot(getFluxGradient(params), size_axis = "l")` now converts its values to a + density with respect to length, and labels them `cm^-1/year`. The flux + gradient is a rate of change of a number density, but it was not recognised as + a density, so its values were plotted per gram against a length axis and + labelled `g^-1/year`. + +- `plotFeedingLevel(include_critical = TRUE)` no longer draws a critical feeding + level above 1 off the top of the plot. The y axis was fixed to the interval + from 0 to 1, and now widens when the data need it. + - `steady()` now successfully converges when the advective flux scheme is set to `"van_leer"` (via `second_order_w`). Previously, the time-stepping iteration would fall into a limit cycle because the flux limiter weights flipped wildly across cells. We resolved this by introducing an exponential moving average relaxation to the limiter `chi` (#522). ## New functions @@ -104,6 +114,28 @@ stability of steady states. ## Other improvements +- Mizer arrays now state what kind of quantity they hold. Every array + constructor gains a `type` argument: `"value"` (the default) for a rate or an + amount, `"density"` for an amount per gram of body weight, `"proportion"` for + a fraction. Two things follow from it. + + A `"density"` is multiplied by the appropriate Jacobian when it is plotted + against a length axis (`size_axis = "l"`), and its units are restated from + `1/g` to `1/cm`. This replaces the guess mizer used to make from the array's + name and units, which recognised only densities that happened to be called + "Number density" or to have units "1/g" — and so missed `getFluxGradient()`. + Arrays that declare no type still fall back to that guess, so existing code + and saved objects are unaffected. + + A `"proportion"` — `getFeedingLevel()`, `getCriticalFeedingLevel()`, + `maturity()`, `repro_prop()`, `psi()`, `resource_level()` — is plotted on a + linear y axis showing the whole of the interval from 0 to 1, so the value can + be read against the scale it belongs to. The range is only ever widened to + include the data, never narrowed to that interval: the critical feeding level + and the resource level can both legitimately exceed 1, and their plots show + it. `plot(getFeedingLevel(params))` therefore now shows the same y range that + `plotFeedingLevel()` always has. + - `plotYieldObservedVsModel()` gains a `gear` argument that restricts the comparison to the catch of the selected gears. Both the model yield and the observed yield are then taken from those gears only, so in a model where diff --git a/R/ArrayResourceBySize-class.R b/R/ArrayResourceBySize-class.R index 7d809e786..284a30f3c 100644 --- a/R/ArrayResourceBySize-class.R +++ b/R/ArrayResourceBySize-class.R @@ -26,6 +26,8 @@ #' `is.ArrayResourceBySize()`, any object to test. #' @param value_name A string giving the human-readable name for the value. #' @param units A string giving the units (e.g. "1/year"). +#' @param type The kind of quantity the values are, see [ArraySpeciesBySize()] +#' and [array_types]. #' @param params A `MizerParams` object. Used for the resource colour and the #' size grid in the `plot()` method. #' @@ -41,10 +43,11 @@ #' plot(mort) #' } ArrayResourceBySize <- function(x, value_name = NULL, units = NULL, - params = NULL) { + type = NULL, params = NULL) { if (!is.numeric(x) || !is.null(dim(x))) { stop("`x` must be a numeric vector.") } + type <- resolve_array_type(type, value_name, units) if (!is.null(params) && length(x) == length(params@initial_n_pp) && is.null(names(x))) { names(x) <- names(params@initial_n_pp) @@ -53,6 +56,7 @@ ArrayResourceBySize <- function(x, value_name = NULL, units = NULL, class = c("ArrayResourceBySize", "numeric"), value_name = value_name, units = units, + type = type, params = params ) } @@ -164,6 +168,7 @@ plot.ArrayResourceBySize <- function(x, return_data = FALSE, log_x = TRUE, log_y = TRUE, log = NULL, wlim = c(NA, NA), ylim = c(NA, NA), y_ticks = 6, ...) { + log_y <- array_log_y(x, log_y, log, !missing(log_y)) log_axes <- parsePlotLog(log, log_x = log_x, log_y = log_y) log_x <- log_axes$log_x log_y <- log_axes$log_y @@ -183,6 +188,7 @@ plot.ArrayResourceBySize <- function(x, return_data = FALSE, y_label <- paste0(value_name, " [", units_str, "]") } + ylim <- array_ylim(x, ylim, log_y, plot_dat[[2]]) plotDataFrame(plot_dat, params, xlab = "Weight (g)", ylab = y_label, xtrans = if (log_x) "log10" else "identity", @@ -379,6 +385,7 @@ get_ArrayResourceBySize_w <- function(x) { result <- NextMethod() attr(result, "value_name") <- attr(x, "value_name") attr(result, "units") <- attr(x, "units") + attr(result, "type") <- attr(x, "type") attr(result, "params") <- attr(x, "params") class(result) <- c("ArrayResourceBySize", "numeric") result @@ -416,6 +423,7 @@ unclass_resource <- function(x) { x <- unclass(x) attr(x, "value_name") <- NULL attr(x, "units") <- NULL + attr(x, "type") <- NULL attr(x, "params") <- NULL x } @@ -461,6 +469,8 @@ str.ArrayResourceBySize <- function(object, ...) { #' object to test. #' @param value_name A string giving the human-readable name for the value. #' @param units A string giving the units (e.g. "1/g"). +#' @param type The kind of quantity the values are, see [ArraySpeciesBySize()] +#' and [array_types]. #' @param params A `MizerParams` object. Used for the resource colour and the #' size grid in the `plot()` method. #' @@ -477,14 +487,16 @@ str.ArrayResourceBySize <- function(object, ...) { #' plot(nr) #' } ArrayTimeByResourceBySize <- function(x, value_name = NULL, units = NULL, - params = NULL) { + type = NULL, params = NULL) { if (!is.matrix(x)) { stop("`x` must be a matrix.") } + type <- resolve_array_type(type, value_name, units) structure(x, class = c("ArrayTimeByResourceBySize", "matrix", "array"), value_name = value_name, units = units, + type = type, params = params ) } @@ -611,7 +623,8 @@ ArrayTimeByResourceBySize_slice <- function(x, time = NULL) { vec <- unclass(x)[tidx, ] ArrayResourceBySize(vec, value_name = value_name, - units = units, params = params) + units = units, type = attr(x, "type"), + params = params) } #' @rdname plot2 @@ -780,6 +793,7 @@ as.data.frame.ArrayTimeByResourceBySize <- function(x, row.names = NULL, if (is.matrix(result) && length(dim(result)) == 2) { attr(result, "value_name") <- attr(x, "value_name") attr(result, "units") <- attr(x, "units") + attr(result, "type") <- attr(x, "type") attr(result, "params") <- attr(x, "params") class(result) <- c("ArrayTimeByResourceBySize", "matrix", "array") } else if (is.null(dim(result)) && !is.null(names(result)) && @@ -788,6 +802,7 @@ as.data.frame.ArrayTimeByResourceBySize <- function(x, row.names = NULL, result <- ArrayResourceBySize(result, value_name = attr(x, "value_name"), units = attr(x, "units"), + type = attr(x, "type"), params = attr(x, "params")) } result diff --git a/R/ArraySpeciesBySize-class.R b/R/ArraySpeciesBySize-class.R index 2ecaa478a..14ecc0c27 100644 --- a/R/ArraySpeciesBySize-class.R +++ b/R/ArraySpeciesBySize-class.R @@ -11,17 +11,27 @@ #' `print()`, `summary()`, `plot()`, and `as.data.frame()` methods. #' #' An `ArraySpeciesBySize` object behaves just like a regular matrix for -#' arithmetic operations and subsetting. It carries two lightweight attributes: +#' arithmetic operations and subsetting. It carries a few lightweight +#' attributes: #' \itemize{ #' \item `value_name` – a human-readable name for the value #' (e.g. "Encounter rate"). #' \item `units` – the units of the rate (e.g. "g/year"). +#' \item `type` – the kind of quantity the values are. #' } #' #' @param x A matrix (species x size). For `is.ArraySpeciesBySize()`, any #' object to test. #' @param value_name A string giving the human-readable name for the value. #' @param units A string giving the units (e.g. "g/year", "1/year"). +#' @param type The kind of quantity the values are, see [array_types]: +#' `"value"` (the default) for a rate or an amount, `"density"` for an amount +#' per gram of body weight, `"proportion"` for a fraction. This is what tells +#' `plot()` to multiply a density by the appropriate Jacobian when it is +#' plotted against a length axis (`size_axis = "l"`), and to show a proportion +#' against the whole of the interval from 0 to 1. The default, `NULL`, treats +#' a `value_name` of `"Number density"` or units of `"1/g"` as a density, the +#' way mizer recognised one before this attribute existed. #' @param params A `MizerParams` object. Used for species colours, linetypes, #' and size ranges in the `plot()` method. #' @param representation Either `"point"` (the default) for a quantity sampled @@ -40,12 +50,14 @@ #' summary(enc) #' } ArraySpeciesBySize <- function(x, value_name = NULL, units = NULL, + type = NULL, params = NULL, representation = c("point", "average")) { if (!is.matrix(x)) { stop("`x` must be a matrix.") } representation <- match.arg(representation) + type <- resolve_array_type(type, value_name, units) if (!is.null(params) && identical(dim(x), dim(params@metab))) { dimnames(x) <- dimnames(params@metab) } @@ -53,6 +65,7 @@ ArraySpeciesBySize <- function(x, value_name = NULL, units = NULL, class = c("ArraySpeciesBySize", "matrix", "array"), value_name = value_name, units = units, + type = type, params = params, representation = representation ) @@ -324,6 +337,7 @@ plot.ArraySpeciesBySize <- function(x, species = NULL, total = FALSE, background = TRUE, y_ticks = 6, ...) { size_axis <- plot_size_axis(size_axis) + log_y <- array_log_y(x, log_y, log, !missing(log_y)) log_axes <- parsePlotLog(log, log_x = log_x, log_y = log_y) log_x <- log_axes$log_x log_y <- log_axes$log_y @@ -335,19 +349,15 @@ plot.ArraySpeciesBySize <- function(x, species = NULL, plot_dat <- prepare_ArraySpeciesBySize_plot_data( x, species = species, all.sizes = all.sizes, wlim = wlim, total = total, background = background) - density_power <- array_spectrum_power(x) - if (is.null(density_power)) { - plot_dat <- convert_plot_size_axis(plot_dat, params, size_axis) - } else { - plot_dat <- convert_plot_spectrum_axis(plot_dat, params, size_axis, - power = density_power) - } + plot_dat <- convert_plot_density_axis(plot_dat, params, size_axis, + density_wrt = array_density_wrt(x)) if (identical(size_axis, "l")) { plot_dat <- filter_plot_length_limits(plot_dat, llim) } if (return_data) return(plot_dat) + ylim <- array_ylim(x, ylim, log_y, plot_dat[[2]]) y_label <- array_y_label(x, default = "Rate", size_axis = size_axis) plotDataFrame(plot_dat, params, xlab = plot_size_xlab(size_axis), @@ -524,7 +534,7 @@ plot2.ArraySpeciesBySize <- function(x, y, name1 = "First", name2 = "Second", ylim = ylim, y_ticks = y_ticks, legend_var = "Legend", size_axis = size_axis, - spectrum_power = array_spectrum_power(x)) + density_wrt = array_density_wrt(x)) } #' Plot relative difference between two mizer arrays @@ -668,20 +678,108 @@ compare_array_metadata <- function(x, y) { warning("The first array has y units `", units1, "`, but the second array has y units `", units2, "`.") } + type1 <- array_type(x) + type2 <- array_type(y) + if (!identical(type1, type2)) { + warning("The first array holds a value of type `", type1, + "`, but the second array holds a value of type `", type2, + "`. Only the first is used to decide how the values are ", + "plotted.") + } } -array_spectrum_power <- function(x) { - is_density <- identical(attr(x, "value_name"), "Number density") || - identical(attr(x, "units"), "1/g") - if (is_density) 0 else NULL +#' Kinds of quantity a mizer array can hold +#' +#' Mizer arrays record what kind of quantity their values are in their `type` +#' attribute, because some kinds need handling that the numbers alone do not +#' reveal: +#' \describe{ +#' \item{`"value"`}{the default: a rate, an amount, anything that needs no +#' special handling.} +#' \item{`"density"`}{an amount per gram of body weight, like a number +#' density. Plotting a density against a length axis restates it per +#' centimetre, which changes the values and not just the axis.} +#' \item{`"proportion"`}{a fraction, like the feeding level. Plotted on a +#' linear y axis showing the whole of the interval from 0 to 1, so that the +#' value can be read against the scale it belongs to.} +#' } +#' +#' A `"proportion"` is not *restricted* to the interval from 0 to 1: the +#' critical feeding level and the resource level can both exceed 1, and their +#' plots show it. The type is a statement about what the number means, not a +#' bound that mizer enforces. +#' +#' @format A character vector of the three types. +#' @keywords internal +array_types <- c("value", "density", "proportion") + +#' Validate the type of a mizer array +#' +#' @param type One of [array_types]. +#' @return The validated type. +#' @keywords internal +validate_array_type <- function(type) { + if (!is.character(type) || length(type) != 1 || is.na(type) || + !type %in% array_types) { + stop("`type` must be one of ", + paste0("\"", array_types, "\"", collapse = ", "), ".") + } + type } -array_units <- function(x, size_axis = "w") { - if (identical(plot_size_axis(size_axis), "l") && - !is.null(array_spectrum_power(x))) { - return("1/cm") +#' Resolve the type of a mizer array +#' +#' Called by the array constructors. An explicit `type` is validated and used as +#' given; `NULL` means the constructor was called without the argument, in which +#' case a density is recognised from the other metadata, the way mizer +#' recognised one before the `type` attribute existed. That keeps arrays built +#' by extension packages, and arrays saved by earlier versions, behaving as they +#' did. +#' +#' @param type The type supplied to the constructor, or `NULL`. +#' @param value_name The `value_name` of the array. +#' @param units The `units` of the array. +#' @return One of [array_types]. +#' @keywords internal +resolve_array_type <- function(type, value_name = NULL, units = NULL) { + if (!is.null(type)) { + return(validate_array_type(type)) + } + if (identical(value_name, "Number density") || identical(units, "1/g")) { + return("density") } - attr(x, "units") + "value" +} + +#' The type of a mizer array +#' +#' @param x A mizer array object. +#' @return One of [array_types]. +#' @keywords internal +array_type <- function(x) { + resolve_array_type(attr(x, "type"), + attr(x, "value_name"), attr(x, "units")) +} + +#' The density measure of a mizer array +#' +#' The bridge from the array metadata into the density machinery of the plots. +#' Mizer arrays are indexed by the model's weight grid, so a stored density is +#' always a density with respect to weight; the other measures in +#' [density_measures] arise only for quantities that the spectrum plots compute +#' on the fly, such as a density per logarithmic weight. +#' +#' @param x A mizer array object. +#' @return `"w"` if the array holds a density, otherwise `NA_character_`. +#' @keywords internal +array_density_wrt <- function(x) { + if (identical(array_type(x), "density")) "w" else NA_character_ +} + +array_units <- function(x, size_axis = "w") { + density_wrt <- array_density_wrt(x) + convert_density_units(attr(x, "units"), density_wrt, + density_target_measure(density_wrt, size_axis)) } array_y_label <- function(x, default = "Value", size_axis = "w") { @@ -813,13 +911,8 @@ addPlot.ArraySpeciesBySize <- function(plot, x, species = NULL, x, species = species, all.sizes = all.sizes, wlim = wlim, total = total, background = background) params <- attr(x, "params") - density_power <- array_spectrum_power(x) - if (is.null(density_power)) { - plot_dat <- convert_plot_size_axis(plot_dat, params, size_axis) - } else { - plot_dat <- convert_plot_spectrum_axis(plot_dat, params, size_axis, - power = density_power) - } + plot_dat <- convert_plot_density_axis(plot_dat, params, size_axis, + density_wrt = array_density_wrt(x)) if (identical(size_axis, "l")) { plot_dat <- filter_plot_length_limits(plot_dat, llim) } @@ -1102,6 +1195,7 @@ get_ArraySpeciesBySize_w <- function(x) { if (is.matrix(result) && length(dim(result)) == 2) { attr(result, "value_name") <- attr(x, "value_name") attr(result, "units") <- attr(x, "units") + attr(result, "type") <- attr(x, "type") attr(result, "params") <- attr(x, "params") attr(result, "representation") <- attr(x, "representation") class(result) <- c("ArraySpeciesBySize", "matrix", "array") @@ -1124,6 +1218,7 @@ unclass_rate <- function(x) { x <- unclass(x) attr(x, "value_name") <- NULL attr(x, "units") <- NULL + attr(x, "type") <- NULL attr(x, "params") <- NULL attr(x, "representation") <- NULL x diff --git a/R/ArrayTimeBySpecies-class.R b/R/ArrayTimeBySpecies-class.R index 9a017f92e..c38ff75d7 100644 --- a/R/ArrayTimeBySpecies-class.R +++ b/R/ArrayTimeBySpecies-class.R @@ -23,6 +23,8 @@ #' object to test. #' @param value_name A string giving the human-readable name for the value. #' @param units A string giving the units (e.g. "g", "g/year"). +#' @param type The kind of quantity the values are, see [ArraySpeciesBySize()] +#' and [array_types]. #' @param params A `MizerParams` object holding the model that created the #' values. #' @@ -37,14 +39,16 @@ #' summary(bio) #' } ArrayTimeBySpecies <- function(x, value_name = NULL, units = NULL, - params = NULL) { + type = NULL, params = NULL) { if (!is.matrix(x)) { stop("`x` must be a matrix.") } + type <- resolve_array_type(type, value_name, units) structure(x, class = c("ArrayTimeBySpecies", "matrix", "array"), value_name = value_name, units = units, + type = type, params = params ) } @@ -182,6 +186,7 @@ plot.ArrayTimeBySpecies <- function(x, species = NULL, log_y = TRUE, log = NULL, return_data = FALSE, ...) { + log_y <- array_log_y(x, log_y, log, !missing(log_y)) log_axes <- parsePlotLog(log, log_x = log_x, log_y = log_y) log_x <- log_axes$log_x log_y <- log_axes$log_y @@ -201,6 +206,7 @@ plot.ArrayTimeBySpecies <- function(x, species = NULL, if (return_data) return(plot_dat) + ylim <- array_ylim(x, ylim, log_y, plot_dat[[2]]) plotDataFrame(plot_dat, params, xlab = "Year", ylab = y_label, xtrans = if (log_x) "log10" else "identity", ytrans = if (log_y) "log10" else "identity", @@ -423,6 +429,7 @@ as.data.frame.ArrayTimeBySpecies <- function(x, row.names = NULL, if (is.matrix(result) && length(dim(result)) == 2) { attr(result, "value_name") <- attr(x, "value_name") attr(result, "units") <- attr(x, "units") + attr(result, "type") <- attr(x, "type") attr(result, "params") <- attr(x, "params") class(result) <- c("ArrayTimeBySpecies", "matrix", "array") } @@ -443,6 +450,7 @@ unclass_time <- function(x) { x <- unclass(x) attr(x, "value_name") <- NULL attr(x, "units") <- NULL + attr(x, "type") <- NULL attr(x, "params") <- NULL x } diff --git a/R/ArrayTimeBySpeciesBySize-class.R b/R/ArrayTimeBySpeciesBySize-class.R index 4ec08b801..fac9712bf 100644 --- a/R/ArrayTimeBySpeciesBySize-class.R +++ b/R/ArrayTimeBySpeciesBySize-class.R @@ -17,6 +17,7 @@ #' \item `value_name` – a human-readable name for the value #' (e.g. "Fishing mortality"). #' \item `units` – the units of the value (e.g. "1/year"). +#' \item `type` – the kind of quantity the values are. #' \item `params` – the `MizerParams` object that the value was computed from. #' } #' @@ -24,6 +25,8 @@ #' `is.ArrayTimeBySpeciesBySize()`, any object to test. #' @param value_name A string giving the human-readable name for the value. #' @param units A string giving the units (e.g. "1/year"). +#' @param type The kind of quantity the values are, see [ArraySpeciesBySize()] +#' and [array_types]. #' @param params A `MizerParams` object. Used for species colours, linetypes, #' and size ranges in the `plot()` and `animateSpectra()` methods. #' @param representation Either `"point"` (the default) for a quantity sampled @@ -44,16 +47,19 @@ #' plot(fmort, time = 2007) #' } ArrayTimeBySpeciesBySize <- function(x, value_name = NULL, units = NULL, + type = NULL, params = NULL, representation = c("point", "average")) { if (!is.array(x) || length(dim(x)) != 3) { stop("`x` must be a 3D array.") } representation <- match.arg(representation) + type <- resolve_array_type(type, value_name, units) structure(x, class = c("ArrayTimeBySpeciesBySize", "array"), value_name = value_name, units = units, + type = type, params = params, representation = representation ) @@ -229,7 +235,8 @@ plot.ArrayTimeBySpeciesBySize <- function(x, species = NULL, time = NULL, nrow = dim(arr)[2], dimnames = dimnames(arr)[2:3]) slice <- ArraySpeciesBySize(slice, value_name = value_name, - units = units, params = params) + units = units, type = array_type(x), + params = params) plot.ArraySpeciesBySize(slice, species = species, all.sizes = all.sizes, highlight = highlight, return_data = return_data, @@ -337,7 +344,8 @@ ArrayTimeBySpeciesBySize_slice <- function(x, time = NULL) { nrow = dim(arr)[2], dimnames = dimnames(arr)[2:3]) ArraySpeciesBySize(slice, value_name = value_name, - units = units, params = params, + units = units, type = array_type(x), + params = params, representation = representation) } @@ -382,8 +390,6 @@ animate.ArrayTimeBySpeciesBySize <- function(x, species = NULL, log_y <- log_axes$log_y params <- attr(x, "params") - value_name <- attr(x, "value_name") %||% "Value" - units_str <- attr(x, "units") all_species <- dimnames(x)[[2]] @@ -443,20 +449,12 @@ animate.ArrayTimeBySpeciesBySize <- function(x, species = NULL, df <- rbind(df, total_sums[, names(df)]) } - spectrum_power <- array_spectrum_power(x) - y_label <- if (identical(size_axis, "l") && - !is.null(spectrum_power)) { - paste0(value_name, " [1/cm]") - } else if (!is.null(units_str) && nzchar(units_str)) { - paste0(value_name, " [", units_str, "]") - } else { - value_name - } + y_label <- array_y_label(x, default = "Value", size_axis = size_axis) animate_plotly(df, params, log_x, log_y, y_label, wlim, llim, ylim, size_axis = size_axis, - spectrum_power = spectrum_power, + density_wrt = array_density_wrt(x), frame_duration = frame_duration, transition_duration = transition_duration, easing = easing) @@ -485,6 +483,7 @@ as.data.frame.ArrayTimeBySpeciesBySize <- function(x, row.names = NULL, if (is.array(result) && length(dim(result)) == 3) { attr(result, "value_name") <- attr(x, "value_name") attr(result, "units") <- attr(x, "units") + attr(result, "type") <- attr(x, "type") attr(result, "params") <- attr(x, "params") attr(result, "representation") <- attr(x, "representation") class(result) <- c("ArrayTimeBySpeciesBySize", "array") @@ -492,18 +491,21 @@ as.data.frame.ArrayTimeBySpeciesBySize <- function(x, row.names = NULL, dim_names <- names(dimnames(result)) attrs <- list(value_name = attr(x, "value_name"), units = attr(x, "units"), + type = array_type(x), params = attr(x, "params"), representation = attr(x, "representation") %||% "point") if (identical(dim_names, c("sp", "w"))) { result <- ArraySpeciesBySize(result, value_name = attrs$value_name, units = attrs$units, + type = attrs$type, params = attrs$params, representation = attrs$representation) } else if (identical(dim_names, c("time", "sp"))) { result <- ArrayTimeBySpecies(result, value_name = attrs$value_name, units = attrs$units, + type = attrs$type, params = attrs$params) } } @@ -523,6 +525,7 @@ unclass_tss <- function(x) { x <- unclass(x) attr(x, "value_name") <- NULL attr(x, "units") <- NULL + attr(x, "type") <- NULL attr(x, "params") <- NULL attr(x, "representation") <- NULL x diff --git a/R/MizerSim-class.R b/R/MizerSim-class.R index a60de03ff..cc9786f4e 100644 --- a/R/MizerSim-class.R +++ b/R/MizerSim-class.R @@ -343,7 +343,8 @@ N <- function(sim) { N.MizerSim <- function(sim) { assert_that(is(sim, "MizerSim")) ArrayTimeBySpeciesBySize(sim@n, value_name = "Number density", - units = "1/g", params = sim@params) + units = "1/g", type = "density", + params = sim@params) } #' @rdname N @@ -359,7 +360,8 @@ NResource <- function(sim) { NResource.MizerSim <- function(sim) { assert_that(is(sim, "MizerSim")) ArrayTimeByResourceBySize(sim@n_pp, value_name = "Number density", - units = "1/g", params = sim@params) + units = "1/g", type = "density", + params = sim@params) } @@ -384,7 +386,7 @@ finalN.MizerSim <- function(sim) { n <- sim@params@initial_n # Needed to get the right dimnames n[] <- sim@n[dim(sim@n)[[1]], , ] ArraySpeciesBySize(n, value_name = "Number density", - params = sim@params) + type = "density", params = sim@params) } #' @rdname finalN @@ -402,7 +404,7 @@ finalNResource.MizerSim <- function(sim) { assert_that(is(sim, "MizerSim")) ArrayResourceBySize(sim@n_pp[dim(sim@n_pp)[[1]], ], value_name = "Number density", units = "1/g", - params = sim@params) + type = "density", params = sim@params) } #' @rdname finalN diff --git a/R/animateSpectra.R b/R/animateSpectra.R index 4d32099b1..d9d9b2c8a 100644 --- a/R/animateSpectra.R +++ b/R/animateSpectra.R @@ -221,8 +221,7 @@ animate.MizerSim <- function(x, species = NULL, animate_plotly(nf, sim@params, log_x, log_y, y_label, wlim, llim, ylim, size_axis = size_axis, - spectrum_power = power, - spectrum_per_log_size = spectrum$per_log_size, + density_wrt = spectrum_density_wrt(spectrum$per_log_size), frame_duration = frame_duration, transition_duration = transition_duration, easing = easing) @@ -237,20 +236,13 @@ animate_plotly <- function(df, params, log_x, log_y, y_label, wlim = c(NA, NA), llim = c(NA, NA), ylim = c(NA, NA), size_axis = "w", - spectrum_power = NULL, - spectrum_per_log_size = NULL, + density_wrt = NA_character_, frame_duration = 500, transition_duration = 500, easing = "linear") { size_axis <- plot_size_axis(size_axis) - if (is.null(spectrum_power)) { - df <- convert_plot_size_axis(df, params, size_axis) - } else { - per_log_size <- spectrum_per_log_size %||% (spectrum_power == 2) - df <- convert_plot_spectrum_axis(df, params, size_axis, - power = spectrum_power, - per_log_size = per_log_size, - value_col = "value") - } + df <- convert_plot_density_axis(df, params, size_axis, + density_wrt = density_wrt, + value_col = "value") x_var <- plot_size_x_var(size_axis) legend_name_order <- intersect(names(params@linecolour), unique(df$legend_name)) diff --git a/R/get_initial_n.R b/R/get_initial_n.R index ae8e9a091..429f55e42 100644 --- a/R/get_initial_n.R +++ b/R/get_initial_n.R @@ -47,7 +47,7 @@ get_initial_n <- function(params, n0_mult = NULL, a = 0.35) { ) ] <- 0 return(ArraySpeciesBySize(initial_n, value_name = "Number density", - params = params)) + type = "density", params = params)) } p <- params @@ -83,5 +83,5 @@ get_initial_n <- function(params, n0_mult = NULL, a = 0.35) { } p <- matchBiomasses(p) return(ArraySpeciesBySize(p@initial_n, value_name = "Number density", - params = p)) + type = "density", params = p)) } diff --git a/R/plots.R b/R/plots.R index 9c59fffa0..f2a4f6247 100644 --- a/R/plots.R +++ b/R/plots.R @@ -403,11 +403,9 @@ plotHover.mizer_plot <- function(x = ggplot2::last_plot(), ..., #' line colour. #' @param size_axis Optional. If non-NULL, the x-axis is converted to weight #' (`"w"`) or length (`"l"`). -#' @param spectrum_power Optional power used to weight a number spectrum. When -#' supplied, spectrum values are transformed along with a length axis. -#' @param spectrum_per_log_size Whether the spectrum is a density with respect -#' to logarithmic size, which selects the Jacobian used for a length axis. -#' Defaults to `spectrum_power == 2`. +#' @param density_wrt The measure the values are a density with respect to, see +#' [density_measures]. `NA` (the default) means the values are not a density +#' and are left alone when the size axis changes. #' @return A `mizer_plot` (ggplot2) object. #' @keywords internal plotComparisonDataFrame <- function(frame1, frame2, params, @@ -418,8 +416,7 @@ plotComparisonDataFrame <- function(frame1, frame2, params, y_ticks = 6, highlight = NULL, legend_var = "Legend", size_axis = NULL, - spectrum_power = NULL, - spectrum_per_log_size = NULL) { + density_wrt = NA_character_) { assert_that(is.data.frame(frame1), is.data.frame(frame2), is(params, "MizerParams")) @@ -441,16 +438,10 @@ plotComparisonDataFrame <- function(frame1, frame2, params, } if (!is.null(size_axis)) { size_axis <- plot_size_axis(size_axis) - if (is.null(spectrum_power)) { - frame <- convert_plot_size_axis(frame, params, size_axis, - species_col = group_var) - } else { - per_log_size <- spectrum_per_log_size %||% (spectrum_power == 2) - frame <- convert_plot_spectrum_axis(frame, params, size_axis, - power = spectrum_power, - per_log_size = per_log_size, - species_col = group_var) - } + frame <- convert_plot_density_axis(frame, params, size_axis, + density_wrt = density_wrt, + species_col = group_var, + value_col = y_var) x_var <- plot_size_x_var(size_axis) } @@ -670,6 +661,67 @@ plot_size_tooltip <- function(size_axis, before = NULL, after = NULL) { c(before, plot_size_x_var(size_axis), after) } +#' Y-axis limits for a plot of a proportion +#' +#' A proportion is easiest to read against the whole of the interval from 0 to +#' 1, so that is the range a plot of one shows by default. The range is only +#' ever *widened* to include the data, never narrowed to the interval: a +#' critical feeding level or a resource level above 1 is a real feature of the +#' model and must stay visible. +#' +#' Only the ends of `ylim` that the caller left as `NA` are filled in, so an +#' explicit limit always wins. A logarithmic axis is left alone, having no place +#' for the 0. +#' +#' @param ylim Numeric vector of length two, the limits the caller asked for. +#' @param log_y Whether the y axis is logarithmic. +#' @param values The values being plotted. +#' @return A numeric vector of length two. +#' @keywords internal +proportion_ylim <- function(ylim, log_y, values) { + if (isTRUE(log_y)) return(ylim) + values <- values[is.finite(values)] + full <- range(c(0, 1, values)) + if (is.na(ylim[1])) ylim[1] <- full[1] + if (is.na(ylim[2])) ylim[2] <- full[2] + ylim +} + +#' Y-axis limits an array's type calls for +#' +#' Only a `"proportion"` has an opinion. A `"density"` is handled where the size +#' axis is converted, and a `"value"` needs nothing. +#' +#' @param x A mizer array object. +#' @inheritParams proportion_ylim +#' @return A numeric vector of length two. +#' @keywords internal +array_ylim <- function(x, ylim, log_y, values) { + if (identical(array_type(x), "proportion")) { + return(proportion_ylim(ylim, log_y, values)) + } + ylim +} + +#' The logarithmic y axis an array's type calls for +#' +#' A proportion belongs on a linear axis, so a plot of one turns `log_y` off +#' unless the caller asked for a particular axis. Called before +#' [parsePlotLog()], which is why it also has to check `log`. +#' +#' @param x A mizer array object. +#' @param log_y The `log_y` argument of the plot method. +#' @param log The `log` argument of the plot method. +#' @param given Whether the caller supplied `log_y` (i.e. `!missing(log_y)`). +#' @return The `log_y` to use. +#' @keywords internal +array_log_y <- function(x, log_y, log, given) { + if (!given && is.null(log) && identical(array_type(x), "proportion")) { + return(FALSE) + } + log_y +} + #' Convert plotting data from weight to length #' #' When `size_axis = "l"`, adds a length column `l` computed from the weight @@ -728,20 +780,204 @@ convert_plot_size_axis <- function(plot_dat, params, size_axis, drop = FALSE] } +#' Density measures a spectrum can be expressed in +#' +#' A size spectrum is a density, and a density only has a meaning together with +#' the variable it is a density with respect to. That variable is one of +#' \describe{ +#' \item{`"w"`}{a density with respect to weight, e.g. numbers per gram.} +#' \item{`"log_w"`}{a density with respect to logarithmic weight, e.g. +#' numbers per log weight interval.} +#' \item{`"l"`}{a density with respect to length, e.g. numbers per cm.} +#' \item{`"log_l"`}{a density with respect to logarithmic length.} +#' \item{`NA`}{not a density, e.g. a rate or a dimensionless quantity. Such +#' values are left alone when the size axis changes.} +#' } +#' +#' Mizer arrays are indexed by the model's weight grid, so an array that holds +#' a density (`type = "density"`, see [array_types]) always holds one with +#' respect to weight. The other measures arise for quantities the spectrum plots +#' compute on the fly: `plotSpectra(per_log_size = TRUE)` shows a density with +#' respect to logarithmic weight, and either can be restated per unit length by +#' `size_axis = "l"`. +#' +#' @format A character vector of the four density measures. +#' @keywords internal +density_measures <- c("w", "log_w", "l", "log_l") + +#' Validate a density measure +#' +#' @param density_wrt A density measure, see [density_measures]. `NULL` and `NA` +#' both stand for "not a density". +#' @return The validated measure, or `NA_character_` when the values are not a +#' density. +#' @keywords internal +validate_density_wrt <- function(density_wrt) { + if (is.null(density_wrt)) return(NA_character_) + if (length(density_wrt) == 1 && all(is.na(density_wrt))) { + return(NA_character_) + } + if (!is.character(density_wrt) || length(density_wrt) != 1 || + !density_wrt %in% density_measures) { + stop("`density_wrt` must be NA or one of ", + paste0("\"", density_measures, "\"", collapse = ", "), ".") + } + density_wrt +} + +#' The density measure a plot against a given size axis calls for +#' +#' Plotting against a length axis turns a density with respect to weight into a +#' density with respect to length, and a density with respect to logarithmic +#' weight into one with respect to logarithmic length. Whether the density is +#' per size or per logarithmic size is a property of the values and is left +#' alone; only the size variable follows the axis. +#' +#' @param density_wrt The measure the values are a density with respect to, see +#' [density_measures]. +#' @param size_axis Either `"w"` (weight) or `"l"` (length). +#' @return The density measure to express the values in, or `NA_character_` if +#' the values are not a density. +#' @keywords internal +density_target_measure <- function(density_wrt, size_axis) { + density_wrt <- validate_density_wrt(density_wrt) + if (is.na(density_wrt)) return(NA_character_) + paste0(if (startsWith(density_wrt, "log_")) "log_" else "", + plot_size_axis(size_axis)) +} + +#' Factor relating a density measure to a density with respect to weight +#' +#' Writing \eqn{N_w} for the density with respect to weight, the density with +#' respect to measure \eqn{m} is \eqn{N_w} times the factor returned here. With +#' the allometric weight-length relationship \eqn{w = a l^b} these factors are +#' \eqn{1} for `"w"`, \eqn{w} for `"log_w"`, \eqn{dw/dl = b w / l} for `"l"` +#' and \eqn{l\,dw/dl = b w} for `"log_l"`. +#' +#' @param measure One of [density_measures]. +#' @param w,l,b Numeric vectors of the same length giving the weight, the +#' corresponding length, and the exponent of the weight-length relationship. +#' @return A numeric vector of factors. +#' @keywords internal +density_measure_weight <- function(measure, w, l, b) { + switch(measure, + w = rep(1, length(w)), + log_w = w, + l = b * w / l, + log_l = b * w, + stop("Unknown density measure `", measure, "`.")) +} + +#' Jacobian converting between two density measures +#' +#' @param from,to Density measures, see [density_measures]. +#' @inheritParams density_measure_weight +#' @return A numeric vector by which to multiply a density with respect to +#' `from` to obtain the density with respect to `to`. +#' @keywords internal +density_measure_jacobian <- function(from, to, w, l, b) { + if (identical(from, to)) return(rep(1, length(w))) + density_measure_weight(to, w, l, b) / density_measure_weight(from, w, l, b) +} + +#' The size unit appearing in the units of a density +#' +#' @param measure One of [density_measures]. +#' @return `"g"` or `"cm"`, or `NA_character_` for a density with respect to a +#' logarithmic size, whose units carry no size unit. +#' @keywords internal +density_size_unit <- function(measure) { + switch(measure, w = "g", l = "cm", NA_character_) +} + +#' Restate the units of a density in a different density measure +#' +#' The size unit is swapped inside the two spellings mizer uses for a per-size +#' factor, `1/g` and `g^-1`. Units that state no per-size factor are returned +#' unchanged, since there is then nothing to identify as the size unit. +#' +#' @param units The units of the values, possibly `NULL`. +#' @param from,to Density measures, see [density_measures]. +#' @return The units expressed in the `to` measure. +#' @keywords internal +convert_density_units <- function(units, from, to) { + if (is.na(from) || is.na(to) || identical(from, to)) return(units) + to_sym <- density_size_unit(to) + if (is.null(units) || !nzchar(units)) { + # Nothing was declared, so fall back to the canonical density units + return(if (is.na(to_sym)) units else paste0("1/", to_sym)) + } + from_sym <- density_size_unit(from) + if (is.na(from_sym) || is.na(to_sym)) return(units) + units <- sub(paste0("\\b1/", from_sym, "\\b"), paste0("1/", to_sym), units) + sub(paste0("\\b", from_sym, "\\^-1"), paste0(to_sym, "^-1"), units) +} + +#' Express plotting data on the requested size axis +#' +#' Converts the size coordinate of the plotting data to the requested axis and, +#' when the values are a density, multiplies them by the Jacobian that restates +#' them in the density measure that axis calls for (see +#' [density_target_measure()]). Values that are not a density are left alone. +#' +#' The Jacobian is a per-species quantity, so rows whose species is not one of +#' the model's species — the "Total" row, for instance — cannot be converted and +#' are dropped whenever a conversion is needed. +#' +#' @inheritParams convert_plot_size_axis +#' @param density_wrt The measure the values are a density with respect to, see +#' [density_measures]. `NA` (the default) means the values are not a density. +#' @param value_col Name or index of the value column. Defaults to the second +#' column. +#' @return The plotting data with its size coordinate, and where called for its +#' values, expressed for the requested axis. The size coordinate is the first +#' column. +#' @keywords internal +convert_plot_density_axis <- function(plot_dat, params, size_axis, + density_wrt = NA_character_, + species_col = "Species", + value_col = 2) { + size_axis <- plot_size_axis(size_axis) + density_wrt <- validate_density_wrt(density_wrt) + target <- density_target_measure(density_wrt, size_axis) + needs_jacobian <- !is.na(target) && !identical(target, density_wrt) + if (identical(size_axis, "w") && !needs_jacobian) { + return(plot_dat) + } + if (is.numeric(value_col)) { + value_col <- names(plot_dat)[[value_col]] + } + # Both a length axis and the Jacobian need the lengths, and the Jacobian + # needs the weights as well. + plot_dat <- convert_plot_size_axis(plot_dat, params, "l", + species_col = species_col, + drop_w = FALSE) + if (needs_jacobian && nrow(plot_dat) > 0) { + species_idx <- match(as.character(plot_dat[[species_col]]), + as.character(params@species_params$species)) + plot_dat[[value_col]] <- plot_dat[[value_col]] * + density_measure_jacobian(density_wrt, target, + plot_dat$w, plot_dat$l, + params@species_params$b[species_idx]) + } + x_var <- plot_size_x_var(size_axis) + plot_dat[, c(x_var, setdiff(names(plot_dat), c("l", "w"))), drop = FALSE] +} + #' Convert a weight-based spectrum to a length-based spectrum #' #' A density with respect to weight is converted to a density with respect to #' length with the Jacobian `dw/dl = b * w / l`. A density with respect to #' logarithmic weight is instead converted with -#' `d log(w) / d log(l) = b`. +#' `d log(w) / d log(l) = b`. This is the interface used by the `power`-based +#' spectrum plots; arrays carry their density measure explicitly and use +#' [convert_plot_density_axis()] instead. #' -#' @inheritParams convert_plot_size_axis +#' @inheritParams convert_plot_density_axis #' @param power The power of weight multiplying the number density. #' @param per_log_size Whether the spectrum is a density with respect to #' logarithmic size rather than with respect to size. Defaults to #' `power == 2`, the only power for which this used to be the case. -#' @param value_col Name or index of the spectrum-value column. Defaults to the -#' second column. #' @return The plotting data with both its size coordinate and spectrum values #' expressed for the requested axis. #' @keywords internal @@ -749,22 +985,20 @@ convert_plot_spectrum_axis <- function(plot_dat, params, size_axis, power, per_log_size = power == 2, species_col = "Species", value_col = 2) { - size_axis <- plot_size_axis(size_axis) - if (identical(size_axis, "w")) { - return(plot_dat) - } - if (is.numeric(value_col)) { - value_col <- names(plot_dat)[[value_col]] - } - plot_dat <- convert_plot_size_axis(plot_dat, params, size_axis, - species_col = species_col, - drop_w = FALSE) - species_idx <- match(as.character(plot_dat[[species_col]]), - as.character(params@species_params$species)) - sp <- params@species_params[species_idx, , drop = FALSE] - jacobian <- if (per_log_size) sp$b else sp$b * plot_dat$w / plot_dat$l - plot_dat[[value_col]] <- plot_dat[[value_col]] * jacobian - plot_dat[, c("l", setdiff(names(plot_dat), c("l", "w"))), drop = FALSE] + convert_plot_density_axis(plot_dat, params, size_axis, + density_wrt = spectrum_density_wrt(per_log_size), + species_col = species_col, + value_col = value_col) +} + +#' The density measure of a `power`-based spectrum +#' +#' @param per_log_size Whether the spectrum is a density with respect to +#' logarithmic size. +#' @return `"log_w"` or `"w"`. +#' @keywords internal +spectrum_density_wrt <- function(per_log_size) { + if (isTRUE(per_log_size)) "log_w" else "w" } @@ -2069,6 +2303,7 @@ plotSpectra2 <- function(object1, object2, name1 = "First", name2 = "Second", background = background, size_axis = "w", return_data = TRUE, ...) params <- if (is(object1, "MizerSim")) object1@params else object1 + density_wrt <- spectrum_density_wrt(spectrum$per_log_size) plotComparisonDataFrame(sf1, sf2, validParams(params), name1 = name1, name2 = name2, @@ -2083,8 +2318,7 @@ plotSpectra2 <- function(object1, object2, name1 = "First", name2 = "Second", ylim = ylim, highlight = highlight, legend_var = "Legend", size_axis = size_axis, - spectrum_power = power, - spectrum_per_log_size = spectrum$per_log_size) + density_wrt = density_wrt) } #' Resolve the power of weight multiplying a spectrum @@ -2623,6 +2857,12 @@ plot_feeding_level <- function(params, feed, species, highlight, plot_dat$Legend <- factor(plot_dat$Species, levels = legend_levels) linesize <- make_linesize(legend_levels, highlight) + # The feeding level array declares itself a proportion, so show the whole + # of [0, 1] — widened if the critical feeding level rises above it, which + # it can. A logarithmic axis is left to the data. + feeding_level_ylim <- if (log_y) NULL else + array_ylim(feed, c(NA, NA), log_y, plot_dat[["Feeding level"]]) + # We do not use `plotDataFrame()` to create the plot because it would not # handle the alpha transparency for the critical feeding level. @@ -2646,8 +2886,7 @@ plot_feeding_level <- function(params, feed, species, highlight, limits = plot_size_xlim(wlim, size_axis, llim)) + scale_y_continuous(name = "Feeding Level", trans = if (log_y) "log10" else "identity") + - # Feeding level is naturally bounded in [0, 1] on linear scale. - coord_cartesian(ylim = if (log_y) NULL else c(0, 1)) + + coord_cartesian(ylim = feeding_level_ylim) + scale_colour_manual(values = params@linecolour[legend_levels]) + scale_linetype_manual(values = params@linetype[legend_levels]) + scale_discrete_manual("linewidth", values = linesize), diff --git a/R/rate_functions.R b/R/rate_functions.R index 3230cc637..9bff83a47 100644 --- a/R/rate_functions.R +++ b/R/rate_functions.R @@ -187,7 +187,7 @@ getFeedingLevel.MizerParams <- function(object, n, n_pp, n_other, encounter = encounter, t = t) } return(ArraySpeciesBySize(feeding_level, value_name = "Feeding level", - params = params)) + type = "proportion", params = params)) } #' @export @@ -195,7 +195,8 @@ getFeedingLevel.MizerSim <- function(object, n, n_pp, n_other, time_range, drop = FALSE, ...) { sim <- object sim_size_rate(sim, time_range, drop, target = "FeedingLevel", - slot = "feeding_level", value_name = "Feeding level", ...) + slot = "feeding_level", value_name = "Feeding level", + type = "proportion", ...) } @@ -220,7 +221,7 @@ getCriticalFeedingLevel.MizerParams <- function(params) { params <- validParams(params) result <- params@metab / params@intake_max / params@species_params$alpha ArraySpeciesBySize(result, value_name = "Critical feeding level", - params = params) + type = "proportion", params = params) } @@ -1478,7 +1479,8 @@ getFluxGradient.MizerParams <- function(object, dimnames(gradient) <- dimnames(params@metab) ArraySpeciesBySize(gradient, value_name = "Flux gradient", - units = "g^-1/year", params = params) + units = "g^-1/year", type = "density", + params = params) } #' @rdname getFluxGradient @@ -1523,5 +1525,6 @@ getFluxGradient.MizerSim <- function(object, n, n_pp, n_other, t, ..., dimnames(gradient) <- dimnames(params@metab) gradient }, - value_name = "Flux gradient", units = "g^-1/year") + value_name = "Flux gradient", units = "g^-1/year", + type = "density") } diff --git a/R/setInitialValues.R b/R/setInitialValues.R index 6606947fa..072340437 100644 --- a/R/setInitialValues.R +++ b/R/setInitialValues.R @@ -144,7 +144,7 @@ initialN <- function(object) { initialN.MizerParams <- function(object) { params <- validParams(object) ArraySpeciesBySize(params@initial_n, value_name = "Number density", - params = params) + type = "density", params = params) } #' @rdname initialN-set #' @usage NULL @@ -152,7 +152,7 @@ initialN.MizerParams <- function(object) { initialN.MizerSim <- function(object) { ArraySpeciesBySize(object@params@initial_n, value_name = "Number density", - params = object@params) + type = "density", params = object@params) } #' Initial value for resource spectrum @@ -199,7 +199,7 @@ initialNResource <- function(object) { initialNResource.MizerParams <- function(object) { params <- validParams(object) ArrayResourceBySize(params@initial_n_pp, value_name = "Number density", - units = "1/g", params = params) + units = "1/g", type = "density", params = params) } #' @rdname initialNResource-set #' @usage NULL @@ -207,5 +207,5 @@ initialNResource.MizerParams <- function(object) { initialNResource.MizerSim <- function(object) { ArrayResourceBySize(object@params@initial_n_pp, value_name = "Number density", units = "1/g", - params = object@params) + type = "density", params = object@params) } diff --git a/R/setReproduction.R b/R/setReproduction.R index 189afc004..ef4dd6741 100644 --- a/R/setReproduction.R +++ b/R/setReproduction.R @@ -387,7 +387,7 @@ maturity <- function(params) { maturity.MizerParams <- function(params) { ArraySpeciesBySize(params@maturity, value_name = "Maturity proportion", - params = params) + type = "proportion", params = params) } #' @rdname setReproduction @@ -417,7 +417,7 @@ repro_prop.MizerParams <- function(params) { rp[is.nan(rp)] <- 0 rp[rp > 1] <- 1 ArraySpeciesBySize(rp, value_name = "Reproductive proportion", - params = params) + type = "proportion", params = params) } #' @rdname setReproduction @@ -431,5 +431,5 @@ repro_prop.MizerParams <- function(params) { psi <- function(params) { ArraySpeciesBySize(params@psi, value_name = "Population-level reproductive proportion", - params = params) + type = "proportion", params = params) } diff --git a/R/setResource.R b/R/setResource.R index 3bf25f1ee..01301effd 100644 --- a/R/setResource.R +++ b/R/setResource.R @@ -512,7 +512,7 @@ resource_rate <- function(params) { #' @export resource_capacity <- function(params) { ArrayResourceBySize(params@cc_pp, value_name = "Resource capacity", - units = "1/g", params = params) + units = "1/g", type = "density", params = params) } #' @rdname setResource @@ -529,7 +529,7 @@ resource_capacity <- function(params) { resource_level <- function(params) { ArrayResourceBySize(params@initial_n_pp / params@cc_pp, value_name = "Resource level", units = "", - params = params) + type = "proportion", params = params) } #' @rdname setResource diff --git a/R/sim_rates.R b/R/sim_rates.R index bfba812af..2c7a66b7b 100644 --- a/R/sim_rates.R +++ b/R/sim_rates.R @@ -72,12 +72,15 @@ get_sim_rate_slice <- function(sim, time_idx) { #' `get_sim_rate_slice()`. #' @param value_name Name of the value stored in the returned array. #' @param units Optional units of the value stored in the returned array. +#' @param type The kind of quantity the values are, see [ArraySpeciesBySize()] +#' and [array_types]. #' #' @return A time x species x size array, possibly with dimensions dropped. #' @keywords internal get_species_size_rate_from_sim <- function(sim, time_range, drop, rate_fun, value_name, units = NULL, + type = NULL, representation = "point") { time_elements <- get_sim_rate_time_elements(sim, time_range) @@ -98,12 +101,14 @@ get_species_size_rate_from_sim <- function(sim, time_range, drop, result <- ArrayTimeBySpeciesBySize(result, value_name = value_name, units = units, + type = type, params = sim@params, representation = representation) } else if (is.matrix(result) && names(dimnames(result))[[1]] == "sp") { result <- ArraySpeciesBySize(result, value_name = value_name, - units = units, params = sim@params, + units = units, type = type, + params = sim@params, representation = representation) } result @@ -288,7 +293,7 @@ mizer_rates_subset <- function(params, n, n_pp, n_other, t, effort, #' @return An `ArrayTimeBySpeciesBySize` object (or a reduced array if `drop`). #' @keywords internal sim_size_rate <- function(sim, time_range, drop, target, slot, - value_name, units = NULL, + value_name, units = NULL, type = NULL, use_sim_effort = FALSE, representation = "point", ...) { params <- validParams(sim@params) @@ -316,7 +321,7 @@ sim_size_rate <- function(sim, time_range, drop, target, slot, } m }, - value_name = value_name, units = units, + value_name = value_name, units = units, type = type, representation = representation) } diff --git a/inst/skills/analyse-and-plot/SKILL.md b/inst/skills/analyse-and-plot/SKILL.md index b913379e6..95a4d2290 100644 --- a/inst/skills/analyse-and-plot/SKILL.md +++ b/inst/skills/analyse-and-plot/SKILL.md @@ -167,19 +167,21 @@ If your indicator decomposes the encounter rate — a diet or trophic-level styl quantity — see the note on `encounter_kernel()` in the `extend-mizer` skill before pairing `pred_kernel()` with `getEncounter()`. -## Plotting any array directly with `plot()` +## Plotting mizer arrays -Every static plot mizer produces is a **ggplot2 object** you can extend with `+`. The arrays returned by the summary and rate functions carry a mizer array class and have their own `plot()` method, so you can visualise **any** quantity -without a dedicated plot function or custom ggplot code. +without a dedicated plot function or custom ggplot code. They also carry a +`value_name`, `units` and their `params`, and have `print()`, `summary()` and +`as.data.frame()` methods. | Class | Typical source | `plot()` shows | |---|---|---| | `ArrayTimeBySpecies` | `getBiomass(sim)`, `getSSB(sim)`, `getYield(sim)`, `getN(sim)` | value vs time, one line per species | | `ArraySpeciesBySize` | `getFeedingLevel(params)`, `getPredMort(params)`, `getEncounter(params)` | value vs size, one line per species | | `ArrayTimeBySpeciesBySize` | `getFMort(sim)`, `getPredMort(sim)` | one time slice vs size (set with `time`) | -| `ArrayResourceBySize` | `NResource(params)`, `getResourceMort(params)`, `resource_rate(params)`, `resource_capacity(params)` | resource quantity vs size | +| `ArrayResourceBySize` | `NResource(params)`, `finalNResource(sim)`, `getResourceMort(params)`, `resource_rate(params)`, `resource_capacity(params)`, `resource_level(params)` | resource quantity vs size | +| `ArrayTimeByResourceBySize` | `NResource(sim)` | one time slice vs size (set with `time`) | ```r plot(getBiomass(sim)) # value vs time, one line per species @@ -209,7 +211,7 @@ plotRelative(getEGrowth(params), getEGrowth(params2)) # relative difference plotHover(getBiomass(sim)) # interactive (hover) version of any array plot ``` -## Common arguments +### Common arguments Most analysis and plotting functions — including `plot()` on an array and the dedicated `plot…()` functions below — share these optional arguments: @@ -235,104 +237,156 @@ the underlying numbers — for example the size range that a biomass is summed over — pass `min_w`/`max_w` (or `min_l`/`max_l`) to the `get…()` function instead, e.g. `plot(getBiomass(sim, min_w = 10))`. -`size_axis = "l"` converts the axis with the length–weight parameters `a` and -`b`, so it is unavailable for the resource, which has no species to take them -from. For a *density* it converts the y-axis too, via the appropriate Jacobian — -see the next section. - -### Which density a spectrum plot shows - -`plotSpectra()`, `plotSpectra2()`, `plotCDF()`, `plotCDF2()` and `animate()` -describe the plotted quantity with two independent logical arguments, each of -which contributes one factor of the weight: - -| | `per_log_size = FALSE` | `per_log_size = TRUE` | -|---|---|---| -| `biomass = FALSE` | number density | number density per log size | -| `biomass = TRUE` | biomass density | biomass density per log size | - -The older single `power` argument is the sum of the two (0, 1, 1, 2 across that -table) and is still accepted, but it cannot tell the two `power = 1` cells -apart — it is read as the biomass density with respect to weight, which is what -picks the y-axis label and the length-axis Jacobian. Supplying `power` together -with a flag that contradicts it is an error, so express the choice with the -flags. `plotCDF()` accepts only `per_log_size = FALSE`: a cumulative total does -not depend on the density it was accumulated from. - -**`log_x` does not change the y-axis.** Showing weight on a logarithmic axis is -a display choice; converting a density per unit weight into a density per -logarithmic weight interval is `per_log_size`. Conflating the two is the usual -reason a spectrum looks like it has the wrong slope. - Which arguments apply depends on the array's shape: - `plot()` accepts `species`, `tlim`, `total`, `background`, `highlight`, `log_x`, `log_y`, `ylim`. -- `plot()` accepts `species`, `highlight`, `log_x`, `log_y`, - `wlim`, `llim`, `ylim`, `size_axis`, `all.sizes`. `size_axis` and `llim` - belong to this shape only — a plot against time has no size axis to convert. +- `plot()` accepts `species`, `highlight`, `total`, + `background`, `log_x`, `log_y`, `wlim`, `llim`, `ylim`, `size_axis`, + `all.sizes`. `size_axis` and `llim` belong to the size shapes only — a plot + against time has no size axis to convert. +- `plot()` takes one time slice and hands it to the + `ArraySpeciesBySize` method, so it accepts everything that method does plus + `time` (default: the last time step). It has no `tlim`: only one time is + shown. +- `plot()` accepts `log_x`, `log_y`, `wlim`, `ylim` only. + The resource is a single spectrum, so there is nothing for `species`, + `highlight`, `total` or `background` to select, and no `size_axis`/`llim` + because the resource has no length-weight relationship. +- `plot()` accepts the same as + `ArrayResourceBySize` plus `time`. + +All five also accept `return_data = TRUE`, which returns the data frame behind +the plot instead of the plot, and `y_ticks` to set the number of y-axis ticks. + +### What kind of value an array holds + +Every mizer array declares what kind of quantity it holds, in its `type` +attribute, because two kinds need handling that the numbers alone do not reveal: + +| `type` | Meaning | What the plots do with it | +|---|---|---| +| `"value"` | a rate, an amount — the default | nothing special | +| `"density"` | an amount per gram of body weight | converts the values, not just the axis, when plotted against length | +| `"proportion"` | a fraction | shows the whole of the interval from 0 to 1 on a linear y axis | -## Dedicated plot functions +Read it with `array_type(x)`, and set it when you build an array of your own: -Each dedicated `plot…()` function is essentially `plot()` applied to the matching -`get…()` array, so `plotBiomass(sim)` is `plot(getBiomass(sim))`. They accept the -common arguments above, and each has a `plotly…()` counterpart (e.g. -`plotlyBiomass()`) for interactive use — the array `plot()`s use `plotHover()` -instead. +```{r eval=FALSE} +ArraySpeciesBySize(x, value_name = "Number density", units = "1/g", + type = "density", params = params) +``` -**Against time:** +An array that declares nothing is taken to be a density if it is named +`"Number density"` or has units `"1/g"`, which is how mizer recognised densities +before the attribute existed. -| Function | How it relates to plotting the array directly | -|---|---| -| `plotBiomass(sim)` | same as `plot(getBiomass(sim))` | -| `plotYield(sim)` | same as `plot(getYield(sim))` | -| `plotYieldGear(sim)` | like `plotYield()` but keeps the gear dimension, one panel per gear | +### Plotting densities -**Against body size.** By default these show the final time step; use -`time_range` to average over a period. +A density is an amount *per unit size*, so its numerical value depends on which +size variable it is a density in. Changing that variable — weight to length, or +size to log size — therefore changes the plotted **values**, not just the axis: +it needs a Jacobian factor. The plot functions apply it for you, for the arrays +that declare themselves densities: -| Function | How it relates to plotting the array directly | +| Source | Density | |---|---| -| `plotFeedingLevel(sim)` | same as `plot(getFeedingLevel(sim))` | -| `plotPredMort(sim)` | same as `plot(getPredMort(sim))` | -| `plotFMort(sim)` | same as `plot(getFMort(sim))` | -| `plotSpectra(sim)` | abundance/biomass spectra: additionally overlays the resource spectrum and background species, and `biomass`/`per_log_size` choose the plotted density (see [above](#which-density-a-spectrum-plot-shows)) | -| `plotCDF(sim)` | cumulative version of the spectrum (`normalise` for proportion vs total) | -| `plotGrowthCurves(sim)` | a distinct plot: size at age rather than a size spectrum | -| `plotDiet(params)` | a distinct plot: stacked diet composition by prey | +| `initialN(params)`, `finalN(sim)`, `N(sim)`, `get_initial_n(params)` | consumer number density, per gram | +| `initialNResource(params)`, `finalNResource(sim)`, `NResource(sim)` | resource number density, per gram | +| `resource_capacity(params)` | resource carrying capacity, per gram | +| `getFluxGradient(params)` | rate of change of the number density, per gram per year | -**Calibration:** `plotBiomassObservedVsModel(params)` and -`plotYieldObservedVsModel(params)`; the latter takes a `gear` argument that -restricts both the modelled and the observed catch to the named gears. See the -`calibrate-model` skill. +The factors are built from the length-weight relationship $w = a\, l^b$ of each +species, taken from the `a` and `b` columns of `species_params`: + +| Argument | Factor | +|---|---|---| +| `size_axis = "w"`, `per_log_size = TRUE` | $dw/d\log w = w$ | +| `size_axis = "l"`, `per_log_size = FALSE` | $dw/dl = b\, w / l$ | +| `size_axis = "l"`, `per_log_size = TRUE` | $dw / d\log l = b\, w$ | + +Two things to watch: + +- **The resource is dropped from a length axis.** The resource spectrum has no + length-weight relationship, so `plotSpectra(params, size_axis = "l")` shows the + species only. Use `size_axis = "w"` to see the resource. +- **`plot()` on a density array has no `per_log_size` argument.** Mizer arrays + are indexed by the weight grid, so an array that holds a density holds one per + gram; `plot()` honours `size_axis` but nothing switches an array between per + size and per log size. Use `plotSpectra()` when you want a per-log-size + spectrum. + +### Plotting proportions + +A proportion is easiest to read against the whole of the scale it belongs to, so +`plot()` of one uses a linear y axis running from 0 to 1. The arrays that +declare themselves proportions are `getFeedingLevel()`, +`getCriticalFeedingLevel()`, `maturity()`, `repro_prop()`, `psi()` and +`resource_level()`. + +The range is only ever *widened* to include the data, never narrowed to that +interval — a critical feeding level or a resource level above 1 is a real +feature of the model, and stays visible. An explicit `ylim` or `log_y` always +wins. + +## Plotting size spectra + +"Size spectrum" is just a different name for "density as a function of size". +`plotSpectra()` is the function you want for the plot size spectra. It +plots the abundance or biomass density against size, one line per species, and +unlike a plain `plot()` of a density array it also overlays the resource +spectrum (`resource = TRUE`, the default) and any background species +(`background =TRUE`). Which density it shows is set by `biomass` and +`per_log_size`, described below. + +By default it shows the final time step of a simulation; pass `time_range` to +average over a period, or give it a `MizerParams` object to see the current +state. The common arguments above all apply, and `plotlySpectra()` is the +interactive twin. ```r -plotBiomass(sim, species = c("Cod", "Herring"), total = TRUE) +plotSpectra(params) # spectra of the current state plotSpectra(sim, per_log_size = TRUE, time_range = 1990:2000) -plotGrowthCurves(sim, species = "Cod", max_age = 20) -plotDiet(params, species = "Cod") +plotSpectra(sim, species = c("Cod", "Herring"), resource = FALSE) +plotSpectra(sim, biomass = TRUE, size_axis = "l") # biomass density against length ``` -**Overview:** `plot(sim)` combines several panels; `plot(params)` shows the same -panels for a model's steady state (without the biomass-through-time panel). +### Which density a spectrum plot shows + +`plotSpectra()`, `plotSpectra2()`, `plotCDF()`, `plotCDF2()` and `animate()` +describe the plotted quantity with two independent logical arguments: + +| | `per_log_size = FALSE` | `per_log_size = TRUE` | +|---|---|---| +| `biomass = FALSE` | number density | number density per log size | +| `biomass = TRUE` | biomass density | biomass density per log size | + +The older single `power` argument is the sum of the two (0, 1, 1, 2 across that +table) and is still accepted. + +**`log_x` does not change the y-axis.** Showing weight on a logarithmic axis is +a display choice; you need to use `per_log_size` to convert a density per unit +weight into a density per +logarithmic weight interval. Conflating the two is the usual +reason a spectrum looks like it has the wrong slope. -## Cumulative distributions +### Cumulative distributions `plotCDF(object, species, biomass, normalise)` plots cumulative abundance or biomass over size — steadier than a density spectrum for eyeballing where biomass sits. `biomass = TRUE` (default) accumulates biomass, `biomass = FALSE` accumulates numbers; `normalise = FALSE` plots the cumulative total rather than -the proportion. Unlike in `plotSpectra()`, only `per_log_size = FALSE` is -accepted: the integral does not depend on it. +the proportion. The `per_log_size` argument is not used: a cumulative total +does not depend on it. ```r plotCDF(NS_params, species = c("Cod", "Herring")) plotCDF(NS_sim, biomass = FALSE, normalise = FALSE) ``` -## Comparing two simulations or models +### Comparing two size distributions -For whole spectra use the dedicated functions below; for any other rate array +For whole spectra use the functions in the table below; for any other rate array use `plot2()` and `plotRelative()` from the array toolkit above. | Function | Shows | @@ -347,7 +401,7 @@ plotSpectraRelative(params, params2) # 2 (N2 - N1) / (N1 + N2) plotCDF2(sim, sim2, "Unfished", "Fished") ``` -## Animating spectra through time +### Animating spectra through time `animate()` plays a spectrum or rate array through the course of a simulation (`animateSpectra()` is a retained alias). @@ -358,42 +412,49 @@ animate(getFMort(sim)) # an ArrayTimeBySpeciesBySize over time animate(NResource(sim)) # an ArrayTimeByResourceBySize over time ``` -## The plankton resource +## Dedicated plot functions -Resource-related quantities come back as an `ArrayResourceBySize` — a numeric -vector over the size grid carrying a `value_name`, `units`, and its `params`, -with `print()`, `summary()`, `as.data.frame()`, and `plot()` methods. Producers -include `NResource(params)` / `finalNResource(sim)`, `getResourceMort(params)`, -`resource_rate(params)` (intrinsic birth rate), `resource_capacity(params)` -(carrying capacity), and `resource_level(params)`. +Besides the spectrum plots above, mizer has a dedicated `plot…()` function for +each of the common summary quantities. Each is essentially `plot()` applied to +the matching `get…()` array, so `plotBiomass(sim)` is `plot(getBiomass(sim))`. +They accept the common arguments above, and each has a `plotly…()` counterpart +(e.g. `plotlyBiomass()`) for interactive use — the array `plot()`s use +`plotHover()` instead. -```r -plot(getResourceMort(params)) # resource mortality vs size -summary(NResource(params)) -``` +**Against time:** -The array toolkit works on the resource too: +| Function | How it relates to plotting the array directly | +|---|---| +| `plotBiomass(sim)` | same as `plot(getBiomass(sim))` | +| `plotYield(sim)` | same as `plot(getYield(sim))` | +| `plotYieldGear(sim)` | like `plotYield()` but keeps the gear dimension, one panel per gear | -```r -plot2(resource_capacity(params), resource_capacity(params2), "Before", "After") -plotRelative(resource_capacity(params), resource_capacity(params2)) -addPlot(plot(NResource(params)), resource_capacity(params)) -``` +**Against body size.** By default these show the final time step; use +`time_range` to average over a period. -A resource array holds a single spectrum, so `species`, `total` and `background` -do nothing there and warn if you set them, and `size_axis = "l"` is unavailable -because the weight-length relationship is a species parameter. +| Function | How it relates to plotting the array directly | +|---|---| +| `plotFeedingLevel(sim)` | same as `plot(getFeedingLevel(sim))` | +| `plotPredMort(sim)` | same as `plot(getPredMort(sim))` | +| `plotFMort(sim)` | same as `plot(getFMort(sim))` | +| `plotGrowthCurves(sim)` | a distinct plot: size at age rather than a size spectrum | +| `plotDiet(params)` | a distinct plot: stacked diet composition by prey | -Time-resolved resource data (`NResource(sim)`) is an `ArrayTimeByResourceBySize`, -which `animate()` can play through time, and which the comparison functions above -slice at a chosen `time`. To include the resource in a species spectrum plot, -pass `resource = TRUE` (supported by `plotSpectra()`, `plotCDF()`, and friends). +**Calibration:** `plotBiomassObservedVsModel(params)` and +`plotYieldObservedVsModel(params)`; the latter takes a `gear` argument that +restricts both the modelled and the observed catch to the named gears. See the +`calibrate-model` skill. ```r -animate(NResource(sim)) # resource spectrum over time -plot2(NResource(sim), NResource(sim2), time = 1990) +plotBiomass(sim, species = c("Cod", "Herring"), total = TRUE) +plotFMort(sim, time_range = 1990:2000) +plotGrowthCurves(sim, species = "Cod", max_age = 20) +plotDiet(params, species = "Cod") ``` +**Overview:** `plot(sim)` combines several panels; `plot(params)` shows the same +panels for a model's steady state (without the biomass-through-time panel). + ## Working with ggplot2 All plotting functions return a ggplot2 object, so you can customise them: diff --git a/inst/skills/analyse-and-plot/quick-reference.md b/inst/skills/analyse-and-plot/quick-reference.md index a0e68c766..5678e415b 100644 --- a/inst/skills/analyse-and-plot/quick-reference.md +++ b/inst/skills/analyse-and-plot/quick-reference.md @@ -38,27 +38,6 @@ ArraySpeciesBySize(x, params = params, representation = "average") # size-resol bin_average_weight(K, params) # the primitive, if you are not doing an integral encounter_kernel(params) # kernel getEncounter() uses; NOT pred_kernel() -# ── Dedicated plot functions ────────────────────────────────────────────────── -# Each plot*() is a shortcut for plot() on the matching get*() array, and each has -# an interactive plotly*() twin (plotlyBiomass(), plotlySpectra(), …). -plot(sim) # 5-panel summary -plotBiomass(sim) # biomass vs time -plotYield(sim) # yield vs time -plotYieldGear(sim) # yield vs time, faceted by gear -plotSpectra(sim) # abundance spectra vs size (+ resource & background) -plotFeedingLevel(sim) # feeding level vs size -plotPredMort(sim) # predation mortality vs size -plotFMort(sim) # fishing mortality vs size -plotGrowthCurves(sim) # size vs age -plotDiet(params, species = "Cod") # diet composition vs size -plotCDF(sim) # cumulative biomass/abundance over size - -# ── Choosing what a spectrum plot shows ─────────────────────────────────────── -plotSpectra(sim, biomass = TRUE) # biomass rather than number -plotSpectra(sim, per_log_size = TRUE) # density per log size -plotSpectra(sim, size_axis = "l") # x axis in length, not weight -plotSpectra(sim, log_x = TRUE) # display only: does NOT change the y density - # ── Plot any array directly, plus combine / compare tools ───────────────────── plot(getResourceMort(params)) # any get*() array plots directly p <- plot(getBiomass(sim), species = "Cod") @@ -66,10 +45,31 @@ addPlot(p, getBiomass(sim), species = "Herring", linetype = "dashed") # add lin plot2(getFMort(params), getFMort(params2), "Before", "After") # compare arrays plotRelative(getEGrowth(params), getEGrowth(params2)) # relative diff plotHover(getBiomass(sim)) # interactive (hover) version of an array plot -animate(sim) # animate spectra through time + +# ── Size spectra and other densities ────────────────────────────────────────── +plotSpectra(sim) # abundance spectra vs size (+ resource & background) +plotCDF(sim) # cumulative biomass/abundance over size +animate(sim) # animate spectra through time +plotSpectra(sim, biomass = TRUE) # biomass rather than number +plotSpectra(sim, per_log_size = TRUE) # density per log size +plotSpectra(sim, size_axis = "l") # x axis in length, not weight +plotSpectra(sim, log_x = TRUE) # display only: does NOT change the y density # ── Compare two simulations or models ───────────────────────────────────────── plotSpectra2(params, params2, "Before", "After") plotSpectraRelative(params, params2) # relative difference of spectra plotCDF2(sim, sim2, "Unfished", "Fished") + +# ── Dedicated plot functions ────────────────────────────────────────────────── +# Each plot*() is a shortcut for plot() on the matching get*() array, and each has +# an interactive plotly*() twin (plotlyBiomass(), plotlySpectra(), …). +plot(sim) # 5-panel summary +plotBiomass(sim) # biomass vs time +plotYield(sim) # yield vs time +plotYieldGear(sim) # yield vs time, faceted by gear +plotFeedingLevel(sim) # feeding level vs size +plotPredMort(sim) # predation mortality vs size +plotFMort(sim) # fishing mortality vs size +plotGrowthCurves(sim) # size vs age +plotDiet(params, species = "Cod") # diet composition vs size ``` diff --git a/inst/skills/upgrade-mizer-code/SKILL.md b/inst/skills/upgrade-mizer-code/SKILL.md index a78fb352c..f45f82695 100644 --- a/inst/skills/upgrade-mizer-code/SKILL.md +++ b/inst/skills/upgrade-mizer-code/SKILL.md @@ -45,6 +45,13 @@ plots) are in the changelog and are not repeated here. | `plotSpectra()` or `plotCDF()` errors that `power` and `biomass` are contradictory | supplying both is no longer silently resolved | `biomass` and `per_log_size` replace `power` (3.3) | | A `plotSpectra()` call with both `power` and `biomass`, or any `plotly...()` call with `biomass`, now gives a different plot | `biomass` is no longer ignored | `biomass` and `per_log_size` replace `power` (3.3) | | `plotCDF(per_log_size = TRUE)` errors | meaningless for a cumulative distribution | `biomass` and `per_log_size` replace `power` (3.3) | +| `plot(getFluxGradient(...), size_axis = "l")` gives different values, or a `cm^-1/year` label where it used to say `g^-1/year` | the flux gradient is a density and was not recognised as one | Arrays say what kind of value they hold (3.3) | +| `plot()` of a feeding level, maturity, `psi()` or resource level has a y axis running from 0 to 1 where it used to fit the data | these arrays now declare themselves proportions | Arrays say what kind of value they hold (3.3) | +| `plot(resource_level(params))` has a linear y axis where it used to be logarithmic | a proportion is plotted on a linear axis by default | Arrays say what kind of value they hold (3.3) | +| `plotFeedingLevel(include_critical = TRUE)` shows a critical feeding level peak that used to be cut off at 1 | the fixed [0, 1] window is now widened to fit the data | Arrays say what kind of value they hold (3.3) | +| A custom array plotted with `size_axis = "l"` is not transformed, or is transformed when it should not be | an array declares what it holds with `type` now | Arrays say what kind of value they hold (3.3) | +| `array_spectrum_power()`, or `spectrum_power =` in an internal plot helper, is no longer found | replaced by the `type` metadata | Arrays say what kind of value they hold (3.3) | +| New warning that two arrays hold "a value of type" different things in `plot2()` or `plotRelative()` | the two arrays disagree about what they hold | Arrays say what kind of value they hold (3.3) | | New warning that a change to a species or resource parameter "has not taken effect" | the rate it feeds was set by hand and is no longer calculated | A change that cannot take effect now warns (3.3) | | That warning appears with `given_species_params<-()` but not with `species_params<-()` | the diagnostics belong to the given species parameter setter | The two species parameter setters divide the diagnostics between them (3.3) | | Setting a given species parameter to `NA` now warns that the change has not taken effect | clearing a value counts as a change, and a frozen array blocks it | The two species parameter setters divide the diagnostics between them (3.3) | @@ -181,6 +188,57 @@ things change: integrating a density over size gives the same cumulative quantity either way. Use `biomass` on its own there. +### Arrays say what kind of value they hold + +Mizer arrays now carry a `type` attribute saying what kind of quantity their +values are: `"value"` (the default) for a rate or an amount, `"density"` for an +amount per gram of body weight, `"proportion"` for a fraction. Two kinds of +plotting behaviour follow from it, and both used to be decided some other way. + +**Densities.** Plotting a density against a length axis (`size_axis = "l"`) has +to multiply the values by a Jacobian, because a density per gram is not a +density per centimetre. mizer used to decide which arrays those were by looking +at their metadata strings, treating an array as a density if it was named +`"Number density"` or had units `"1/g"`. For mizer's own number spectra — +`initialN()`, `N()`, `finalN()`, `NResource()`, `resource_capacity()` — nothing +changes; they were recognised before and are tagged now. What changes is +`getFluxGradient()`: it is a rate of change of a number density, with units +`g^-1/year`, and neither of the old string tests recognised it, so on a length +axis its values were left as densities per gram and were mislabelled as such. +They are now converted with the `dw/dl = b w / l` Jacobian and labelled +`cm^-1/year`. The new curve is the right one; if you were reading values off the +old one, they were per gram plotted against length. + +**Proportions.** `getFeedingLevel()`, `getCriticalFeedingLevel()`, `maturity()`, +`repro_prop()`, `psi()` and `resource_level()` now declare themselves +proportions, and a plot of one shows the whole of the interval from 0 to 1 on a +linear y axis, so the value can be read against the scale it belongs to. Three +consequences: + +- `plot(getFeedingLevel(params))` and the other array plots gain that y range, + where they used to fit the axis to the data. This is the range + `plotFeedingLevel()` has always shown, so the dedicated function and the array + plot now agree. +- `plot(resource_level(params))` gets a linear y axis instead of a logarithmic + one. Pass `log_y = TRUE` to get the old axis back; any explicit `log_y` or + `log` you already pass is respected. +- The range is only ever *widened* to include the data, never narrowed to the + interval from 0 to 1. So `plotFeedingLevel(include_critical = TRUE)` now shows + a critical feeding level above 1, which the old fixed window drew off the top + of the plot. Nothing is ever hidden, and an explicit `ylim` still wins. + +**Declaring it yourself.** An array of your own is taken to be a density or a +proportion only if you say so, by passing `type` to the array constructor. If +you do not pass it, the old string tests still run as a fallback, so existing +code that named an array `"Number density"` or gave it units `"1/g"` keeps +working, and arrays saved by earlier versions keep working when they are loaded. + +Extension packages that called the unexported plotting helpers directly should +note that `plotComparisonDataFrame()` and the internal `animate_plotly()` take a +single `density_wrt` argument in place of `spectrum_power` and +`spectrum_per_log_size`, and that the internal `array_spectrum_power()` is gone. +The `power`-based interface of `plotSpectra()` and friends is unchanged. + ### Length and weight parameters follow the one you gave last A size can be given either as a weight (`w_mat`, `w_max`, …) or as the length it diff --git a/man/ArrayResourceBySize.Rd b/man/ArrayResourceBySize.Rd index 57d3834f0..b1bfcabf7 100644 --- a/man/ArrayResourceBySize.Rd +++ b/man/ArrayResourceBySize.Rd @@ -5,7 +5,13 @@ \alias{is.ArrayResourceBySize} \title{S3 class for resource size spectra} \usage{ -ArrayResourceBySize(x, value_name = NULL, units = NULL, params = NULL) +ArrayResourceBySize( + x, + value_name = NULL, + units = NULL, + type = NULL, + params = NULL +) is.ArrayResourceBySize(x) } @@ -17,6 +23,9 @@ is.ArrayResourceBySize(x) \item{units}{A string giving the units (e.g. "1/year").} +\item{type}{The kind of quantity the values are, see \code{\link[=ArraySpeciesBySize]{ArraySpeciesBySize()}} +and \link{array_types}.} + \item{params}{A \code{MizerParams} object. Used for the resource colour and the size grid in the \code{plot()} method.} } diff --git a/man/ArraySpeciesBySize.Rd b/man/ArraySpeciesBySize.Rd index 05c69ea9a..f3b19a102 100644 --- a/man/ArraySpeciesBySize.Rd +++ b/man/ArraySpeciesBySize.Rd @@ -9,6 +9,7 @@ ArraySpeciesBySize( x, value_name = NULL, units = NULL, + type = NULL, params = NULL, representation = c("point", "average") ) @@ -23,6 +24,15 @@ object to test.} \item{units}{A string giving the units (e.g. "g/year", "1/year").} +\item{type}{The kind of quantity the values are, see \link{array_types}: +\code{"value"} (the default) for a rate or an amount, \code{"density"} for an amount +per gram of body weight, \code{"proportion"} for a fraction. This is what tells +\code{plot()} to multiply a density by the appropriate Jacobian when it is +plotted against a length axis (\code{size_axis = "l"}), and to show a proportion +against the whole of the interval from 0 to 1. The default, \code{NULL}, treats +a \code{value_name} of \code{"Number density"} or units of \code{"1/g"} as a density, the +way mizer recognised one before this attribute existed.} + \item{params}{A \code{MizerParams} object. Used for species colours, linetypes, and size ranges in the \code{plot()} method.} @@ -46,11 +56,13 @@ The \code{ArraySpeciesBySize} class wraps these arrays to provide convenient } \details{ An \code{ArraySpeciesBySize} object behaves just like a regular matrix for -arithmetic operations and subsetting. It carries two lightweight attributes: +arithmetic operations and subsetting. It carries a few lightweight +attributes: \itemize{ \item \code{value_name} – a human-readable name for the value (e.g. "Encounter rate"). \item \code{units} – the units of the rate (e.g. "g/year"). +\item \code{type} – the kind of quantity the values are. } } \examples{ diff --git a/man/ArrayTimeByResourceBySize.Rd b/man/ArrayTimeByResourceBySize.Rd index cf108a7a1..91711bfd2 100644 --- a/man/ArrayTimeByResourceBySize.Rd +++ b/man/ArrayTimeByResourceBySize.Rd @@ -5,7 +5,13 @@ \alias{is.ArrayTimeByResourceBySize} \title{S3 class for time x resource-size arrays} \usage{ -ArrayTimeByResourceBySize(x, value_name = NULL, units = NULL, params = NULL) +ArrayTimeByResourceBySize( + x, + value_name = NULL, + units = NULL, + type = NULL, + params = NULL +) is.ArrayTimeByResourceBySize(x) } @@ -17,6 +23,9 @@ object to test.} \item{units}{A string giving the units (e.g. "1/g").} +\item{type}{The kind of quantity the values are, see \code{\link[=ArraySpeciesBySize]{ArraySpeciesBySize()}} +and \link{array_types}.} + \item{params}{A \code{MizerParams} object. Used for the resource colour and the size grid in the \code{plot()} method.} } diff --git a/man/ArrayTimeBySpecies.Rd b/man/ArrayTimeBySpecies.Rd index 9a61be9e6..9753b5d51 100644 --- a/man/ArrayTimeBySpecies.Rd +++ b/man/ArrayTimeBySpecies.Rd @@ -5,7 +5,13 @@ \alias{is.ArrayTimeBySpecies} \title{S3 class for time x species arrays} \usage{ -ArrayTimeBySpecies(x, value_name = NULL, units = NULL, params = NULL) +ArrayTimeBySpecies( + x, + value_name = NULL, + units = NULL, + type = NULL, + params = NULL +) is.ArrayTimeBySpecies(x) } @@ -17,6 +23,9 @@ object to test.} \item{units}{A string giving the units (e.g. "g", "g/year").} +\item{type}{The kind of quantity the values are, see \code{\link[=ArraySpeciesBySize]{ArraySpeciesBySize()}} +and \link{array_types}.} + \item{params}{A \code{MizerParams} object holding the model that created the values.} } diff --git a/man/ArrayTimeBySpeciesBySize.Rd b/man/ArrayTimeBySpeciesBySize.Rd index f565fa890..900aad7fb 100644 --- a/man/ArrayTimeBySpeciesBySize.Rd +++ b/man/ArrayTimeBySpeciesBySize.Rd @@ -9,6 +9,7 @@ ArrayTimeBySpeciesBySize( x, value_name = NULL, units = NULL, + type = NULL, params = NULL, representation = c("point", "average") ) @@ -23,6 +24,9 @@ is.ArrayTimeBySpeciesBySize(x) \item{units}{A string giving the units (e.g. "1/year").} +\item{type}{The kind of quantity the values are, see \code{\link[=ArraySpeciesBySize]{ArraySpeciesBySize()}} +and \link{array_types}.} + \item{params}{A \code{MizerParams} object. Used for species colours, linetypes, and size ranges in the \code{plot()} and \code{animateSpectra()} methods.} @@ -52,6 +56,7 @@ arithmetic operations and subsetting. It carries these lightweight attributes: \item \code{value_name} – a human-readable name for the value (e.g. "Fishing mortality"). \item \code{units} – the units of the value (e.g. "1/year"). +\item \code{type} – the kind of quantity the values are. \item \code{params} – the \code{MizerParams} object that the value was computed from. } } diff --git a/man/array_density_wrt.Rd b/man/array_density_wrt.Rd new file mode 100644 index 000000000..752441d0a --- /dev/null +++ b/man/array_density_wrt.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ArraySpeciesBySize-class.R +\name{array_density_wrt} +\alias{array_density_wrt} +\title{The density measure of a mizer array} +\usage{ +array_density_wrt(x) +} +\arguments{ +\item{x}{A mizer array object.} +} +\value{ +\code{"w"} if the array holds a density, otherwise \code{NA_character_}. +} +\description{ +The bridge from the array metadata into the density machinery of the plots. +Mizer arrays are indexed by the model's weight grid, so a stored density is +always a density with respect to weight; the other measures in +\link{density_measures} arise only for quantities that the spectrum plots compute +on the fly, such as a density per logarithmic weight. +} +\keyword{internal} diff --git a/man/array_log_y.Rd b/man/array_log_y.Rd new file mode 100644 index 000000000..57e9102bb --- /dev/null +++ b/man/array_log_y.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plots.R +\name{array_log_y} +\alias{array_log_y} +\title{The logarithmic y axis an array's type calls for} +\usage{ +array_log_y(x, log_y, log, given) +} +\arguments{ +\item{x}{A mizer array object.} + +\item{log_y}{The \code{log_y} argument of the plot method.} + +\item{log}{The \code{log} argument of the plot method.} + +\item{given}{Whether the caller supplied \code{log_y} (i.e. \code{!missing(log_y)}).} +} +\value{ +The \code{log_y} to use. +} +\description{ +A proportion belongs on a linear axis, so a plot of one turns \code{log_y} off +unless the caller asked for a particular axis. Called before +\code{\link[=parsePlotLog]{parsePlotLog()}}, which is why it also has to check \code{log}. +} +\keyword{internal} diff --git a/man/array_type.Rd b/man/array_type.Rd new file mode 100644 index 000000000..5082b2a12 --- /dev/null +++ b/man/array_type.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ArraySpeciesBySize-class.R +\name{array_type} +\alias{array_type} +\title{The type of a mizer array} +\usage{ +array_type(x) +} +\arguments{ +\item{x}{A mizer array object.} +} +\value{ +One of \link{array_types}. +} +\description{ +The type of a mizer array +} +\keyword{internal} diff --git a/man/array_types.Rd b/man/array_types.Rd new file mode 100644 index 000000000..f5ad3ad08 --- /dev/null +++ b/man/array_types.Rd @@ -0,0 +1,33 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ArraySpeciesBySize-class.R +\name{array_types} +\alias{array_types} +\title{Kinds of quantity a mizer array can hold} +\format{ +A character vector of the three types. +} +\usage{ +array_types +} +\description{ +Mizer arrays record what kind of quantity their values are in their \code{type} +attribute, because some kinds need handling that the numbers alone do not +reveal: +\describe{ +\item{\code{"value"}}{the default: a rate, an amount, anything that needs no +special handling.} +\item{\code{"density"}}{an amount per gram of body weight, like a number +density. Plotting a density against a length axis restates it per +centimetre, which changes the values and not just the axis.} +\item{\code{"proportion"}}{a fraction, like the feeding level. Plotted on a +linear y axis showing the whole of the interval from 0 to 1, so that the +value can be read against the scale it belongs to.} +} +} +\details{ +A \code{"proportion"} is not \emph{restricted} to the interval from 0 to 1: the +critical feeding level and the resource level can both exceed 1, and their +plots show it. The type is a statement about what the number means, not a +bound that mizer enforces. +} +\keyword{internal} diff --git a/man/array_ylim.Rd b/man/array_ylim.Rd new file mode 100644 index 000000000..c7ffe37cb --- /dev/null +++ b/man/array_ylim.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plots.R +\name{array_ylim} +\alias{array_ylim} +\title{Y-axis limits an array's type calls for} +\usage{ +array_ylim(x, ylim, log_y, values) +} +\arguments{ +\item{x}{A mizer array object.} + +\item{ylim}{Numeric vector of length two, the limits the caller asked for.} + +\item{log_y}{Whether the y axis is logarithmic.} + +\item{values}{The values being plotted.} +} +\value{ +A numeric vector of length two. +} +\description{ +Only a \code{"proportion"} has an opinion. A \code{"density"} is handled where the size +axis is converted, and a \code{"value"} needs nothing. +} +\keyword{internal} diff --git a/man/convert_density_units.Rd b/man/convert_density_units.Rd new file mode 100644 index 000000000..e125dbd2a --- /dev/null +++ b/man/convert_density_units.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plots.R +\name{convert_density_units} +\alias{convert_density_units} +\title{Restate the units of a density in a different density measure} +\usage{ +convert_density_units(units, from, to) +} +\arguments{ +\item{units}{The units of the values, possibly \code{NULL}.} + +\item{from, to}{Density measures, see \link{density_measures}.} +} +\value{ +The units expressed in the \code{to} measure. +} +\description{ +The size unit is swapped inside the two spellings mizer uses for a per-size +factor, \code{1/g} and \code{g^-1}. Units that state no per-size factor are returned +unchanged, since there is then nothing to identify as the size unit. +} +\keyword{internal} diff --git a/man/convert_plot_density_axis.Rd b/man/convert_plot_density_axis.Rd new file mode 100644 index 000000000..b9d4c3960 --- /dev/null +++ b/man/convert_plot_density_axis.Rd @@ -0,0 +1,49 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plots.R +\name{convert_plot_density_axis} +\alias{convert_plot_density_axis} +\title{Express plotting data on the requested size axis} +\usage{ +convert_plot_density_axis( + plot_dat, + params, + size_axis, + density_wrt = NA_character_, + species_col = "Species", + value_col = 2 +) +} +\arguments{ +\item{plot_dat}{A data frame of plotting data with a \code{w} column and a species +column.} + +\item{params}{A MizerParams object providing the weight-length parameters.} + +\item{size_axis}{Either \code{"w"} (weight) or \code{"l"} (length).} + +\item{density_wrt}{The measure the values are a density with respect to, see +\link{density_measures}. \code{NA} (the default) means the values are not a density.} + +\item{species_col}{Name of the column identifying the species. Default is +\code{"Species"}.} + +\item{value_col}{Name or index of the value column. Defaults to the second +column.} +} +\value{ +The plotting data with its size coordinate, and where called for its +values, expressed for the requested axis. The size coordinate is the first +column. +} +\description{ +Converts the size coordinate of the plotting data to the requested axis and, +when the values are a density, multiplies them by the Jacobian that restates +them in the density measure that axis calls for (see +\code{\link[=density_target_measure]{density_target_measure()}}). Values that are not a density are left alone. +} +\details{ +The Jacobian is a per-species quantity, so rows whose species is not one of +the model's species — the "Total" row, for instance — cannot be converted and +are dropped whenever a conversion is needed. +} +\keyword{internal} diff --git a/man/convert_plot_spectrum_axis.Rd b/man/convert_plot_spectrum_axis.Rd index 9d8416dc3..787e16265 100644 --- a/man/convert_plot_spectrum_axis.Rd +++ b/man/convert_plot_spectrum_axis.Rd @@ -31,8 +31,8 @@ logarithmic size rather than with respect to size. Defaults to \item{species_col}{Name of the column identifying the species. Default is \code{"Species"}.} -\item{value_col}{Name or index of the spectrum-value column. Defaults to the -second column.} +\item{value_col}{Name or index of the value column. Defaults to the second +column.} } \value{ The plotting data with both its size coordinate and spectrum values @@ -42,6 +42,8 @@ expressed for the requested axis. A density with respect to weight is converted to a density with respect to length with the Jacobian \code{dw/dl = b * w / l}. A density with respect to logarithmic weight is instead converted with -\verb{d log(w) / d log(l) = b}. +\verb{d log(w) / d log(l) = b}. This is the interface used by the \code{power}-based +spectrum plots; arrays carry their density measure explicitly and use +\code{\link[=convert_plot_density_axis]{convert_plot_density_axis()}} instead. } \keyword{internal} diff --git a/man/density_measure_jacobian.Rd b/man/density_measure_jacobian.Rd new file mode 100644 index 000000000..d91529cba --- /dev/null +++ b/man/density_measure_jacobian.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plots.R +\name{density_measure_jacobian} +\alias{density_measure_jacobian} +\title{Jacobian converting between two density measures} +\usage{ +density_measure_jacobian(from, to, w, l, b) +} +\arguments{ +\item{from, to}{Density measures, see \link{density_measures}.} + +\item{w, l, b}{Numeric vectors of the same length giving the weight, the +corresponding length, and the exponent of the weight-length relationship.} +} +\value{ +A numeric vector by which to multiply a density with respect to +\code{from} to obtain the density with respect to \code{to}. +} +\description{ +Jacobian converting between two density measures +} +\keyword{internal} diff --git a/man/density_measure_weight.Rd b/man/density_measure_weight.Rd new file mode 100644 index 000000000..32f84853e --- /dev/null +++ b/man/density_measure_weight.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plots.R +\name{density_measure_weight} +\alias{density_measure_weight} +\title{Factor relating a density measure to a density with respect to weight} +\usage{ +density_measure_weight(measure, w, l, b) +} +\arguments{ +\item{measure}{One of \link{density_measures}.} + +\item{w, l, b}{Numeric vectors of the same length giving the weight, the +corresponding length, and the exponent of the weight-length relationship.} +} +\value{ +A numeric vector of factors. +} +\description{ +Writing \eqn{N_w} for the density with respect to weight, the density with +respect to measure \eqn{m} is \eqn{N_w} times the factor returned here. With +the allometric weight-length relationship \eqn{w = a l^b} these factors are +\eqn{1} for \code{"w"}, \eqn{w} for \code{"log_w"}, \eqn{dw/dl = b w / l} for \code{"l"} +and \eqn{l\,dw/dl = b w} for \code{"log_l"}. +} +\keyword{internal} diff --git a/man/density_measures.Rd b/man/density_measures.Rd new file mode 100644 index 000000000..a5b37ea33 --- /dev/null +++ b/man/density_measures.Rd @@ -0,0 +1,33 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plots.R +\name{density_measures} +\alias{density_measures} +\title{Density measures a spectrum can be expressed in} +\format{ +A character vector of the four density measures. +} +\usage{ +density_measures +} +\description{ +A size spectrum is a density, and a density only has a meaning together with +the variable it is a density with respect to. That variable is one of +\describe{ +\item{\code{"w"}}{a density with respect to weight, e.g. numbers per gram.} +\item{\code{"log_w"}}{a density with respect to logarithmic weight, e.g. +numbers per log weight interval.} +\item{\code{"l"}}{a density with respect to length, e.g. numbers per cm.} +\item{\code{"log_l"}}{a density with respect to logarithmic length.} +\item{\code{NA}}{not a density, e.g. a rate or a dimensionless quantity. Such +values are left alone when the size axis changes.} +} +} +\details{ +Mizer arrays are indexed by the model's weight grid, so an array that holds +a density (\code{type = "density"}, see \link{array_types}) always holds one with +respect to weight. The other measures arise for quantities the spectrum plots +compute on the fly: \code{plotSpectra(per_log_size = TRUE)} shows a density with +respect to logarithmic weight, and either can be restated per unit length by +\code{size_axis = "l"}. +} +\keyword{internal} diff --git a/man/density_size_unit.Rd b/man/density_size_unit.Rd new file mode 100644 index 000000000..f4a943d4c --- /dev/null +++ b/man/density_size_unit.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plots.R +\name{density_size_unit} +\alias{density_size_unit} +\title{The size unit appearing in the units of a density} +\usage{ +density_size_unit(measure) +} +\arguments{ +\item{measure}{One of \link{density_measures}.} +} +\value{ +\code{"g"} or \code{"cm"}, or \code{NA_character_} for a density with respect to a +logarithmic size, whose units carry no size unit. +} +\description{ +The size unit appearing in the units of a density +} +\keyword{internal} diff --git a/man/density_target_measure.Rd b/man/density_target_measure.Rd new file mode 100644 index 000000000..bf56386b2 --- /dev/null +++ b/man/density_target_measure.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plots.R +\name{density_target_measure} +\alias{density_target_measure} +\title{The density measure a plot against a given size axis calls for} +\usage{ +density_target_measure(density_wrt, size_axis) +} +\arguments{ +\item{density_wrt}{The measure the values are a density with respect to, see +\link{density_measures}.} + +\item{size_axis}{Either \code{"w"} (weight) or \code{"l"} (length).} +} +\value{ +The density measure to express the values in, or \code{NA_character_} if +the values are not a density. +} +\description{ +Plotting against a length axis turns a density with respect to weight into a +density with respect to length, and a density with respect to logarithmic +weight into one with respect to logarithmic length. Whether the density is +per size or per logarithmic size is a property of the values and is left +alone; only the size variable follows the axis. +} +\keyword{internal} diff --git a/man/getMeanWeight.Rd b/man/getMeanWeight.Rd index c1a6f5b4d..b99773dbd 100644 --- a/man/getMeanWeight.Rd +++ b/man/getMeanWeight.Rd @@ -32,10 +32,10 @@ A vector containing the mean weight of the community through time, or a single value if called with a \code{MizerParams} object. } \description{ -Calculates the mean weight of the community. This is simply the total -biomass of the community divided by the abundance in numbers. You can -specify minimum and maximum weight or length range for the species. Lengths -take precedence over weights (i.e. if both min_l and min_w are supplied, only +Calculates the mean weight of the community. This is simply the total biomass +of the community divided by the abundance in numbers. You can specify minimum +and maximum weight or length for the included size range. Lengths take +precedence over weights (i.e. if both min_l and min_w are supplied, only min_l will be used). You can also specify the species to be used in the calculation. } diff --git a/man/get_species_size_rate_from_sim.Rd b/man/get_species_size_rate_from_sim.Rd index 8e6315e68..a701a23cb 100644 --- a/man/get_species_size_rate_from_sim.Rd +++ b/man/get_species_size_rate_from_sim.Rd @@ -11,6 +11,7 @@ get_species_size_rate_from_sim( rate_fun, value_name, units = NULL, + type = NULL, representation = "point" ) } @@ -27,6 +28,9 @@ get_species_size_rate_from_sim( \item{value_name}{Name of the value stored in the returned array.} \item{units}{Optional units of the value stored in the returned array.} + +\item{type}{The kind of quantity the values are, see \code{\link[=ArraySpeciesBySize]{ArraySpeciesBySize()}} +and \link{array_types}.} } \value{ A time x species x size array, possibly with dimensions dropped. diff --git a/man/plotComparisonDataFrame.Rd b/man/plotComparisonDataFrame.Rd index 3ae061429..836bb3e8f 100644 --- a/man/plotComparisonDataFrame.Rd +++ b/man/plotComparisonDataFrame.Rd @@ -20,8 +20,7 @@ plotComparisonDataFrame( highlight = NULL, legend_var = "Legend", size_axis = NULL, - spectrum_power = NULL, - spectrum_per_log_size = NULL + density_wrt = NA_character_ ) } \arguments{ @@ -51,12 +50,9 @@ line colour.} \item{size_axis}{Optional. If non-NULL, the x-axis is converted to weight (\code{"w"}) or length (\code{"l"}).} -\item{spectrum_power}{Optional power used to weight a number spectrum. When -supplied, spectrum values are transformed along with a length axis.} - -\item{spectrum_per_log_size}{Whether the spectrum is a density with respect -to logarithmic size, which selects the Jacobian used for a length axis. -Defaults to \code{spectrum_power == 2}.} +\item{density_wrt}{The measure the values are a density with respect to, see +\link{density_measures}. \code{NA} (the default) means the values are not a density +and are left alone when the size axis changes.} } \value{ A \code{mizer_plot} (ggplot2) object. diff --git a/man/proportion_ylim.Rd b/man/proportion_ylim.Rd new file mode 100644 index 000000000..1b9c08890 --- /dev/null +++ b/man/proportion_ylim.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plots.R +\name{proportion_ylim} +\alias{proportion_ylim} +\title{Y-axis limits for a plot of a proportion} +\usage{ +proportion_ylim(ylim, log_y, values) +} +\arguments{ +\item{ylim}{Numeric vector of length two, the limits the caller asked for.} + +\item{log_y}{Whether the y axis is logarithmic.} + +\item{values}{The values being plotted.} +} +\value{ +A numeric vector of length two. +} +\description{ +A proportion is easiest to read against the whole of the interval from 0 to +1, so that is the range a plot of one shows by default. The range is only +ever \emph{widened} to include the data, never narrowed to the interval: a +critical feeding level or a resource level above 1 is a real feature of the +model and must stay visible. +} +\details{ +Only the ends of \code{ylim} that the caller left as \code{NA} are filled in, so an +explicit limit always wins. A logarithmic axis is left alone, having no place +for the 0. +} +\keyword{internal} diff --git a/man/resolve_array_type.Rd b/man/resolve_array_type.Rd new file mode 100644 index 000000000..a65b9d025 --- /dev/null +++ b/man/resolve_array_type.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ArraySpeciesBySize-class.R +\name{resolve_array_type} +\alias{resolve_array_type} +\title{Resolve the type of a mizer array} +\usage{ +resolve_array_type(type, value_name = NULL, units = NULL) +} +\arguments{ +\item{type}{The type supplied to the constructor, or \code{NULL}.} + +\item{value_name}{The \code{value_name} of the array.} + +\item{units}{The \code{units} of the array.} +} +\value{ +One of \link{array_types}. +} +\description{ +Called by the array constructors. An explicit \code{type} is validated and used as +given; \code{NULL} means the constructor was called without the argument, in which +case a density is recognised from the other metadata, the way mizer +recognised one before the \code{type} attribute existed. That keeps arrays built +by extension packages, and arrays saved by earlier versions, behaving as they +did. +} +\keyword{internal} diff --git a/man/sim_size_rate.Rd b/man/sim_size_rate.Rd index e49ba6916..32db8087d 100644 --- a/man/sim_size_rate.Rd +++ b/man/sim_size_rate.Rd @@ -12,6 +12,7 @@ sim_size_rate( slot, value_name, units = NULL, + type = NULL, use_sim_effort = FALSE, representation = "point", ... diff --git a/man/spectrum_density_wrt.Rd b/man/spectrum_density_wrt.Rd new file mode 100644 index 000000000..c2205ce99 --- /dev/null +++ b/man/spectrum_density_wrt.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plots.R +\name{spectrum_density_wrt} +\alias{spectrum_density_wrt} +\title{The density measure of a \code{power}-based spectrum} +\usage{ +spectrum_density_wrt(per_log_size) +} +\arguments{ +\item{per_log_size}{Whether the spectrum is a density with respect to +logarithmic size.} +} +\value{ +\code{"log_w"} or \code{"w"}. +} +\description{ +The density measure of a \code{power}-based spectrum +} +\keyword{internal} diff --git a/man/validate_array_type.Rd b/man/validate_array_type.Rd new file mode 100644 index 000000000..d1701086c --- /dev/null +++ b/man/validate_array_type.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ArraySpeciesBySize-class.R +\name{validate_array_type} +\alias{validate_array_type} +\title{Validate the type of a mizer array} +\usage{ +validate_array_type(type) +} +\arguments{ +\item{type}{One of \link{array_types}.} +} +\value{ +The validated type. +} +\description{ +Validate the type of a mizer array +} +\keyword{internal} diff --git a/man/validate_density_wrt.Rd b/man/validate_density_wrt.Rd new file mode 100644 index 000000000..6fcddef90 --- /dev/null +++ b/man/validate_density_wrt.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plots.R +\name{validate_density_wrt} +\alias{validate_density_wrt} +\title{Validate a density measure} +\usage{ +validate_density_wrt(density_wrt) +} +\arguments{ +\item{density_wrt}{A density measure, see \link{density_measures}. \code{NULL} and \code{NA} +both stand for "not a density".} +} +\value{ +The validated measure, or \code{NA_character_} when the values are not a +density. +} +\description{ +Validate a density measure +} +\keyword{internal} diff --git a/tests/testthat/_snaps/diffusion.md b/tests/testthat/_snaps/diffusion.md index cc8baafac..5de207595 100644 --- a/tests/testthat/_snaps/diffusion.md +++ b/tests/testthat/_snaps/diffusion.md @@ -8,11 +8,6 @@ "attributes": {}, "value": [3, 100] }, - "representation": { - "type": "character", - "attributes": {}, - "value": ["point"] - }, "dimnames": { "type": "list", "attributes": { @@ -49,6 +44,16 @@ "type": "character", "attributes": {}, "value": ["g^2/year"] + }, + "type": { + "type": "character", + "attributes": {}, + "value": ["value"] + }, + "representation": { + "type": "character", + "attributes": {}, + "value": ["point"] } }, "value": [9.48647462e-08, 3656.95994306, 1.69732512e-06, 1.8570558e-07, 4121.12704495, 2.27934255e-06, 1.20210149e-07, 4644.09696314, 2.30819663e-06, 1.96761485e-07, 5233.30149599, 3.29545175e-06, 1.36495081e-07, 5897.10790712, 4.14760577e-06, 3.26088307e-07, 6644.93557076, 4.96890033e-06, 2.198273e-07, 7487.38677913, 6.27760774e-06, 4.1634069e-07, 8436.39328597, 7.00889449e-06, 3.6659511e-07, 9505.38027589, 6.02946004e-06, 5.46847612e-07, 10709.44954671, 7.12655572e-06, 4.8533169e-07, 12065.58375461, 7.50981695e-06, 7.09820358e-07, 13592.87357694, 9.2956858e-06, 4.34032512e-07, 15312.76956019, 0.00001045, 5.11798495e-07, 17249.36019014, 0.00001166, 7.76370168e-07, 19429.67727569, 0.00001224, 9.0404828e-07, 21884.0289711, 0.00001136, 1.25162577e-06, 24646.35952373, 9.40439436e-06, 1.81788876e-06, 27754.63290923, 0.00001218, 2.81715493e-06, 31251.23459687, 0.00001576, 3.7385721e-06, 35183.38134033, 0.00001739, 5.22963502e-06, 39603.52250057, 0.00002734, 6.91357672e-06, 44569.70712479, 0.00002849, 6.87630941e-06, 50145.87765436, 0.0000243, 7.69397167e-06, 56402.03211243, 0.00001437, 4.5208836e-06, 63414.16981689, 3.21108664e-06, 2.99150658e-06, 71263.89836209, 7.67315505e-07, 4.10472235e-06, 80037.52850687, 0, 5.39955808e-06, 89824.41508864, 0, 4.84135242e-06, 100714.21286143, 0, 3.64040562e-06, 112792.60499975, 0, 7.1377219e-06, 126134.93294671, 0, 8.22199749e-06, 140797.02446136, 0, 5.6825898e-06, 156802.41855014, 0, 0, 174125.19357512, 0, 0, 192667.84440614, 0, 0, 212234.32139214, 0, 0.00002668, 232499.69319134, 0, 0, 252980.17470419, 7.83014041e-06, 0.0000885, 273010.50545249, 0.00026461, 0.00004078, 291739.37661047, 0.00062369, 0.00004697, 308156.27428242, 0.00142417, 0, 321162.06003233, 0.00276436, 0, 329687.77166604, 0.00561668, 0, 332850.45380998, 0.01145955, 0, 330114.69442355, 0.02313852, 0, 321414.58184301, 0.04805541, 0, 307193.40132741, 0.09469173, 0.0000632, 288342.60756965, 0.17693044, 0, 266056.68294459, 0.31663261, 0, 241647.80379745, 0.65377285, 0, 216373.65183291, 1.23478416, 0, 191310.1443561, 2.32193392, 0, 167288.1338898, 4.25923511, 0, 144881.39181829, 7.54517712, 0, 124431.67820451, 14.79357127, 0, 106093.95421592, 29.92728692, 0, 89885.55303012, 54.76284952, 0.00010561, 75724.11839802, 112.93231093, 0, 63472.68137578, 212.45721693, 0, 52958.0026282, 423.94037361, 0, 43997.20778711, 813.81701792, 0, 36409.26551023, 1385.63055553, 0, 30015.91230687, 2348.78994926, 0, 24653.24610125, 4284.41430115, 0, 20186.34323212, 6397.61268657, 0, 16480.47125505, 10364.30567096, 0, 13435.27159266, 15902.04261839, 0, 10977.13151455, 24742.96702238, 0, 9060.17991008, 36091.49758579, 0, 7655.040591, 49612.72776351, 0, 7003.29985308, 72812.36605259, 0, 7340.24343354, 105210.64652445, 0, 8991.95237917, 143122.9640039, 0, 12827.78509116, 194908.12289064, 0.00407014, 23049.96592118, 270324.60906555, 0.00135328, 35669.85266084, 355873.55659716, 0.0046261, 56092.76507761, 463679.53316835, 0.00277392, 113847.88707591, 646499.20131023, 0.00808105, 168882.6595434, 835901.85342547, 0.00750343, 276724.27678897, 1126343.39646072, 0.0151086, 442030.22444935, 1461001.30935027, 0.01657659, 753543.49523169, 2050533.01177798, 0.03255914, 1263139.25184622, 2849981.61998694, 0.04746652, 1774638.87153815, 3738568.42258159, 0.08699178, 2467922.33911121, 4894455.97080318, 0.15934073, 3806381.11405868, 7082729.63278195, 0.28480191, 5089221.54201819, 9535476.80303147, 0.53238125, 6172262.65658001, 11915575.47417483, 1.01445287, 8484398.68823839, 16150649.17140858, 1.96516509, 12197743.98676105, 22478586.69232488, 3.79069138, 18184809.84260187, 32314359.24083617, 7.32252201, 24051403.53403921, 43000555.42258975, 14.19997282, 29759173.0631741, 55134957.51526751, 27.51659558, 38419048.40017083, 70672475.5503502, 53.18498376, 52856417.44448193, 96931301.85960405, 102.60567529, 68578390.68362136, 126259537.96633053, 197.14421668, 91309325.29216686, 171106005.34029698, 377.7469003, 114532908.85634583, 218327258.55163282, 720.83355549, 162943543.47969717, 292841600.09714305, 1370.11185876, 200282120.10402918, 369497077.84643334] diff --git a/tests/testthat/_snaps/rate_functions.md b/tests/testthat/_snaps/rate_functions.md index 544424081..a9b6069e4 100644 --- a/tests/testthat/_snaps/rate_functions.md +++ b/tests/testthat/_snaps/rate_functions.md @@ -8,11 +8,6 @@ "attributes": {}, "value": [3, 100] }, - "representation": { - "type": "character", - "attributes": {}, - "value": ["point"] - }, "dimnames": { "type": "list", "attributes": { @@ -44,6 +39,16 @@ "type": "character", "attributes": {}, "value": ["Feeding level"] + }, + "type": { + "type": "character", + "attributes": {}, + "value": ["proportion"] + }, + "representation": { + "type": "character", + "attributes": {}, + "value": ["point"] } }, "value": [0, 0.99978275, 4.17764441e-10, 0, 0.99975286, 3.72919719e-10, 0, 0.99971801, 3.84977524e-10, 0, 0.99967727, 3.56977063e-10, 0, 0.99962953, 4.22013329e-10, 0, 0.99957346, 4.28706391e-10, 0, 0.99950742, 4.86134069e-10, 6.73028733e-12, 0.99942945, 5.41704765e-10, 4.56752321e-12, 0.99933715, 6.61388086e-10, 9.22008312e-11, 0.9992276, 7.75291598e-10, 3.01997356e-11, 0.99909725, 1.26970254e-09, 1.50495613e-10, 0.99894174, 1.72847987e-09, 3.87576224e-11, 0.99875573, 2.55380344e-09, 1.036148e-10, 0.99853266, 3.66916503e-09, 9.26085775e-11, 0.99826446, 5.63150277e-09, 1.75987414e-10, 0.99794113, 8.67797041e-09, 1.01988704e-10, 0.99755036, 1.29154126e-08, 2.14204009e-10, 0.99707684, 1.84117697e-08, 2.0483799e-10, 0.99650161, 2.86044791e-08, 2.49494426e-10, 0.99580106, 4.29439552e-08, 2.80090147e-10, 0.99494576, 6.50633038e-08, 3.29892602e-10, 0.99389902, 9.89694832e-08, 2.39546596e-10, 0.99261499, 1.39609808e-07, 2.57677715e-10, 0.99103636, 2.11507481e-07, 6.42918608e-11, 0.98909142, 3.07238426e-07, 1.39920776e-10, 0.9866905, 4.48143341e-07, 1.55527242e-10, 0.98372149, 6.37335466e-07, 2.15853988e-10, 0.98004452, 9.22142766e-07, 2.6955008e-10, 0.97548545, 1.30596805e-06, 3.11588988e-10, 0.96982844, 1.92173002e-06, 2.91675749e-10, 0.96280753, 2.89645117e-06, 3.23518197e-10, 0.95409773, 4.19548652e-06, 3.05754525e-10, 0.94330651, 6.15923335e-06, 4.30439262e-10, 0.92996735, 8.89331311e-06, 2.13675243e-10, 0.91353795, 0.00001289, 3.2815706e-10, 0.89340715, 0.00001869, 7.83961659e-10, 0.86891591, 0.00002841, 0, 0.83939902, 0.0000415, 0, 0.80425342, 0.00006046, 4.808013e-10, 0.76303613, 0.00008961, 7.38402534e-10, 0.71558697, 0.00012988, 1.26002225e-10, 0.66215825, 0.00018333, 0, 0.60351953, 0.00026964, 5.28336699e-10, 0.54099511, 0.0003996, 0, 0.47639765, 0.00059419, 1.10767753e-09, 0.41184702, 0.00088599, 0, 0.34950481, 0.00130369, 4.35428836e-10, 0.29129085, 0.00187911, 0, 0.23865765, 0.00265424, 0, 0.19247701, 0.00394013, 3.11555043e-10, 0.15304111, 0.00566478, 7.1771759e-10, 0.12016426, 0.0081051, 1.10225261e-09, 0.09331975, 0.01145429, 1.33753032e-09, 0.07178866, 0.01591739, 2.07554274e-09, 0.05478567, 0.02275161, 2.97944341e-09, 0.04153803, 0.03284214, 4.19444134e-09, 0.03132317, 0.04575503, 6.01686262e-09, 0.02355565, 0.06613532, 9.14061656e-09, 0.01770137, 0.09249947, 1.3818257e-08, 0.01338223, 0.13132545, 2.04021927e-08, 0.01026991, 0.18287058, 3.11533699e-08, 0.00805304, 0.24163724, 4.64041209e-08, 0.00664922, 0.31328136, 6.97848718e-08, 0.00619544, 0.405886, 1.04419678e-07, 0.00593183, 0.48770454, 1.56636208e-07, 0.00665392, 0.58109872, 2.33570845e-07, 0.00834591, 0.66697083, 3.48093334e-07, 0.01137808, 0.74728509, 5.16026742e-07, 0.01594057, 0.81125378, 7.63628745e-07, 0.02197844, 0.85940336, 1.12623375e-06, 0.03259203, 0.90067562, 1.65762384e-06, 0.04786438, 0.93127591, 2.43381219e-06, 0.0677319, 0.95180696, 3.56641425e-06, 0.09413099, 0.96658678, 5.21797753e-06, 0.13806546, 0.97731915, 7.62686847e-06, 0.1794657, 0.98422255, 0.00001114, 0.22921422, 0.98895558, 0.00001629, 0.31843648, 0.99257739, 0.00002383, 0.38475244, 0.99479808, 0.00003492, 0.47106256, 0.99642981, 0.00005123, 0.55973521, 0.99749162, 0.00007525, 0.66114908, 0.99831135, 0.00011056, 0.7586879, 0.99886526, 0.00016236, 0.81935161, 0.99921245, 0.0002381, 0.86763907, 0.99945105, 0.00034851, 0.915116, 0.99964054, 0.00050905, 0.94028828, 0.99975635, 0.000742, 0.95390828, 0.99982661, 0.00107961, 0.96793405, 0.99988174, 0.00156867, 0.97898505, 0.99992087, 0.00227714, 0.98735068, 0.99994874, 0.00330364, 0.99149861, 0.99996538, 0.00479079, 0.99377083, 0.99997599, 0.00694353, 0.9955667, 0.99998325, 0.01005315, 0.99702088, 0.99998873, 0.01452806, 0.99788391, 0.99999218, 0.02093158, 0.99852306, 0.99999468, 0.0300269, 0.99890655, 0.99999624, 0.04282747, 0.9992684, 0.9999974, 0.06064619, 0.99944751, 0.99999814] @@ -96,6 +101,11 @@ "attributes": {}, "value": ["1/year"] }, + "type": { + "type": "character", + "attributes": {}, + "value": ["value"] + }, "representation": { "type": "character", "attributes": {}, @@ -152,6 +162,11 @@ "attributes": {}, "value": ["1/year"] }, + "type": { + "type": "character", + "attributes": {}, + "value": ["value"] + }, "representation": { "type": "character", "attributes": {}, @@ -266,6 +281,11 @@ "attributes": {}, "value": ["1/year"] }, + "type": { + "type": "character", + "attributes": {}, + "value": ["value"] + }, "representation": { "type": "character", "attributes": {}, @@ -322,6 +342,11 @@ "attributes": {}, "value": ["1/year"] }, + "type": { + "type": "character", + "attributes": {}, + "value": ["value"] + }, "representation": { "type": "character", "attributes": {}, @@ -341,11 +366,6 @@ "attributes": {}, "value": [3, 100] }, - "representation": { - "type": "character", - "attributes": {}, - "value": ["point"] - }, "dimnames": { "type": "list", "attributes": { @@ -382,6 +402,16 @@ "type": "character", "attributes": {}, "value": ["g/year"] + }, + "type": { + "type": "character", + "attributes": {}, + "value": ["value"] + }, + "representation": { + "type": "character", + "attributes": {}, + "value": ["point"] } }, "value": [0, 0.16164535, 0, 0, 0.18168717, 0, 0, 0.20421141, 0, 0, 0.22952496, 0, 0, 0.25797253, 0, 0, 0.28994117, 0, 0, 0.32586548, 0, 0, 0.36623329, 0, 0, 0.4115921, 0, 0, 0.46255614, 0, 0, 0.51981429, 0, 0, 0.5841388, 0, 0, 0.65639488, 0, 0, 0.73755117, 0, 0, 0.82869126, 0, 0, 0.9310259, 0, 0, 1.04590611, 0, 0, 1.1748368, 0, 0, 1.3194906, 0, 0, 1.48172124, 0, 0, 1.66357574, 0, 0, 1.86730387, 0, 0, 2.09536292, 0, 0, 2.35041479, 0, 0, 2.63531125, 0, 0, 2.95306102, 0, 0, 3.30677036, 0, 0, 3.69954503, 0, 0, 4.13433766, 0, 0, 4.61371872, 0, 0, 5.13954367, 0, 0, 5.71248201, 0, 0, 6.33136994, 0, 0, 6.99234869, 0, 0, 7.68776246, 0, 0, 8.40482279, 0, 0, 9.12411145, 0, 0, 9.81810395, 0, 0, 10.45005188, 0, 0, 10.97373906, 0, 0, 11.33475114, 0, 0, 11.47384257, 0, 0, 11.33260292, 0, 0, 10.86085827, 0, 0, 10.02429171, 0, 0, 8.8100849, 0, 0, 7.22855074, 0, 0, 5.30988758, 0, 0, 3.09688477, 0, 0, 0.63584019, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3.15165786, 0, 0, 37.79835226, 0, 0, 89.65590267, 0, 0, 169.63387526, 0, 0, 259.09529128, 0, 0, 379.28172474, 0, 0, 517.42639926, 0, 0, 677.4980242, 0, 0, 847.3760778, 0, 0, 1025.04218852, 0, 0, 1222.0970465, 0, 0, 1431.81851959, 0, 0, 1653.02453602, 0, 0, 1893.11244383, 0, 0, 2156.26596851, 0, 0, 2443.78150896, 0, 23.10928534, 2761.91942849, 0, 169.73887122, 3117.03758308, 0, 310.55941645, 3511.58704635, 0, 525.29220709, 3953.12168296, 0, 794.36251051, 4446.95938048, 0, 1155.86271992, 5000.92884374, 0, 1583.92811532, 5621.98361261, 0, 1978.75743921, 6318.46694318, 0, 2401.39915089, 7100.19480407, 0, 2895.3766748, 7978.06340058, 0, 3370.04761683, 8963.54439725, 0, 3856.77828662, 10070.06685159, 0, 4414.92142164, 11312.84307469, 0, 5032.30150687, 12708.59221342, 0, 5714.68582631, 14276.17707836, 0, 6453.0923513, 16036.70520621, 0, 7268.78028023, 18013.98929706, 0, 8182.34442095, 20234.73933525, 0, 9206.48646854, 22728.93586051, 0, 10350.6444723, 25530.19701529, 0, 11633.42895089, 28676.31581404, 0, 13070.62391409, 32209.69578942, 0, 14684.7597491, 36177.97194789, 0, 16494.02287271, 40634.60792102] @@ -397,11 +427,6 @@ "attributes": {}, "value": [3, 100] }, - "representation": { - "type": "character", - "attributes": {}, - "value": ["average"] - }, "dimnames": { "type": "list", "attributes": { @@ -438,6 +463,16 @@ "type": "character", "attributes": {}, "value": ["g/year"] + }, + "type": { + "type": "character", + "attributes": {}, + "value": ["value"] + }, + "representation": { + "type": "character", + "attributes": {}, + "value": ["average"] } }, "value": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7.76455306e-06, 0, 0, 0.0000557, 0, 0, 0.0003963, 0, 0, 0.00280456, 0, 0, 0.01974809, 0, 23.10928534, 0.13864872, 0, 169.73887122, 0.97128925, 0, 310.55941645, 6.76122629, 0, 525.29220709, 45.81367061, 0, 794.36251051, 270.68858557, 0, 1155.86271992, 992.38888965, 0, 1583.92811532, 1831.75786922, 0, 1978.75743921, 2408.98697451, 0, 2401.39915089, 2922.82765383, 0, 2895.3766748, 3494.24069871, 0, 3370.04761683, 4166.32911037, 0, 3856.77828662, 4965.18873202, 0, 4414.92142164, 5916.60431368, 0, 5032.30150687, 7050.01525121, 0, 5714.68582631, 8400.3125895, 0, 6453.0923513, 10008.97064126, 0, 7268.78028023, 11925.45316006, 0, 8182.34442095, 14208.66651243, 0, 9206.48646854, 16928.77472337, 0, 10350.6444723, 20169.32222793, 0, 11633.42895089, 24029.85821174, 0, 13070.62391409, 28628.9349749, 0, 14684.7597491, 34107.78386028, 0, 16494.02287271, 40634.60792102] @@ -481,11 +516,6 @@ "attributes": {}, "value": [3, 100] }, - "representation": { - "type": "character", - "attributes": {}, - "value": ["point"] - }, "dimnames": { "type": "list", "attributes": { @@ -522,6 +552,16 @@ "type": "character", "attributes": {}, "value": ["g/year"] + }, + "type": { + "type": "character", + "attributes": {}, + "value": ["value"] + }, + "representation": { + "type": "character", + "attributes": {}, + "value": ["point"] } }, "value": [0, 0.16164535, 0, 0, 0.18168717, 0, 0, 0.20421141, 0, 0, 0.22952496, 0, 0, 0.25797253, 0, 0, 0.28994117, 0, 0, 0.32586548, 0, 0, 0.36623329, 0, 0, 0.4115921, 0, 0, 0.46255614, 0, 0, 0.51981429, 0, 0, 0.5841388, 0, 0, 0.65639488, 0, 0, 0.73755117, 0, 0, 0.82869126, 0, 0, 0.9310259, 0, 0, 1.04590611, 0, 0, 1.1748368, 0, 0, 1.3194906, 0, 0, 1.48172124, 0, 0, 1.66357574, 0, 0, 1.86730387, 0, 0, 2.09536292, 0, 0, 2.35041479, 0, 0, 2.63531125, 0, 0, 2.95306102, 0, 0, 3.30677036, 0, 0, 3.69954503, 0, 0, 4.13433766, 0, 0, 4.61371872, 0, 0, 5.13954367, 0, 0, 5.71248201, 0, 0, 6.33136994, 0, 0, 6.99234869, 0, 0, 7.68776246, 0, 0, 8.40482279, 0, 0, 9.12411145, 0, 0, 9.81810395, 0, 0, 10.45005188, 0, 0, 10.97373906, 0, 0, 11.33475114, 0, 0, 11.47384257, 0, 0, 11.33260292, 0, 0, 10.86085827, 0, 0, 10.02429171, 0, 0, 8.8100849, 0, 0, 7.22855074, 0, 0, 5.30988758, 0, 0, 3.09688477, 0, 0, 0.63584019, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3.15165786, 0, 0, 37.79835226, 0, 0, 89.65590267, 0, 0, 169.63387526, 0, 0, 259.09529128, 0, 0, 379.28172474, 0, 0, 517.42639926, 0, 0, 677.4980242, 0, 0, 847.3760778, 0, 0, 1025.04218852, 0, 0, 1222.0970465, 0, 0, 1431.81851183, 0, 0, 1653.02448033, 0, 0, 1893.11204752, 0, 0, 2156.26316395, 0, 0, 2443.76176087, 0, 0, 2761.78077977, 0, 0, 3116.06629384, 0, 0, 3504.82582005, 0, 0, 3907.30801235, 0, 0, 4176.27079491, 0, 0, 4008.53995409, 0, 0, 3790.22574339, 0, 0, 3909.47996867, 0, 0, 4177.36715024, 0, 0, 4483.82270188, 0, 0, 4797.21528688, 0, 0, 5104.87811957, 0, 0, 5396.23876101, 0, 0, 5658.57696221, 0, 0, 5875.86448886, 0, 0, 6027.73456496, 0, 0, 6088.536137, 0, 0, 6026.07282282, 0, 0, 5800.16113713, 0, 0, 5360.87478736, 0, 0, 4646.4576023, 0, 0, 3580.76081452, 0, 0, 2070.18808761, 0, 0, 5.02041075e-10] diff --git a/tests/testthat/test-ArraySpeciesBySize-class.R b/tests/testthat/test-ArraySpeciesBySize-class.R index cd97b348c..1a2d79878 100644 --- a/tests/testthat/test-ArraySpeciesBySize-class.R +++ b/tests/testthat/test-ArraySpeciesBySize-class.R @@ -137,6 +137,141 @@ test_that("length-axis array plots only transform number densities", { expect_equal(rate_l[[2]], rate_w[[2]]) }) +test_that("ArraySpeciesBySize records the type of its values", { + mat <- matrix(1:30, nrow = 3, ncol = 10, + dimnames = list(NS_params_small@species_params$species, + signif(NS_params_small@w[1:10], 3))) + + # An explicit type is used as given + for (type in array_types) { + expect_identical(array_type(ArraySpeciesBySize(mat, type = type)), type) + } + expect_error(ArraySpeciesBySize(mat, type = "rate"), "must be one of") + expect_error(ArraySpeciesBySize(mat, type = NA), "must be one of") + expect_error(ArraySpeciesBySize(mat, type = c("density", "value")), + "must be one of") + + # Without one, a density is recognised from the other metadata, so that + # arrays built before the attribute existed keep behaving as they did + expect_identical(array_type( + ArraySpeciesBySize(mat, value_name = "Number density")), "density") + expect_identical(array_type( + ArraySpeciesBySize(mat, value_name = "Resource capacity", + units = "1/g")), "density") + expect_identical(array_type( + ArraySpeciesBySize(mat, value_name = "Encounter rate", + units = "g/year")), "value") + + # An explicit type overrides that guess in both directions + expect_identical(array_type( + ArraySpeciesBySize(mat, value_name = "Number density", + type = "value")), "value") + expect_identical(array_type( + ArraySpeciesBySize(mat, value_name = "Flux", units = "1/year", + type = "density")), "density") + + # An array from before the attribute existed carries no attribute at all + legacy <- ArraySpeciesBySize(mat, value_name = "Number density") + attr(legacy, "type") <- NULL + expect_identical(array_type(legacy), "density") + + # Subsetting preserves the type, arithmetic strips it with the class + tagged <- ArraySpeciesBySize(mat, type = "proportion") + expect_identical(array_type(tagged[1:2, ]), "proportion") + expect_null(attr(tagged * 2, "type")) +}) + +test_that("only a density array is converted onto a length axis", { + mat <- matrix(1, nrow = 3, ncol = 10, + dimnames = list(NS_params_small@species_params$species, + signif(NS_params_small@w[1:10], 3))) + # Mizer arrays live on the weight grid, so a density in one is always a + # density with respect to weight + expect_identical(array_density_wrt(ArraySpeciesBySize(mat, + type = "density")), + "w") + for (type in c("value", "proportion")) { + expect_identical(array_density_wrt(ArraySpeciesBySize(mat, + type = type)), + NA_character_) + } +}) + +test_that("array_units restates the units on the plotted axis", { + mat <- matrix(1, nrow = 3, ncol = 10, + dimnames = list(NS_params_small@species_params$species, + signif(NS_params_small@w[1:10], 3))) + density <- ArraySpeciesBySize(mat, value_name = "Number density", + units = "1/g", type = "density") + expect_identical(array_units(density, "w"), "1/g") + expect_identical(array_units(density, "l"), "1/cm") + + # Compound units keep the rest of the unit string + rate_density <- ArraySpeciesBySize(mat, units = "g^-1/year", + type = "density") + expect_identical(array_units(rate_density, "l"), "cm^-1/year") + + # Values that are not a density keep their units on either axis + rate <- ArraySpeciesBySize(mat, units = "g/year") + expect_identical(array_units(rate, "l"), "g/year") + proportion <- ArraySpeciesBySize(mat, units = "", type = "proportion") + expect_identical(array_units(proportion, "l"), "") +}) + +test_that("array plots use the declared type", { + density <- initialN(NS_params_small) + sp <- NS_params_small@species_params + by_weight <- plot(density, size_axis = "w", return_data = TRUE) + by_length <- plot(density, size_axis = "l", return_data = TRUE) + sp_idx <- match(by_length$Species, sp$species) + jacobian <- unname(sp$b[sp_idx]) * by_weight$w / by_length$l + expect_equal(by_length[[2]], by_weight[[2]] * jacobian) + + # Declaring the same values not to be a density leaves them alone + not_density <- density + attr(not_density, "type") <- "value" + expect_equal(plot(not_density, size_axis = "l", return_data = TRUE)[[2]], + by_weight[[2]]) +}) + +test_that("a proportion is plotted against the whole of [0, 1]", { + feeding_level <- getFeedingLevel(NS_params_small) + expect_identical(array_type(feeding_level), "proportion") + expect_identical(plot(feeding_level)$scales$get_scales("y")$limits, + c(0, 1)) + + # A value above 1 is a real feature of the model and stays visible + above_one <- ArraySpeciesBySize(unclass_rate(feeding_level) * 3, + value_name = "Test proportion", + type = "proportion", + params = NS_params_small) + limits <- plot(above_one)$scales$get_scales("y")$limits + expect_identical(limits[[1]], 0) + # The sizes outside a species' size range are not plotted, so compare + # against the values that are + expect_gte(limits[[2]], max(plot(above_one, return_data = TRUE)[[2]])) + + # An explicit limit always wins, and a log axis is left alone + expect_identical(plot(feeding_level, ylim = c(0.1, 0.5))$scales$ + get_scales("y")$limits, + c(0.1, 0.5)) + expect_identical(plot(feeding_level, ylim = c(NA, 0.5))$scales$ + get_scales("y")$limits, + c(0, 0.5)) + expect_equal(plot(feeding_level, log_y = TRUE)$scales$get_scales("y")$limits, + c(NA_real_, NA_real_)) + + # Values that are not a proportion are unaffected + expect_true(all(is.na(plot(enc_small)$scales$get_scales("y")$limits))) +}) + +test_that("plot2 warns when the two arrays hold different types of value", { + density <- initialN(NS_params_small) + as_value <- density + attr(as_value, "type") <- "value" + expect_warning(plot2(density, as_value), "type") +}) + test_that("plot.ArraySpeciesBySize supports base plot log argument", { p_y <- plot(enc_small, log = "y") expect_identical(p_y$scales$get_scales("x")$trans$name, "identity") diff --git a/tests/testthat/test-ArrayTimeBySpeciesBySize-class.R b/tests/testthat/test-ArrayTimeBySpeciesBySize-class.R index 8ccef5f65..e60ba403f 100644 --- a/tests/testthat/test-ArrayTimeBySpeciesBySize-class.R +++ b/tests/testthat/test-ArrayTimeBySpeciesBySize-class.R @@ -188,6 +188,35 @@ test_that("[.ArrayTimeBySpeciesBySize returns ArrayTimeBySpecies when size is dr expect_identical(attr(slice, "value_name"), "Fishing mortality") }) +test_that("the type survives subsetting, slicing and plotting", { + n <- N(NS_sim_small) + expect_identical(array_type(n), "density") + expect_identical(array_type(n[1:2, , ]), "density") + expect_identical(array_type(n[1, , ]), "density") + expect_identical(array_type(ArrayTimeBySpeciesBySize_slice(n)), "density") + expect_identical(array_type(fmort_small), "value") + + # Dropping the size dimension leaves a time series of the same quantity + feeding_level <- getFeedingLevel(NS_sim_small) + expect_identical(array_type(feeding_level), "proportion") + expect_identical(array_type(feeding_level[, , 3]), "proportion") + + # The slice that plot() takes is a density, so a length axis transforms it + sp <- NS_params_small@species_params + by_weight <- plot(n, size_axis = "w", return_data = TRUE) + by_length <- plot(n, size_axis = "l", return_data = TRUE) + sp_idx <- match(by_length$Species, sp$species) + jacobian <- unname(sp$b[sp_idx]) * by_weight$w / by_length$l + expect_equal(by_length[[2]], by_weight[[2]] * jacobian) + expect_identical(plot(n, size_axis = "l")$scales$get_scales("y")$name, + "Number density [1/cm]") + + # A rate is left alone + rate_w <- plot(fmort_small, size_axis = "w", return_data = TRUE) + rate_l <- plot(fmort_small, size_axis = "l", return_data = TRUE) + expect_equal(rate_l[[2]], rate_w[[2]]) +}) + test_that("[.ArrayTimeBySpeciesBySize leaves time by size matrices plain", { slice <- fmort_small[, 1, ] expect_false(is.ArrayTimeBySpeciesBySize(slice)) diff --git a/tests/testthat/test-plots.R b/tests/testthat/test-plots.R index 1eb79e379..918b02efe 100644 --- a/tests/testthat/test-plots.R +++ b/tests/testthat/test-plots.R @@ -620,6 +620,173 @@ test_that("length-axis spectra transform their densities", { expect_equal(limited$scales$get_scales("y")$limits, c(log10(limit), NA)) }) +test_that("proportion_ylim widens to [0, 1] without ever hiding data", { + # The whole of [0, 1] is shown even for values that use little of it + expect_identical(proportion_ylim(c(NA, NA), FALSE, c(0.2, 0.3)), c(0, 1)) + # and the range is widened, never narrowed, for values outside it + expect_identical(proportion_ylim(c(NA, NA), FALSE, c(0.2, 2.5)), c(0, 2.5)) + expect_identical(proportion_ylim(c(NA, NA), FALSE, c(-0.5, 0.3)), c(-0.5, 1)) + # An explicit limit always wins, end by end + expect_identical(proportion_ylim(c(0.1, 0.5), FALSE, c(0.2, 0.3)), + c(0.1, 0.5)) + expect_identical(proportion_ylim(c(NA, 0.5), FALSE, c(0.2, 0.3)), c(0, 0.5)) + expect_identical(proportion_ylim(c(0.1, NA), FALSE, c(0.2, 0.3)), c(0.1, 1)) + # A logarithmic axis has no place for the 0, so it is left alone + expect_identical(proportion_ylim(c(NA, NA), TRUE, c(0.2, 0.3)), c(NA, NA)) + # Non-finite values are ignored rather than swallowing the range + expect_identical(proportion_ylim(c(NA, NA), FALSE, c(0.2, NA, Inf)), + c(0, 1)) +}) + +test_that("validate_density_wrt accepts only the known measures", { + expect_identical(validate_density_wrt(NULL), NA_character_) + expect_identical(validate_density_wrt(NA), NA_character_) + expect_identical(validate_density_wrt(NA_character_), NA_character_) + for (measure in density_measures) { + expect_identical(validate_density_wrt(measure), measure) + } + expect_error(validate_density_wrt("weight"), "must be NA or one of") + expect_error(validate_density_wrt(c("w", "l")), "must be NA or one of") + expect_error(validate_density_wrt(1), "must be NA or one of") +}) + +test_that("the target density measure follows the size axis", { + # The size variable follows the axis, but whether the values are per size + # or per logarithmic size is a property of the values and is preserved. + expect_identical(density_target_measure("w", "l"), "l") + expect_identical(density_target_measure("l", "w"), "w") + expect_identical(density_target_measure("log_w", "l"), "log_l") + expect_identical(density_target_measure("log_l", "w"), "log_w") + expect_identical(density_target_measure("w", "w"), "w") + expect_identical(density_target_measure("log_l", "l"), "log_l") + # Values that are not a density have no target measure + expect_identical(density_target_measure(NA, "l"), NA_character_) + expect_identical(density_target_measure(NULL, "l"), NA_character_) +}) + +test_that("density Jacobians are correct, reciprocal and transitive", { + w <- c(1, 10, 100) + l <- c(2, 5, 20) + b <- c(3, 3.1, 2.9) + expect_equal(density_measure_jacobian("w", "l", w, l, b), b * w / l) + expect_equal(density_measure_jacobian("l", "w", w, l, b), l / (b * w)) + expect_equal(density_measure_jacobian("log_w", "log_l", w, l, b), b) + expect_equal(density_measure_jacobian("log_l", "log_w", w, l, b), 1 / b) + for (measure in density_measures) { + expect_equal(density_measure_jacobian(measure, measure, w, l, b), + rep(1, 3)) + } + # Converting there and back is the identity + for (from in density_measures) { + for (to in density_measures) { + expect_equal(density_measure_jacobian(from, to, w, l, b) * + density_measure_jacobian(to, from, w, l, b), + rep(1, 3)) + } + } + # A density per log size is the size times the density per size + expect_equal(density_measure_jacobian("w", "log_w", w, l, b), w) + expect_equal(density_measure_jacobian("l", "log_l", w, l, b), l) +}) + +test_that("convert_density_units swaps the size unit", { + expect_identical(convert_density_units("1/g", "w", "l"), "1/cm") + expect_identical(convert_density_units("g^-1/year", "w", "l"), + "cm^-1/year") + expect_identical(convert_density_units("1/cm", "l", "w"), "1/g") + expect_identical(convert_density_units("cm^-1/year", "l", "w"), + "g^-1/year") + # No conversion, so no change + expect_identical(convert_density_units("1/g", "w", "w"), "1/g") + expect_identical(convert_density_units("g/year", NA_character_, + NA_character_), "g/year") + # Nothing declared falls back to the canonical units of the target + expect_identical(convert_density_units(NULL, "w", "l"), "1/cm") + expect_identical(convert_density_units("", "l", "w"), "1/g") + expect_null(convert_density_units(NULL, "log_w", "log_l")) + # A per log size density carries no size unit, so nothing is swapped + expect_identical(convert_density_units("1/year", "log_w", "log_l"), + "1/year") + # Units with no per-size factor to identify are left alone + expect_identical(convert_density_units("g/year", "w", "l"), "g/year") +}) + +test_that("convert_plot_density_axis converts in both directions", { + sp <- params@species_params + w <- params@w[1:5] + plot_dat <- data.frame(w = rep(w, 2), value = seq_len(10), + Species = rep(sp$species[1:2], each = 5)) + sp_idx <- match(as.character(plot_dat$Species), as.character(sp$species)) + l <- w2l(plot_dat$w, sp[sp_idx, ]) + b <- unname(sp$b[sp_idx]) + jacobian <- b * plot_dat$w / l + + # A density with respect to weight, plotted against length + converted <- convert_plot_density_axis(plot_dat, params, "l", + density_wrt = "w") + expect_identical(names(converted), c("l", "value", "Species")) + expect_equal(converted$l, l) + expect_equal(converted$value, plot_dat$value * jacobian) + + # A density with respect to length, plotted against weight, is the inverse + length_dat <- plot_dat + length_dat$value <- converted$value + back <- convert_plot_density_axis(length_dat, params, "w", + density_wrt = "l") + expect_identical(names(back), c("w", "value", "Species")) + expect_equal(back$value, plot_dat$value) + + # Densities per logarithmic size use the b Jacobian + log_converted <- convert_plot_density_axis(plot_dat, params, "l", + density_wrt = "log_w") + expect_equal(log_converted$value, plot_dat$value * b) + log_dat <- plot_dat + log_dat$value <- log_converted$value + log_back <- convert_plot_density_axis(log_dat, params, "w", + density_wrt = "log_l") + expect_equal(log_back$value, plot_dat$value) + + # Values that are not a density only have their size coordinate converted + not_density <- convert_plot_density_axis(plot_dat, params, "l") + expect_equal(not_density$value, plot_dat$value) + expect_equal(not_density$l, l) + # and are untouched on a weight axis + expect_identical(convert_plot_density_axis(plot_dat, params, "w"), + plot_dat) + expect_identical(convert_plot_density_axis(plot_dat, params, "w", + density_wrt = "log_w"), + plot_dat) + + # The value column can be named or indexed + expect_equal(convert_plot_density_axis(plot_dat, params, "l", + density_wrt = "w", + value_col = "value"), + converted) + + # The Jacobian is a species property, so rows with no species are dropped + with_total <- rbind(plot_dat, + data.frame(w = w, value = 1, Species = "Total")) + expect_identical(nrow(convert_plot_density_axis(with_total, params, "w", + density_wrt = "l")), + nrow(plot_dat)) +}) + +test_that("convert_plot_spectrum_axis agrees with the density conversion", { + plot_dat <- data.frame(w = params@w[1:5], value = seq_len(5), + Species = params@species_params$species[[1]]) + expect_identical(convert_plot_spectrum_axis(plot_dat, params, "l", + power = 0), + convert_plot_density_axis(plot_dat, params, "l", + density_wrt = "w")) + expect_identical(convert_plot_spectrum_axis(plot_dat, params, "l", + power = 2), + convert_plot_density_axis(plot_dat, params, "l", + density_wrt = "log_w")) + expect_identical(spectrum_density_wrt(TRUE), "log_w") + expect_identical(spectrum_density_wrt(FALSE), "w") + expect_identical(spectrum_density_wrt(NULL), "w") +}) + test_that("yield plotting helpers validate comparison and gear selection", { sim_shifted <- sim dimnames(sim_shifted@n)$time <- as.character(10:13) @@ -815,6 +982,25 @@ test_that("plotFeedingLevel trims by size and can include critical levels", { expect_setequal(unique(fl_critical$Type), c("actual", "critical")) }) +test_that("plotFeedingLevel shows the whole of [0, 1] without hiding data", { + expect_identical(plotFeedingLevel(params)$coordinates$limits$y, c(0, 1)) + + # A critical feeding level above 1 stays visible, where the fixed [0, 1] + # window used to draw it off the top of the plot + params_hungry <- params + params_hungry@metab[] <- params@metab * 5 + plotted <- plotFeedingLevel(params_hungry, include_critical = TRUE, + return_data = TRUE)[["Feeding level"]] + expect_gt(max(plotted), 1) + limits <- plotFeedingLevel(params_hungry, + include_critical = TRUE)$coordinates$limits$y + expect_identical(limits[[1]], 0) + expect_gte(limits[[2]], max(plotted)) + + # A logarithmic axis is left to the data, as before + expect_null(plotFeedingLevel(params, log_y = TRUE)$coordinates$limits$y) +}) + test_that("plotGrowthCurves validates size_at_age input", { expect_error(plotGrowthCurves(params, size_at_age = data.frame(age = 1, weight = 2)), "needs to have a 'species' column") diff --git a/tests/testthat/test-rate_functions.R b/tests/testthat/test-rate_functions.R index 88358dfe4..e6df70a37 100644 --- a/tests/testthat/test-rate_functions.R +++ b/tests/testthat/test-rate_functions.R @@ -215,8 +215,8 @@ test_that("species-size rate getters work for MizerSim", { test_that("getCriticalFeedingLevel matches metab over intake_max times alpha", { expected <- params@metab / params@intake_max / params@species_params$alpha expect_equal(getCriticalFeedingLevel(params), expected, - ignore_attr = c("value_name", "units", "class", "params", - "representation")) + ignore_attr = c("value_name", "units", "type", "class", + "params", "representation")) }) # getPredRate ------------------------------------------------------------- @@ -865,6 +865,19 @@ test_that("getFluxGradient returns an ArraySpeciesBySize with the right metadata expect_identical(dimnames(fg), dimnames(NS_params_small@metab)) expect_identical(attr(fg, "value_name"), "Flux gradient") expect_identical(attr(fg, "units"), "g^-1/year") + # The flux gradient is a rate of change of a number density, so it is a + # density with respect to weight and needs the Jacobian on a length axis + expect_identical(array_type(fg), "density") + expect_identical(array_type(getFluxGradient(NS_sim_small)), "density") + by_weight <- plot(fg, size_axis = "w", return_data = TRUE) + by_length <- plot(fg, size_axis = "l", return_data = TRUE) + sp <- NS_params_small@species_params + sp_idx <- match(by_length$Species, sp$species) + expect_equal(by_length[[2]], + by_weight[[2]] * unname(sp$b[sp_idx]) * by_weight$w / + by_length$l) + expect_identical(plot(fg, size_axis = "l")$scales$get_scales("y")$name, + "Flux gradient [cm^-1/year]") }) test_that("getFluxGradient is the discrete divergence of getFlux in the interior", { diff --git a/vignettes/cheatsheet-analysis-and-plotting.Rmd b/vignettes/cheatsheet-analysis-and-plotting.Rmd index afa94b941..2c10c6e2e 100644 --- a/vignettes/cheatsheet-analysis-and-plotting.Rmd +++ b/vignettes/cheatsheet-analysis-and-plotting.Rmd @@ -180,19 +180,21 @@ before pairing [`pred_kernel()`](../reference/setPredKernel.html) with [`getEnco --- -## Plotting any array directly with `plot()` +## Plotting mizer arrays -Every static plot mizer produces is a **ggplot2 object** you can extend with `+`. The arrays returned by the summary and rate functions carry a mizer array class and have their own `plot()` method, so you can visualise **any** quantity -without a dedicated plot function or custom ggplot code. +without a dedicated plot function or custom ggplot code. They also carry a +`value_name`, `units` and their `params`, and have `print()`, [`summary()`](../reference/summary.html) and +`as.data.frame()` methods. | Class | Typical source | `plot()` shows | |---|---|---| | [`ArrayTimeBySpecies`](../reference/ArrayTimeBySpecies.html) | `getBiomass(sim)`, `getSSB(sim)`, `getYield(sim)`, `getN(sim)` | value vs time, one line per species | | [`ArraySpeciesBySize`](../reference/ArraySpeciesBySize.html) | `getFeedingLevel(params)`, `getPredMort(params)`, `getEncounter(params)` | value vs size, one line per species | | [`ArrayTimeBySpeciesBySize`](../reference/ArrayTimeBySpeciesBySize.html) | `getFMort(sim)`, `getPredMort(sim)` | one time slice vs size (set with `time`) | -| [`ArrayResourceBySize`](../reference/ArrayResourceBySize.html) | `NResource(params)`, [`getResourceMort(params)`](../reference/getResourceMort.html), [`resource_rate(params)`](../reference/setResource.html), [`resource_capacity(params)`](../reference/setResource.html) | resource quantity vs size | +| [`ArrayResourceBySize`](../reference/ArrayResourceBySize.html) | `NResource(params)`, `finalNResource(sim)`, [`getResourceMort(params)`](../reference/getResourceMort.html), [`resource_rate(params)`](../reference/setResource.html), [`resource_capacity(params)`](../reference/setResource.html), [`resource_level(params)`](../reference/setResource.html) | resource quantity vs size | +| [`ArrayTimeByResourceBySize`](../reference/ArrayTimeByResourceBySize.html) | `NResource(sim)` | one time slice vs size (set with `time`) | ```{r eval=FALSE} plot(getBiomass(sim)) # value vs time, one line per species @@ -201,8 +203,7 @@ plot(getResourceMort(params)) # plankton resource mortality vs size ``` The array plots come with a small toolkit for combining and comparing them. -Every one of these has a method for every array class in the table above, -including the resource classes: +Every one of these has a method for every array class in the table above: | Function | What it does | |---|---| @@ -223,9 +224,7 @@ plotRelative(getEGrowth(params), getEGrowth(params2)) # relative difference plotHover(getBiomass(sim)) # interactive (hover) version of any array plot ``` ---- - -## Common arguments +### Common arguments Most analysis and plotting functions — including `plot()` on an array and the dedicated `plot…()` functions below — share these optional arguments: @@ -249,112 +248,160 @@ replace the older single `log`. window**: data outside the range is hidden but nothing is recomputed. To change the underlying numbers — for example the size range that a biomass is summed over — pass `min_w`/`max_w` (or `min_l`/`max_l`) to the `get…()` function -instead, e.g. [`plotBiomass(sim, min_w = 10)`](../reference/plotBiomass.html). - -`size_axis = "l"` converts the axis with the length–weight parameters `a` and -`b`, so it is unavailable for the resource, which has no species to take them -from. For a *density* it converts the y-axis too, via the appropriate Jacobian — -see the next section. - -### Which density a spectrum plot shows +instead, e.g. `plot(getBiomass(sim, min_w = 10))`. -[`plotSpectra()`](../reference/plotSpectra.html), [`plotSpectra2()`](../reference/plotSpectra2.html), [`plotCDF()`](../reference/plotCDF.html), [`plotCDF2()`](../reference/plotCDF2.html) and [`animate()`](../reference/animate.html) -describe the plotted quantity with two independent logical arguments, each of -which contributes one factor of the weight: +Which arguments apply depends on the array's shape: -| | `per_log_size = FALSE` | `per_log_size = TRUE` | +- `plot()` accepts `species`, `tlim`, `total`, `background`, + `highlight`, `log_x`, `log_y`, `ylim`. +- `plot()` accepts `species`, `highlight`, `total`, + `background`, `log_x`, `log_y`, `wlim`, `llim`, `ylim`, `size_axis`, + `all.sizes`. `size_axis` and `llim` belong to the size shapes only — a plot + against time has no size axis to convert. +- `plot()` takes one time slice and hands it to the + `ArraySpeciesBySize` method, so it accepts everything that method does plus + `time` (default: the last time step). It has no `tlim`: only one time is + shown. +- `plot()` accepts `log_x`, `log_y`, `wlim`, `ylim` only. + The resource is a single spectrum, so there is nothing for `species`, + `highlight`, `total` or `background` to select, and no `size_axis`/`llim` + because the resource has no length-weight relationship. +- `plot()` accepts the same as + `ArrayResourceBySize` plus `time`. + +All five also accept `return_data = TRUE`, which returns the data frame behind +the plot instead of the plot, and `y_ticks` to set the number of y-axis ticks. + +### What kind of value an array holds + +Every mizer array declares what kind of quantity it holds, in its `type` +attribute, because two kinds need handling that the numbers alone do not reveal: + +| `type` | Meaning | What the plots do with it | |---|---|---| -| `biomass = FALSE` | number density | number density per log size | -| `biomass = TRUE` | biomass density | biomass density per log size | +| `"value"` | a rate, an amount — the default | nothing special | +| `"density"` | an amount per gram of body weight | converts the values, not just the axis, when plotted against length | +| `"proportion"` | a fraction | shows the whole of the interval from 0 to 1 on a linear y axis | -The older single `power` argument is the sum of the two (0, 1, 1, 2 across that -table) and is still accepted, but it cannot tell the two `power = 1` cells -apart — it is read as the biomass density with respect to weight, which is what -picks the y-axis label and the length-axis Jacobian. Supplying `power` together -with a flag that contradicts it is an error, so express the choice with the -flags. `plotCDF()` accepts only `per_log_size = FALSE`: a cumulative total does -not depend on the density it was accumulated from. +Read it with [`array_type(x)`](../reference/array_type.html), and set it when you build an array of your own: -**`log_x` does not change the y-axis.** Showing weight on a logarithmic axis is -a display choice; converting a density per unit weight into a density per -logarithmic weight interval is `per_log_size`. Conflating the two is the usual -reason a spectrum looks like it has the wrong slope. +```{r eval=FALSE} +ArraySpeciesBySize(x, value_name = "Number density", units = "1/g", + type = "density", params = params) +``` -Which arguments apply depends on the array's shape: +An array that declares nothing is taken to be a density if it is named +`"Number density"` or has units `"1/g"`, which is how mizer recognised densities +before the attribute existed. -- `plot()` accepts `species`, `tlim`, `total`, `background`, - `highlight`, `log_x`, `log_y`, `ylim`. -- `plot()` accepts `species`, `highlight`, `log_x`, `log_y`, - `wlim`, `llim`, `ylim`, `size_axis`, `all.sizes`. `size_axis` and `llim` - belong to this shape only — a plot against time has no size axis to convert. +### Plotting densities ---- +A density is an amount *per unit size*, so its numerical value depends on which +size variable it is a density in. Changing that variable — weight to length, or +size to log size — therefore changes the plotted **values**, not just the axis: +it needs a Jacobian factor. The plot functions apply it for you, for the arrays +that declare themselves densities: -## Dedicated plot functions +| Source | Density | +|---|---| +| [`initialN(params)`](../reference/initialN-set.html), [`finalN(sim)`](../reference/finalN.html), [`N(sim)`](../reference/N.html), [`get_initial_n(params)`](../reference/get_initial_n.html) | consumer number density, per gram | +| [`initialNResource(params)`](../reference/initialNResource-set.html), [`finalNResource(sim)`](../reference/finalN.html), [`NResource(sim)`](../reference/N.html) | resource number density, per gram | +| [`resource_capacity(params)`](../reference/setResource.html) | resource carrying capacity, per gram | +| [`getFluxGradient(params)`](../reference/getFluxGradient.html) | rate of change of the number density, per gram per year | -Each dedicated `plot…()` function is essentially `plot()` applied to the matching -`get…()` array, so `plotBiomass(sim)` is `plot(getBiomass(sim))`. They accept the -common arguments above, and each has a `plotly…()` counterpart (e.g. -[`plotlyBiomass()`](../reference/plotBiomass.html)) for interactive use — the array `plot()`s use `plotHover()` -instead. See `?plotting_functions`. +The factors are built from the length-weight relationship $w = a\, l^b$ of each +species, taken from the `a` and `b` columns of [`species_params`](../reference/species_params.html): -**Against time:** +| Argument | Factor | +|---|---|---| +| `size_axis = "w"`, `per_log_size = TRUE` | $dw/d\log w = w$ | +| `size_axis = "l"`, `per_log_size = FALSE` | $dw/dl = b\, w / l$ | +| `size_axis = "l"`, `per_log_size = TRUE` | $dw / d\log l = b\, w$ | + +Two things to watch: + +- **The resource is dropped from a length axis.** The resource spectrum has no + length-weight relationship, so [`plotSpectra(params, size_axis = "l")`](../reference/plotSpectra.html) shows the + species only. Use `size_axis = "w"` to see the resource. +- **`plot()` on a density array has no `per_log_size` argument.** Mizer arrays + are indexed by the weight grid, so an array that holds a density holds one per + gram; `plot()` honours `size_axis` but nothing switches an array between per + size and per log size. Use `plotSpectra()` when you want a per-log-size + spectrum. + +### Plotting proportions + +A proportion is easiest to read against the whole of the scale it belongs to, so +`plot()` of one uses a linear y axis running from 0 to 1. The arrays that +declare themselves proportions are `getFeedingLevel()`, +[`getCriticalFeedingLevel()`](../reference/getCriticalFeedingLevel.html), [`maturity()`](../reference/setReproduction.html), [`repro_prop()`](../reference/setReproduction.html), [`psi()`](../reference/setReproduction.html) and +`resource_level()`. + +The range is only ever *widened* to include the data, never narrowed to that +interval — a critical feeding level or a resource level above 1 is a real +feature of the model, and stays visible. An explicit `ylim` or `log_y` always +wins. -| Function | How it relates to plotting the array directly | -|---|---| -| [`plotBiomass(sim)`](../reference/plotBiomass.html) | same as `plot(getBiomass(sim))` | -| [`plotYield(sim)`](../reference/plotYield.html) | same as `plot(getYield(sim))` | -| [`plotYieldGear(sim)`](../reference/plotYieldGear.html) | like `plotYield()` but keeps the gear dimension, one panel per gear | +--- -**Against body size.** By default these show the final time step; use -`time_range` to average over a period. +## Plotting size spectra -| Function | How it relates to plotting the array directly | -|---|---| -| [`plotFeedingLevel(sim)`](../reference/plotFeedingLevel.html) | same as `plot(getFeedingLevel(sim))` | -| [`plotPredMort(sim)`](../reference/plotPredMort.html) | same as `plot(getPredMort(sim))` | -| [`plotFMort(sim)`](../reference/plotFMort.html) | same as `plot(getFMort(sim))` | -| [`plotSpectra(sim)`](../reference/plotSpectra.html) | abundance/biomass spectra: additionally overlays the resource spectrum and background species, and `biomass`/`per_log_size` choose the plotted density (see [above](#which-density-a-spectrum-plot-shows)) | -| [`plotCDF(sim)`](../reference/plotCDF.html) | cumulative version of the spectrum (`normalise` for proportion vs total) | -| [`plotGrowthCurves(sim)`](../reference/plotGrowthCurves.html) | a distinct plot: size at age rather than a size spectrum | -| [`plotDiet(params)`](../reference/plotDiet.html) | a distinct plot: stacked diet composition by prey | +"Size spectrum" is just a different name for "density as a function of size". +`plotSpectra()` is the function you want for the plot size spectra. It +plots the abundance or biomass density against size, one line per species, and +unlike a plain `plot()` of a density array it also overlays the resource +spectrum (`resource = TRUE`, the default) and any background species +(`background =TRUE`). Which density it shows is set by `biomass` and +`per_log_size`, described below. -**Calibration:** [`plotBiomassObservedVsModel(params)`](../reference/plotBiomassObservedVsModel.html) and -[`plotYieldObservedVsModel(params)`](../reference/plotYieldObservedVsModel.html); the latter takes a `gear` argument that -restricts both the modelled and the observed catch to the named gears. See the -[calibration cheatsheet](cheatsheet-calibration.html). +By default it shows the final time step of a simulation; pass `time_range` to +average over a period, or give it a `MizerParams` object to see the current +state. The common arguments above all apply, and [`plotlySpectra()`](../reference/plotSpectra.html) is the +interactive twin. ```{r eval=FALSE} -plotBiomass(sim, species = c("Cod", "Herring"), total = TRUE) +plotSpectra(params) # spectra of the current state plotSpectra(sim, per_log_size = TRUE, time_range = 1990:2000) -plotGrowthCurves(sim, species = "Cod", max_age = 20) -plotDiet(params, species = "Cod") +plotSpectra(sim, species = c("Cod", "Herring"), resource = FALSE) +plotSpectra(sim, biomass = TRUE, size_axis = "l") # biomass density against length ``` -**Overview:** `plot(sim)` combines several panels; `plot(params)` shows the same -panels for a model's steady state (without the biomass-through-time panel). +### Which density a spectrum plot shows ---- +`plotSpectra()`, [`plotSpectra2()`](../reference/plotSpectra2.html), [`plotCDF()`](../reference/plotCDF.html), [`plotCDF2()`](../reference/plotCDF2.html) and [`animate()`](../reference/animate.html) +describe the plotted quantity with two independent logical arguments: -## Cumulative distributions +| | `per_log_size = FALSE` | `per_log_size = TRUE` | +|---|---|---| +| `biomass = FALSE` | number density | number density per log size | +| `biomass = TRUE` | biomass density | biomass density per log size | + +The older single `power` argument is the sum of the two (0, 1, 1, 2 across that +table) and is still accepted. + +**`log_x` does not change the y-axis.** Showing weight on a logarithmic axis is +a display choice; you need to use `per_log_size` to convert a density per unit +weight into a density per +logarithmic weight interval. Conflating the two is the usual +reason a spectrum looks like it has the wrong slope. + +### Cumulative distributions `plotCDF(object, species, biomass, normalise)` plots cumulative abundance or biomass over size — steadier than a density spectrum for eyeballing where biomass sits. `biomass = TRUE` (default) accumulates biomass, `biomass = FALSE` accumulates numbers; `normalise = FALSE` plots the cumulative total rather than -the proportion. Unlike in `plotSpectra()`, only `per_log_size = FALSE` is -accepted: the integral does not depend on it. +the proportion. The `per_log_size` argument is not used: a cumulative total +does not depend on it. ```{r eval=FALSE} plotCDF(NS_params, species = c("Cod", "Herring")) plotCDF(NS_sim, biomass = FALSE, normalise = FALSE) ``` ---- - -## Comparing two simulations or models +### Comparing two size distributions -For whole spectra use the dedicated functions below; for any other rate array +For whole spectra use the functions in the table below; for any other rate array use `plot2()` and `plotRelative()` from the array toolkit above. | Function | Shows | @@ -369,9 +416,7 @@ plotSpectraRelative(params, params2) # 2 (N2 - N1) / (N1 + N2) plotCDF2(sim, sim2, "Unfished", "Fished") ``` ---- - -## Animating spectra through time +### Animating spectra through time `animate()` plays a spectrum or rate array through the course of a simulation ([`animateSpectra()`](../reference/animate.html) is a retained alias). @@ -384,42 +429,49 @@ animate(NResource(sim)) # an ArrayTimeByResourceBySize over time --- -## The plankton resource +## Dedicated plot functions -Resource-related quantities come back as an `ArrayResourceBySize` — a numeric -vector over the size grid carrying a `value_name`, `units`, and its `params`, -with `print()`, [`summary()`](../reference/summary.html), `as.data.frame()`, and `plot()` methods. Producers -include `NResource(params)` / `finalNResource(sim)`, `getResourceMort(params)`, -`resource_rate(params)` (intrinsic birth rate), `resource_capacity(params)` -(carrying capacity), and [`resource_level(params)`](../reference/setResource.html). +Besides the spectrum plots above, mizer has a dedicated `plot…()` function for +each of the common summary quantities. Each is essentially `plot()` applied to +the matching `get…()` array, so [`plotBiomass(sim)`](../reference/plotBiomass.html) is `plot(getBiomass(sim))`. +They accept the common arguments above, and each has a `plotly…()` counterpart +(e.g. [`plotlyBiomass()`](../reference/plotBiomass.html)) for interactive use — the array `plot()`s use +`plotHover()` instead. -```{r eval=FALSE} -plot(getResourceMort(params)) # resource mortality vs size -summary(NResource(params)) -``` +**Against time:** -The array toolkit works on the resource too: +| Function | How it relates to plotting the array directly | +|---|---| +| [`plotBiomass(sim)`](../reference/plotBiomass.html) | same as `plot(getBiomass(sim))` | +| [`plotYield(sim)`](../reference/plotYield.html) | same as `plot(getYield(sim))` | +| [`plotYieldGear(sim)`](../reference/plotYieldGear.html) | like `plotYield()` but keeps the gear dimension, one panel per gear | -```{r eval=FALSE} -plot2(resource_capacity(params), resource_capacity(params2), "Before", "After") -plotRelative(resource_capacity(params), resource_capacity(params2)) -addPlot(plot(NResource(params)), resource_capacity(params)) -``` +**Against body size.** By default these show the final time step; use +`time_range` to average over a period. -A resource array holds a single spectrum, so `species`, `total` and `background` -do nothing there and warn if you set them, and `size_axis = "l"` is unavailable -because the weight-length relationship is a species parameter. +| Function | How it relates to plotting the array directly | +|---|---| +| [`plotFeedingLevel(sim)`](../reference/plotFeedingLevel.html) | same as `plot(getFeedingLevel(sim))` | +| [`plotPredMort(sim)`](../reference/plotPredMort.html) | same as `plot(getPredMort(sim))` | +| [`plotFMort(sim)`](../reference/plotFMort.html) | same as `plot(getFMort(sim))` | +| [`plotGrowthCurves(sim)`](../reference/plotGrowthCurves.html) | a distinct plot: size at age rather than a size spectrum | +| [`plotDiet(params)`](../reference/plotDiet.html) | a distinct plot: stacked diet composition by prey | -Time-resolved resource data (`NResource(sim)`) is an [`ArrayTimeByResourceBySize`](../reference/ArrayTimeByResourceBySize.html), -which `animate()` can play through time, and which the comparison functions above -slice at a chosen `time`. To include the resource in a species spectrum plot, -pass `resource = TRUE` (supported by `plotSpectra()`, `plotCDF()`, and friends). +**Calibration:** [`plotBiomassObservedVsModel(params)`](../reference/plotBiomassObservedVsModel.html) and +[`plotYieldObservedVsModel(params)`](../reference/plotYieldObservedVsModel.html); the latter takes a `gear` argument that +restricts both the modelled and the observed catch to the named gears. See the +[calibration cheatsheet](cheatsheet-calibration.html). ```{r eval=FALSE} -animate(NResource(sim)) # resource spectrum over time -plot2(NResource(sim), NResource(sim2), time = 1990) +plotBiomass(sim, species = c("Cod", "Herring"), total = TRUE) +plotFMort(sim, time_range = 1990:2000) +plotGrowthCurves(sim, species = "Cod", max_age = 20) +plotDiet(params, species = "Cod") ``` +**Overview:** `plot(sim)` combines several panels; `plot(params)` shows the same +panels for a model's steady state (without the biomass-through-time panel). + --- ## Working with ggplot2 @@ -488,27 +540,6 @@ ArraySpeciesBySize(x, params = params, representation = "average") # size-resol bin_average_weight(K, params) # the primitive, if you are not doing an integral encounter_kernel(params) # kernel getEncounter() uses; NOT pred_kernel() -# ── Dedicated plot functions ────────────────────────────────────────────────── -# Each plot*() is a shortcut for plot() on the matching get*() array, and each has -# an interactive plotly*() twin (plotlyBiomass(), plotlySpectra(), …). -plot(sim) # 5-panel summary -plotBiomass(sim) # biomass vs time -plotYield(sim) # yield vs time -plotYieldGear(sim) # yield vs time, faceted by gear -plotSpectra(sim) # abundance spectra vs size (+ resource & background) -plotFeedingLevel(sim) # feeding level vs size -plotPredMort(sim) # predation mortality vs size -plotFMort(sim) # fishing mortality vs size -plotGrowthCurves(sim) # size vs age -plotDiet(params, species = "Cod") # diet composition vs size -plotCDF(sim) # cumulative biomass/abundance over size - -# ── Choosing what a spectrum plot shows ─────────────────────────────────────── -plotSpectra(sim, biomass = TRUE) # biomass rather than number -plotSpectra(sim, per_log_size = TRUE) # density per log size -plotSpectra(sim, size_axis = "l") # x axis in length, not weight -plotSpectra(sim, log_x = TRUE) # display only: does NOT change the y density - # ── Plot any array directly, plus combine / compare tools ───────────────────── plot(getResourceMort(params)) # any get*() array plots directly p <- plot(getBiomass(sim), species = "Cod") @@ -516,10 +547,31 @@ addPlot(p, getBiomass(sim), species = "Herring", linetype = "dashed") # add lin plot2(getFMort(params), getFMort(params2), "Before", "After") # compare arrays plotRelative(getEGrowth(params), getEGrowth(params2)) # relative diff plotHover(getBiomass(sim)) # interactive (hover) version of an array plot -animate(sim) # animate spectra through time + +# ── Size spectra and other densities ────────────────────────────────────────── +plotSpectra(sim) # abundance spectra vs size (+ resource & background) +plotCDF(sim) # cumulative biomass/abundance over size +animate(sim) # animate spectra through time +plotSpectra(sim, biomass = TRUE) # biomass rather than number +plotSpectra(sim, per_log_size = TRUE) # density per log size +plotSpectra(sim, size_axis = "l") # x axis in length, not weight +plotSpectra(sim, log_x = TRUE) # display only: does NOT change the y density # ── Compare two simulations or models ───────────────────────────────────────── plotSpectra2(params, params2, "Before", "After") plotSpectraRelative(params, params2) # relative difference of spectra plotCDF2(sim, sim2, "Unfished", "Fished") + +# ── Dedicated plot functions ────────────────────────────────────────────────── +# Each plot*() is a shortcut for plot() on the matching get*() array, and each has +# an interactive plotly*() twin (plotlyBiomass(), plotlySpectra(), …). +plot(sim) # 5-panel summary +plotBiomass(sim) # biomass vs time +plotYield(sim) # yield vs time +plotYieldGear(sim) # yield vs time, faceted by gear +plotFeedingLevel(sim) # feeding level vs size +plotPredMort(sim) # predation mortality vs size +plotFMort(sim) # fishing mortality vs size +plotGrowthCurves(sim) # size vs age +plotDiet(params, species = "Cod") # diet composition vs size ``` diff --git a/vignettes/upgrading.Rmd b/vignettes/upgrading.Rmd index 8b26555ca..85d24fa92 100644 --- a/vignettes/upgrading.Rmd +++ b/vignettes/upgrading.Rmd @@ -72,6 +72,57 @@ things change: integrating a density over size gives the same cumulative quantity either way. Use `biomass` on its own there. +### Arrays say what kind of value they hold + +Mizer arrays now carry a `type` attribute saying what kind of quantity their +values are: `"value"` (the default) for a rate or an amount, `"density"` for an +amount per gram of body weight, `"proportion"` for a fraction. Two kinds of +plotting behaviour follow from it, and both used to be decided some other way. + +**Densities.** Plotting a density against a length axis (`size_axis = "l"`) has +to multiply the values by a Jacobian, because a density per gram is not a +density per centimetre. mizer used to decide which arrays those were by looking +at their metadata strings, treating an array as a density if it was named +`"Number density"` or had units `"1/g"`. For mizer's own number spectra — +[`initialN()`](../reference/initialN-set.html), [`N()`](../reference/N.html), [`finalN()`](../reference/finalN.html), [`NResource()`](../reference/N.html), [`resource_capacity()`](../reference/setResource.html) — nothing +changes; they were recognised before and are tagged now. What changes is +[`getFluxGradient()`](../reference/getFluxGradient.html): it is a rate of change of a number density, with units +`g^-1/year`, and neither of the old string tests recognised it, so on a length +axis its values were left as densities per gram and were mislabelled as such. +They are now converted with the `dw/dl = b w / l` Jacobian and labelled +`cm^-1/year`. The new curve is the right one; if you were reading values off the +old one, they were per gram plotted against length. + +**Proportions.** [`getFeedingLevel()`](../reference/getFeedingLevel.html), [`getCriticalFeedingLevel()`](../reference/getCriticalFeedingLevel.html), [`maturity()`](../reference/setReproduction.html), +[`repro_prop()`](../reference/setReproduction.html), [`psi()`](../reference/setReproduction.html) and [`resource_level()`](../reference/setResource.html) now declare themselves +proportions, and a plot of one shows the whole of the interval from 0 to 1 on a +linear y axis, so the value can be read against the scale it belongs to. Three +consequences: + +- `plot(getFeedingLevel(params))` and the other array plots gain that y range, + where they used to fit the axis to the data. This is the range + [`plotFeedingLevel()`](../reference/plotFeedingLevel.html) has always shown, so the dedicated function and the array + plot now agree. +- `plot(resource_level(params))` gets a linear y axis instead of a logarithmic + one. Pass `log_y = TRUE` to get the old axis back; any explicit `log_y` or + `log` you already pass is respected. +- The range is only ever *widened* to include the data, never narrowed to the + interval from 0 to 1. So `plotFeedingLevel(include_critical = TRUE)` now shows + a critical feeding level above 1, which the old fixed window drew off the top + of the plot. Nothing is ever hidden, and an explicit `ylim` still wins. + +**Declaring it yourself.** An array of your own is taken to be a density or a +proportion only if you say so, by passing `type` to the array constructor. If +you do not pass it, the old string tests still run as a fallback, so existing +code that named an array `"Number density"` or gave it units `"1/g"` keeps +working, and arrays saved by earlier versions keep working when they are loaded. + +Extension packages that called the unexported plotting helpers directly should +note that [`plotComparisonDataFrame()`](../reference/plotComparisonDataFrame.html) and the internal `animate_plotly()` take a +single `density_wrt` argument in place of `spectrum_power` and +`spectrum_per_log_size`, and that the internal `array_spectrum_power()` is gone. +The `power`-based interface of `plotSpectra()` and friends is unchanged. + ### Length and weight parameters follow the one you gave last A size can be given either as a weight (`w_mat`, `w_max`, …) or as the length it @@ -145,7 +196,7 @@ also covers the functions that take no `info_level` argument, or pass `info_level = 0` to the one call you want quiet. The resource works the same way. [`resource_params(params)$kappa <- ...`](../reference/resource_params.html) on a -model whose [`resource_capacity()`](../reference/setResource.html) you had set by hand used to change the stored +model whose `resource_capacity()` you had set by hand used to change the stored `kappa` and nothing else, without saying anything at all; it now warns. This is the counterpart of the 3.2 change described under *Frozen arrays are @@ -384,7 +435,7 @@ are still moving. It is a message, not a warning, because convergence at the ### `getStability()` checks that it was given a steady state Both [`getStability()`](../reference/getStability.html) and [`getLimitCycleSim()`](../reference/getLimitCycleSim.html) linearise the dynamics *at* -[`initialN(params)`](../reference/initialN-set.html). If that state is not a fixed point, the eigenvalues describe +`initialN(params)`. If that state is not a fixed point, the eigenvalues describe the neighbourhood of a point the model is not sitting at and the verdict on stability is meaningless. Both now warn in that case. Run `steadyNewton()` first, or silence with `options(mizer_info_level = 0)` if you know what you are doing. @@ -590,8 +641,8 @@ directions (`catchability(params)` and `catchability(params) <- value`, | [`getMetabolicRate()`](../reference/deprecated_accessors.html) | `metab()` | | [`getExtMort()`](../reference/deprecated_accessors.html) | [`ext_mort()`](../reference/setExtMort.html) | | [`getExtEncounter()`](../reference/deprecated_accessors.html) | [`ext_encounter()`](../reference/setExtEncounter.html) | -| [`getMaturityProportion()`](../reference/deprecated_accessors.html) | [`maturity()`](../reference/setReproduction.html) | -| [`getReproductionProportion()`](../reference/deprecated_accessors.html) | [`repro_prop()`](../reference/setReproduction.html) | +| [`getMaturityProportion()`](../reference/deprecated_accessors.html) | `maturity()` | +| [`getReproductionProportion()`](../reference/deprecated_accessors.html) | `repro_prop()` | | [`getReproductionLevel()`](../reference/deprecated_accessors.html) | `reproduction_level()` | Nothing breaks: the old names still return exactly the same value. Renaming is @@ -796,7 +847,7 @@ directly on a bare data frame will see the extra columns and stricter checks `print()` on the array objects returned by the rate getters ([`ArraySpeciesBySize`](../reference/ArraySpeciesBySize.html), [`ArrayTimeBySpecies`](../reference/ArrayTimeBySpecies.html), [`ArrayResourceBySize`](../reference/ArrayResourceBySize.html), [`ArrayTimeByResourceBySize`](../reference/ArrayTimeByResourceBySize.html) and [`ArrayTimeBySpeciesBySize`](../reference/ArrayTimeBySpeciesBySize.html), as returned by `getEncounter()`, `getBiomass()`, -`getFMort()`, [`NResource()`](../reference/N.html) and similar) now truncates the output instead of +`getFMort()`, `NResource()` and similar) now truncates the output instead of flooding the console with all the array entries. If your code or reports relied on the old printed format, use `as.data.frame()` to go back to the full output. From 18d2d5b88120c9d4274b1af384a971ee2a11f753 Mon Sep 17 00:00:00 2001 From: Gustav Delius Date: Sat, 15 Aug 2026 23:23:31 +0100 Subject: [PATCH 3/3] Add per_log_size to the array plot methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `size_axis` and `per_log_size` are the same operation — both change the measure a density is expressed with respect to, and both are computed by `density_measure_jacobian()` — but only the first was available on an array. `plotSpectra()` owned the other, and takes a `MizerParams` or `MizerSim` rather than an array, so an arbitrary density array could not be viewed per log size at all. Worse, `per_log_size = TRUE` was swallowed by `...` and returned the per-size plot with no warning. `plot()`, `plot2()`, `addPlot()` and `animate()` now take `per_log_size` for any array whose type is `"density"`. Unlike `size_axis` it needs no weight-length relationship, so the resource classes take it too, and rows with no species — the "Total" row — survive the conversion on a weight axis. Asking for it on an array that holds anything else is an argument error rather than being ignored. `biomass` deliberately stays with `plotSpectra()`: multiplying by w to count grams instead of individuals changes the quantity rather than the measure, and only means anything for a number density. Also fixes `convert_density_units()` for the newly reachable conversion to a logarithmic measure, which has to remove the per-size factor rather than swap it: `1/g` becomes dimensionless and `g^-1/year` becomes `1/year`. Co-Authored-By: Claude Opus 5 --- NEWS.md | 10 ++ R/ArrayResourceBySize-class.R | 30 +++--- R/ArraySpeciesBySize-class.R | 87 ++++++++++++++--- R/ArrayTimeBySpeciesBySize-class.R | 12 ++- R/animateSpectra.R | 8 ++ R/plots.R | 95 +++++++++++++------ inst/skills/analyse-and-plot/SKILL.md | 40 ++++---- man/addPlot.Rd | 6 ++ man/animate.Rd | 7 ++ man/check_per_log_size.Rd | 22 +++++ man/convert_density_units.Rd | 13 ++- man/convert_plot_density_axis.Rd | 12 ++- man/density_target_measure.Rd | 18 ++-- man/plot.ArrayResourceBySize.Rd | 6 ++ man/plot.ArraySpeciesBySize.Rd | 7 ++ man/plot.ArrayTimeBySpeciesBySize.Rd | 6 ++ man/plot.Rd | 5 + man/plot2.Rd | 6 ++ man/plotComparisonDataFrame.Rd | 6 +- .../testthat/test-ArrayResourceBySize-class.R | 20 ++++ .../testthat/test-ArraySpeciesBySize-class.R | 42 ++++++++ tests/testthat/test-plots.R | 68 +++++++++++++ .../cheatsheet-analysis-and-plotting.Rmd | 46 +++++---- 23 files changed, 467 insertions(+), 105 deletions(-) create mode 100644 man/check_per_log_size.Rd diff --git a/NEWS.md b/NEWS.md index 50ae155eb..7b2edbbdc 100644 --- a/NEWS.md +++ b/NEWS.md @@ -136,6 +136,16 @@ stability of steady states. it. `plot(getFeedingLevel(params))` therefore now shows the same y range that `plotFeedingLevel()` always has. +- `plot()`, `plot2()`, `addPlot()` and `animate()` on an array that holds a + density gain a `per_log_size` argument, which expresses the values per + logarithmic size rather than per size. This is the same change of measure that + `size_axis` makes — both rescale the density by a Jacobian — so the two now + sit side by side, and `plotSpectra()` is no longer the only way to see a + spectrum per log size. Unlike `size_axis` it needs no weight-length + relationship, so the resource classes take it too. Asking for it on an array + that does not hold a density is now an error; it used to be swallowed silently + by `...`. + - `plotYieldObservedVsModel()` gains a `gear` argument that restricts the comparison to the catch of the selected gears. Both the model yield and the observed yield are then taken from those gears only, so in a model where diff --git a/R/ArrayResourceBySize-class.R b/R/ArrayResourceBySize-class.R index 284a30f3c..df918df59 100644 --- a/R/ArrayResourceBySize-class.R +++ b/R/ArrayResourceBySize-class.R @@ -152,6 +152,10 @@ print.summary.ArrayResourceBySize <- function(x, ...) { #' @param ylim A numeric vector of length two providing lower and upper #' limits for the value (y) axis. Use `NA` to refer to the existing #' minimum or maximum. +#' @param per_log_size For an array that holds a density, whether to plot it +#' per logarithmic size (`TRUE`) rather than per size (`FALSE`). The default, +#' `NULL`, plots the density as it stands. An error for an array that does not +#' hold a density. #' @param y_ticks The approximate number of ticks desired on the y axis. #' @param ... Unused. #' @@ -167,7 +171,9 @@ print.summary.ArrayResourceBySize <- function(x, ...) { plot.ArrayResourceBySize <- function(x, return_data = FALSE, log_x = TRUE, log_y = TRUE, log = NULL, wlim = c(NA, NA), ylim = c(NA, NA), + per_log_size = NULL, y_ticks = 6, ...) { + check_per_log_size(x, per_log_size) log_y <- array_log_y(x, log_y, log, !missing(log_y)) log_axes <- parsePlotLog(log, log_x = log_x, log_y = log_y) log_x <- log_axes$log_x @@ -175,18 +181,19 @@ plot.ArrayResourceBySize <- function(x, return_data = FALSE, assert_that(length(wlim) == 2, length(ylim) == 2) - value_name <- attr(x, "value_name") %||% "value" - units_str <- attr(x, "units") params <- attr(x, "params") plot_dat <- prepare_ArrayResourceBySize_plot_data(x, wlim = wlim) + # The resource has no weight-length relationship, so it has no length axis. + # Expressing a density per logarithmic weight needs no such relationship, + # though, so that much is available here. + plot_dat <- convert_plot_density_axis(plot_dat, params, "w", + density_wrt = array_density_wrt(x), + per_log_size = per_log_size) if (return_data) return(plot_dat) - y_label <- value_name - if (!is.null(units_str) && nzchar(units_str)) { - y_label <- paste0(value_name, " [", units_str, "]") - } + y_label <- array_y_label(x, default = "value", per_log_size = per_log_size) ylim <- array_ylim(x, ylim, log_y, plot_dat[[2]]) plotDataFrame(plot_dat, params, xlab = "Weight (g)", @@ -701,6 +708,7 @@ animate.ArrayTimeByResourceBySize <- function(x, species = NULL, ylim = c(NA, NA), tlim = c(NA, NA), size_axis = c("w", "l"), + per_log_size = NULL, total = FALSE, background = TRUE, frame_duration = 500, @@ -712,6 +720,7 @@ animate.ArrayTimeByResourceBySize <- function(x, species = NULL, is.string(easing), length(wlim) == 2, length(ylim) == 2, length(tlim) == 2) warn_unused_resource_args(species, total, background) + check_per_log_size(x, per_log_size) # The length axis is derived from each species' weight-length parameters, # and the resource is not a species. size_axis <- plot_size_axis(size_axis) @@ -724,8 +733,6 @@ animate.ArrayTimeByResourceBySize <- function(x, species = NULL, log_y <- log_axes$log_y params <- attr(x, "params") - value_name <- attr(x, "value_name") %||% "Value" - units_str <- attr(x, "units") times <- as.numeric(dimnames(x)[[1]]) arr <- unclass(x) @@ -748,13 +755,12 @@ animate.ArrayTimeByResourceBySize <- function(x, species = NULL, df$Species <- "Resource" df$legend_name <- "Resource" - y_label <- value_name - if (!is.null(units_str) && nzchar(units_str)) { - y_label <- paste0(value_name, " [", units_str, "]") - } + y_label <- array_y_label(x, default = "Value", per_log_size = per_log_size) animate_plotly(df, params, log_x, log_y, y_label, wlim, llim, ylim, size_axis = size_axis, + density_wrt = array_density_wrt(x), + per_log_size = per_log_size, frame_duration = frame_duration, transition_duration = transition_duration, easing = easing) diff --git a/R/ArraySpeciesBySize-class.R b/R/ArraySpeciesBySize-class.R index 14ecc0c27..c265580f5 100644 --- a/R/ArraySpeciesBySize-class.R +++ b/R/ArraySpeciesBySize-class.R @@ -238,6 +238,11 @@ print.summary.ArraySpeciesBySize <- function(x, ...) { #' \item{`size_axis`}{Whether to plot size as weight (`"w"`, default) or #' length (`"l"`), using the allometric weight-length relationship. #' Densities are transformed to match the chosen axis.} +#' \item{`per_log_size`}{For an array that holds a density, whether to plot it +#' per logarithmic size (`TRUE`) rather than per size (`FALSE`). The +#' default, `NULL`, plots the density as it stands. Unlike `size_axis` this +#' needs no weight-length relationship, so it is available for the resource +#' classes too. An error for an array that does not hold a density.} #' } #' #' Additional argument for [plot.ArrayTimeBySpecies()]: @@ -308,6 +313,11 @@ NULL #' minimum or maximum. #' @param size_axis Whether to plot size as weight (`"w"`, default) or #' length (`"l"`), using the allometric weight-length relationship. +#' @param per_log_size For an array that holds a density, whether to plot it per +#' logarithmic size (`TRUE`) rather than per size (`FALSE`). The default, +#' `NULL`, plots the density as it stands. Unlike `size_axis` this needs no +#' weight-length relationship, so it is available for the resource classes +#' too. An error for an array that does not hold a density. #' @param total A boolean value that determines whether the total over #' all selected species is plotted as well. Default is `FALSE`. #' @param background A boolean value that determines whether background @@ -334,9 +344,11 @@ plot.ArraySpeciesBySize <- function(x, species = NULL, wlim = c(NA, NA), llim = c(NA, NA), ylim = c(NA, NA), size_axis = c("w", "l"), + per_log_size = NULL, total = FALSE, background = TRUE, y_ticks = 6, ...) { size_axis <- plot_size_axis(size_axis) + check_per_log_size(x, per_log_size) log_y <- array_log_y(x, log_y, log, !missing(log_y)) log_axes <- parsePlotLog(log, log_x = log_x, log_y = log_y) log_x <- log_axes$log_x @@ -350,7 +362,8 @@ plot.ArraySpeciesBySize <- function(x, species = NULL, x, species = species, all.sizes = all.sizes, wlim = wlim, total = total, background = background) plot_dat <- convert_plot_density_axis(plot_dat, params, size_axis, - density_wrt = array_density_wrt(x)) + density_wrt = array_density_wrt(x), + per_log_size = per_log_size) if (identical(size_axis, "l")) { plot_dat <- filter_plot_length_limits(plot_dat, llim) } @@ -358,7 +371,8 @@ plot.ArraySpeciesBySize <- function(x, species = NULL, if (return_data) return(plot_dat) ylim <- array_ylim(x, ylim, log_y, plot_dat[[2]]) - y_label <- array_y_label(x, default = "Rate", size_axis = size_axis) + y_label <- array_y_label(x, default = "Rate", size_axis = size_axis, + per_log_size = per_log_size) plotDataFrame(plot_dat, params, xlab = plot_size_xlab(size_axis), ylab = y_label, @@ -462,6 +476,12 @@ parsePlotLog <- function(log, log_x = FALSE, log_y = FALSE) { #' length (`"l"`), using the allometric weight-length relationship. Not #' available for the resource classes, because the weight-length #' relationship is a species parameter.} +#' \item{`per_log_size`}{For an array that holds a density, whether to plot +#' it per logarithmic size (`TRUE`) rather than per size (`FALSE`). The +#' default, `NULL`, plots the density as it stands. Unlike `size_axis` +#' this needs no weight-length relationship, so it is available for the +#' resource classes too. An error for an array that does not hold a +#' density.} #' } #' #' **For `ArrayTimeBySpecies` methods:** @@ -505,10 +525,12 @@ plot2.ArraySpeciesBySize <- function(x, y, name1 = "First", name2 = "Second", y_ticks = 6, all.sizes = FALSE, wlim = c(NA, NA), llim = c(NA, NA), - size_axis = c("w", "l"), ...) { + size_axis = c("w", "l"), + per_log_size = NULL, ...) { check_plot2_compatible(x, y, "ArraySpeciesBySize") compare_array_metadata(x, y) size_axis <- plot_size_axis(size_axis) + check_per_log_size(x, per_log_size) log_axes <- parsePlotLog(log, log_x = log_x, log_y = log_y) log_x <- log_axes$log_x log_y <- log_axes$log_y @@ -517,7 +539,8 @@ plot2.ArraySpeciesBySize <- function(x, y, name1 = "First", name2 = "Second", length(ylim) == 2) params <- attr(x, "params") - y_label <- array_y_label(x, default = "Rate", size_axis = size_axis) + y_label <- array_y_label(x, default = "Rate", size_axis = size_axis, + per_log_size = per_log_size) plot_dat1 <- prepare_ArraySpeciesBySize_plot_data( x, species = species, all.sizes = all.sizes, wlim = wlim, total = total, background = background) @@ -534,7 +557,8 @@ plot2.ArraySpeciesBySize <- function(x, y, name1 = "First", name2 = "Second", ylim = ylim, y_ticks = y_ticks, legend_var = "Legend", size_axis = size_axis, - density_wrt = array_density_wrt(x)) + density_wrt = array_density_wrt(x), + per_log_size = per_log_size) } #' Plot relative difference between two mizer arrays @@ -776,21 +800,51 @@ array_density_wrt <- function(x) { if (identical(array_type(x), "density")) "w" else NA_character_ } -array_units <- function(x, size_axis = "w") { +array_units <- function(x, size_axis = "w", per_log_size = NULL) { density_wrt <- array_density_wrt(x) - convert_density_units(attr(x, "units"), density_wrt, - density_target_measure(density_wrt, size_axis)) + target <- density_target_measure(density_wrt, size_axis, per_log_size) + convert_density_units(attr(x, "units"), density_wrt, target) } -array_y_label <- function(x, default = "Value", size_axis = "w") { +array_y_label <- function(x, default = "Value", size_axis = "w", + per_log_size = NULL) { value_name <- attr(x, "value_name") %||% default - units_str <- array_units(x, size_axis) + # A density per logarithmic size is a different quantity from the density + # itself and has to say so, since its units no longer distinguish it. + if (isTRUE(per_log_size) && !is.na(array_density_wrt(x))) { + value_name <- paste0(value_name, + if (identical(plot_size_axis(size_axis), "l")) + " in log length" else " in log weight") + } + units_str <- array_units(x, size_axis, per_log_size) if (!is.null(units_str) && nzchar(units_str)) { value_name <- paste0(value_name, " [", units_str, "]") } value_name } +#' Check that `per_log_size` applies to a mizer array +#' +#' Expressing values per logarithmic size only means anything for a density, +#' so asking for it on anything else is an argument error rather than something +#' to be quietly ignored — which is what `...` used to do with it. +#' +#' @param x A mizer array object. +#' @param per_log_size The `per_log_size` argument of the plot method. +#' @return `per_log_size`, invisibly, if it applies. +#' @keywords internal +check_per_log_size <- function(x, per_log_size) { + if (!is.null(per_log_size)) { + assert_that(is.flag(per_log_size), noNA(per_log_size)) + if (is.na(array_density_wrt(x))) { + stop("`per_log_size` only applies to an array that holds a ", + "density, but this one holds a value of type `", + array_type(x), "`.") + } + } + invisible(per_log_size) +} + #' Add lines to an existing plot #' #' `r lifecycle::badge("experimental")` @@ -844,6 +898,12 @@ array_y_label <- function(x, default = "Value", size_axis = "w") { #' length (`"l"`), using the allometric weight-length relationship. Not #' available for the resource classes, because the weight-length #' relationship is a species parameter.} +#' \item{`per_log_size`}{For an array that holds a density, whether to plot +#' it per logarithmic size (`TRUE`) rather than per size (`FALSE`). The +#' default, `NULL`, plots the density as it stands. Unlike `size_axis` +#' this needs no weight-length relationship, so it is available for the +#' resource classes too. An error for an array that does not hold a +#' density.} #' } #' #' **For `ArrayTimeBySpecies` methods:** @@ -894,6 +954,7 @@ addPlot.ArraySpeciesBySize <- function(plot, x, species = NULL, wlim = c(NA, NA), llim = c(NA, NA), size_axis = c("w", "l"), + per_log_size = NULL, ...) { if (!inherits(plot, "ggplot")) { stop("The `plot` argument must be a ggplot object.") @@ -903,6 +964,7 @@ addPlot.ArraySpeciesBySize <- function(plot, x, species = NULL, alpha >= 0, alpha <= 1) size_axis <- plot_size_axis(size_axis) + check_per_log_size(x, per_log_size) assert_that(length(wlim) == 2, length(llim) == 2) @@ -912,14 +974,15 @@ addPlot.ArraySpeciesBySize <- function(plot, x, species = NULL, total = total, background = background) params <- attr(x, "params") plot_dat <- convert_plot_density_axis(plot_dat, params, size_axis, - density_wrt = array_density_wrt(x)) + density_wrt = array_density_wrt(x), + per_log_size = per_log_size) if (identical(size_axis, "l")) { plot_dat <- filter_plot_length_limits(plot_dat, llim) } x_var <- plot_size_x_var(size_axis) y_var <- names(plot_dat)[2] check_addPlot_compatible(plot, x_var = x_var, y_var = y_var, - units = array_units(x, size_axis)) + units = array_units(x, size_axis, per_log_size)) mapping <- aes(x = .data[[x_var]], y = .data[[y_var]], group = .data[["Species"]]) diff --git a/R/ArrayTimeBySpeciesBySize-class.R b/R/ArrayTimeBySpeciesBySize-class.R index fac9712bf..ade6d64b4 100644 --- a/R/ArrayTimeBySpeciesBySize-class.R +++ b/R/ArrayTimeBySpeciesBySize-class.R @@ -192,6 +192,10 @@ print.summary.ArrayTimeBySpeciesBySize <- function(x, ...) { #' @param ylim A numeric vector of length two providing lower and upper #' limits for the value (y) axis. Use `NA` to refer to the existing #' minimum or maximum. +#' @param per_log_size For an array that holds a density, whether to plot it +#' per logarithmic size (`TRUE`) rather than per size (`FALSE`). The default, +#' `NULL`, plots the density as it stands. An error for an array that does not +#' hold a density. #' @param size_axis Whether to plot size as weight (`"w"`, default) or #' length (`"l"`), using the allometric weight-length relationship. #' @param total A boolean value that determines whether the total over @@ -217,6 +221,7 @@ plot.ArrayTimeBySpeciesBySize <- function(x, species = NULL, time = NULL, wlim = c(NA, NA), llim = c(NA, NA), ylim = c(NA, NA), size_axis = c("w", "l"), + per_log_size = NULL, total = FALSE, background = TRUE, y_ticks = 6, ...) { params <- attr(x, "params") @@ -243,6 +248,7 @@ plot.ArrayTimeBySpeciesBySize <- function(x, species = NULL, time = NULL, log_x = log_x, log_y = log_y, log = log, wlim = wlim, ylim = ylim, llim = llim, size_axis = size_axis, + per_log_size = per_log_size, total = total, background = background, y_ticks = y_ticks, ...) } @@ -373,6 +379,7 @@ animate.ArrayTimeBySpeciesBySize <- function(x, species = NULL, ylim = c(NA, NA), tlim = c(NA, NA), size_axis = c("w", "l"), + per_log_size = NULL, total = FALSE, background = TRUE, frame_duration = 500, @@ -385,6 +392,7 @@ animate.ArrayTimeBySpeciesBySize <- function(x, species = NULL, is.string(easing), length(wlim) == 2, length(llim) == 2, length(ylim) == 2) size_axis <- plot_size_axis(size_axis) + check_per_log_size(x, per_log_size) log_axes <- parsePlotLog(log, log_x = log_x, log_y = log_y) log_x <- log_axes$log_x log_y <- log_axes$log_y @@ -449,12 +457,14 @@ animate.ArrayTimeBySpeciesBySize <- function(x, species = NULL, df <- rbind(df, total_sums[, names(df)]) } - y_label <- array_y_label(x, default = "Value", size_axis = size_axis) + y_label <- array_y_label(x, default = "Value", size_axis = size_axis, + per_log_size = per_log_size) animate_plotly(df, params, log_x, log_y, y_label, wlim, llim, ylim, size_axis = size_axis, density_wrt = array_density_wrt(x), + per_log_size = per_log_size, frame_duration = frame_duration, transition_duration = transition_duration, easing = easing) diff --git a/R/animateSpectra.R b/R/animateSpectra.R index d9d9b2c8a..c3250fa0b 100644 --- a/R/animateSpectra.R +++ b/R/animateSpectra.R @@ -45,6 +45,12 @@ #' @param log A character string specifying which axes to log-transform: #' `"x"`, `"y"`, `"xy"` or `""`. If supplied, this overrides `log_x` #' and `log_y`. +#' @param per_log_size For an array that holds a density, whether to animate it +#' per logarithmic size (`TRUE`) rather than per size (`FALSE`). The default, +#' `NULL`, animates the density as it stands. Unlike `size_axis` this needs no +#' weight-length relationship, so the `ArrayTimeByResourceBySize` method takes +#' it too. An error for an array that does not hold a density. The `MizerSim` +#' method has its own `per_log_size`, described below. #' @param size_axis Whether to plot size as weight (`"w"`, default) or length #' (`"l"`), using the allometric weight-length relationship. Number and #' biomass densities are transformed to match the chosen axis. The @@ -237,11 +243,13 @@ animate_plotly <- function(df, params, log_x, log_y, y_label, ylim = c(NA, NA), size_axis = "w", density_wrt = NA_character_, + per_log_size = NULL, frame_duration = 500, transition_duration = 500, easing = "linear") { size_axis <- plot_size_axis(size_axis) df <- convert_plot_density_axis(df, params, size_axis, density_wrt = density_wrt, + per_log_size = per_log_size, value_col = "value") x_var <- plot_size_x_var(size_axis) legend_name_order <- intersect(names(params@linecolour), diff --git a/R/plots.R b/R/plots.R index f2a4f6247..d2b80013e 100644 --- a/R/plots.R +++ b/R/plots.R @@ -406,6 +406,8 @@ plotHover.mizer_plot <- function(x = ggplot2::last_plot(), ..., #' @param density_wrt The measure the values are a density with respect to, see #' [density_measures]. `NA` (the default) means the values are not a density #' and are left alone when the size axis changes. +#' @param per_log_size Whether to express a density per logarithmic size. +#' `NULL` (the default) keeps whichever the values already are. #' @return A `mizer_plot` (ggplot2) object. #' @keywords internal plotComparisonDataFrame <- function(frame1, frame2, params, @@ -416,7 +418,8 @@ plotComparisonDataFrame <- function(frame1, frame2, params, y_ticks = 6, highlight = NULL, legend_var = "Legend", size_axis = NULL, - density_wrt = NA_character_) { + density_wrt = NA_character_, + per_log_size = NULL) { assert_that(is.data.frame(frame1), is.data.frame(frame2), is(params, "MizerParams")) @@ -440,6 +443,7 @@ plotComparisonDataFrame <- function(frame1, frame2, params, size_axis <- plot_size_axis(size_axis) frame <- convert_plot_density_axis(frame, params, size_axis, density_wrt = density_wrt, + per_log_size = per_log_size, species_col = group_var, value_col = y_var) x_var <- plot_size_x_var(size_axis) @@ -825,25 +829,29 @@ validate_density_wrt <- function(density_wrt) { density_wrt } -#' The density measure a plot against a given size axis calls for +#' The density measure a plot calls for #' -#' Plotting against a length axis turns a density with respect to weight into a -#' density with respect to length, and a density with respect to logarithmic -#' weight into one with respect to logarithmic length. Whether the density is -#' per size or per logarithmic size is a property of the values and is left -#' alone; only the size variable follows the axis. +#' A density is expressed with respect to two independent choices: the size +#' variable, which follows `size_axis`, and whether it is per size or per +#' logarithmic size, which follows `per_log_size`. Plotting against a length +#' axis therefore turns a density with respect to weight into one with respect +#' to length, and a density with respect to logarithmic weight into one with +#' respect to logarithmic length. #' #' @param density_wrt The measure the values are a density with respect to, see #' [density_measures]. #' @param size_axis Either `"w"` (weight) or `"l"` (length). +#' @param per_log_size Whether to express the values per logarithmic size. +#' `NULL` (the default) keeps whichever the values already are. #' @return The density measure to express the values in, or `NA_character_` if #' the values are not a density. #' @keywords internal -density_target_measure <- function(density_wrt, size_axis) { +density_target_measure <- function(density_wrt, size_axis, + per_log_size = NULL) { density_wrt <- validate_density_wrt(density_wrt) if (is.na(density_wrt)) return(NA_character_) - paste0(if (startsWith(density_wrt, "log_")) "log_" else "", - plot_size_axis(size_axis)) + per_log_size <- per_log_size %||% startsWith(density_wrt, "log_") + paste0(if (isTRUE(per_log_size)) "log_" else "", plot_size_axis(size_axis)) } #' Factor relating a density measure to a density with respect to weight @@ -892,9 +900,15 @@ density_size_unit <- function(measure) { #' Restate the units of a density in a different density measure #' -#' The size unit is swapped inside the two spellings mizer uses for a per-size -#' factor, `1/g` and `g^-1`. Units that state no per-size factor are returned -#' unchanged, since there is then nothing to identify as the size unit. +#' The per-size factor is found in either of the two spellings mizer uses for +#' it, `1/g` and `g^-1`, and is then swapped for the size unit of the target +#' measure. A density per *logarithmic* size carries no size unit at all — a +#' number per log weight interval is just a number — so converting to one +#' removes the factor instead: `1/g` becomes dimensionless and `g^-1/year` +#' becomes `1/year`. +#' +#' Units that state no per-size factor are returned unchanged, since there is +#' then nothing to identify as the size unit. #' #' @param units The units of the values, possibly `NULL`. #' @param from,to Density measures, see [density_measures]. @@ -908,9 +922,14 @@ convert_density_units <- function(units, from, to) { return(if (is.na(to_sym)) units else paste0("1/", to_sym)) } from_sym <- density_size_unit(from) - if (is.na(from_sym) || is.na(to_sym)) return(units) - units <- sub(paste0("\\b1/", from_sym, "\\b"), paste0("1/", to_sym), units) - sub(paste0("\\b", from_sym, "\\^-1"), paste0(to_sym, "^-1"), units) + if (is.na(from_sym)) return(units) + # Replacing the per-size factor by "1" removes it; a units string that is + # then nothing but "1" is better shown as no units at all. + replacement <- if (is.na(to_sym)) "1" else paste0("1/", to_sym) + units <- sub(paste0("\\b1/", from_sym, "\\b"), replacement, units) + replacement <- if (is.na(to_sym)) "1" else paste0(to_sym, "^-1") + units <- sub(paste0("\\b", from_sym, "\\^-1"), replacement, units) + if (identical(units, "1")) "" else units } #' Express plotting data on the requested size axis @@ -920,13 +939,17 @@ convert_density_units <- function(units, from, to) { #' them in the density measure that axis calls for (see #' [density_target_measure()]). Values that are not a density are left alone. #' -#' The Jacobian is a per-species quantity, so rows whose species is not one of -#' the model's species — the "Total" row, for instance — cannot be converted and -#' are dropped whenever a conversion is needed. +#' Anything involving a length is a per-species quantity, because the +#' weight-length relationship is, so rows whose species is not one of the +#' model's species — the "Total" row, for instance — are dropped when a length +#' is needed. Going from a density per size to one per logarithmic size needs no +#' length, and keeps those rows. #' #' @inheritParams convert_plot_size_axis #' @param density_wrt The measure the values are a density with respect to, see #' [density_measures]. `NA` (the default) means the values are not a density. +#' @param per_log_size Whether to express the values per logarithmic size. +#' `NULL` (the default) keeps whichever the values already are. #' @param value_col Name or index of the value column. Defaults to the second #' column. #' @return The plotting data with its size coordinate, and where called for its @@ -935,11 +958,12 @@ convert_density_units <- function(units, from, to) { #' @keywords internal convert_plot_density_axis <- function(plot_dat, params, size_axis, density_wrt = NA_character_, + per_log_size = NULL, species_col = "Species", value_col = 2) { size_axis <- plot_size_axis(size_axis) density_wrt <- validate_density_wrt(density_wrt) - target <- density_target_measure(density_wrt, size_axis) + target <- density_target_measure(density_wrt, size_axis, per_log_size) needs_jacobian <- !is.na(target) && !identical(target, density_wrt) if (identical(size_axis, "w") && !needs_jacobian) { return(plot_dat) @@ -947,18 +971,29 @@ convert_plot_density_axis <- function(plot_dat, params, size_axis, if (is.numeric(value_col)) { value_col <- names(plot_dat)[[value_col]] } - # Both a length axis and the Jacobian need the lengths, and the Jacobian - # needs the weights as well. - plot_dat <- convert_plot_size_axis(plot_dat, params, "l", - species_col = species_col, - drop_w = FALSE) + # A length axis needs the lengths, and so does any Jacobian that involves + # one. The weights are kept because the Jacobians are written in terms of + # both. + needs_length <- identical(size_axis, "l") || + isTRUE(density_wrt %in% c("l", "log_l")) + if (needs_length) { + plot_dat <- convert_plot_size_axis(plot_dat, params, "l", + species_col = species_col, + drop_w = FALSE) + } if (needs_jacobian && nrow(plot_dat) > 0) { - species_idx <- match(as.character(plot_dat[[species_col]]), - as.character(params@species_params$species)) + if (needs_length) { + species_idx <- match(as.character(plot_dat[[species_col]]), + as.character(params@species_params$species)) + l <- plot_dat$l + b <- params@species_params$b[species_idx] + } else { + # Unused by a Jacobian between two weight-based measures. Passing + # NA rather than a placeholder keeps a mistake here loud. + l <- b <- rep(NA_real_, nrow(plot_dat)) + } plot_dat[[value_col]] <- plot_dat[[value_col]] * - density_measure_jacobian(density_wrt, target, - plot_dat$w, plot_dat$l, - params@species_params$b[species_idx]) + density_measure_jacobian(density_wrt, target, plot_dat$w, l, b) } x_var <- plot_size_x_var(size_axis) plot_dat[, c(x_var, setdiff(names(plot_dat), c("l", "w"))), drop = FALSE] diff --git a/inst/skills/analyse-and-plot/SKILL.md b/inst/skills/analyse-and-plot/SKILL.md index 95a4d2290..359ddfefb 100644 --- a/inst/skills/analyse-and-plot/SKILL.md +++ b/inst/skills/analyse-and-plot/SKILL.md @@ -294,10 +294,14 @@ that declare themselves densities: | `initialN(params)`, `finalN(sim)`, `N(sim)`, `get_initial_n(params)` | consumer number density, per gram | | `initialNResource(params)`, `finalNResource(sim)`, `NResource(sim)` | resource number density, per gram | | `resource_capacity(params)` | resource carrying capacity, per gram | -| `getFluxGradient(params)` | rate of change of the number density, per gram per year | +| `getFluxGradient(params)` | rate of change of the flux, per gram per year | -The factors are built from the length-weight relationship $w = a\, l^b$ of each -species, taken from the `a` and `b` columns of `species_params`: +Which density you get is set by two independent arguments, accepted by `plot()`, +`plot2()`, `addPlot()` and `animate()` as well as by the spectrum plots: +`size_axis` chooses the size variable and `per_log_size` chooses whether the +values are per size or per logarithmic size. The factors are built from the +length-weight relationship $w = a\, l^b$ of each species, taken from the `a` and +`b` columns of `species_params`: | Argument | Factor | |---|---|---| @@ -305,16 +309,15 @@ species, taken from the `a` and `b` columns of `species_params`: | `size_axis = "l"`, `per_log_size = FALSE` | $dw/dl = b\, w / l$ | | `size_axis = "l"`, `per_log_size = TRUE` | $dw / d\log l = b\, w$ | -Two things to watch: +```r +plot(initialN(params), per_log_size = TRUE) # per log weight +plot(initialN(params), size_axis = "l", per_log_size = TRUE) # per log length +plot(initialNResource(params), per_log_size = TRUE) # resource too +``` -- **The resource is dropped from a length axis.** The resource spectrum has no - length-weight relationship, so `plotSpectra(params, size_axis = "l")` shows the - species only. Use `size_axis = "w"` to see the resource. -- **`plot()` on a density array has no `per_log_size` argument.** Mizer arrays - are indexed by the weight grid, so an array that holds a density holds one per - gram; `plot()` honours `size_axis` but nothing switches an array between per - size and per log size. Use `plotSpectra()` when you want a per-log-size - spectrum. +`per_log_size` needs no weight-length relationship, only `size_axis` does, so +unlike a length axis it is available for the resource classes. Asking for it on +an array that does not hold a density is an error rather than being ignored. ### Plotting proportions @@ -351,6 +354,10 @@ plotSpectra(sim, species = c("Cod", "Herring"), resource = FALSE) plotSpectra(sim, biomass = TRUE, size_axis = "l") # biomass density against length ``` +**The resource is dropped from a length axis.** The resource spectrum has no + length-weight relationship, so `plotSpectra(params, size_axis = "l")` shows the + species only. Use `size_axis = "w"` to see the resource. + ### Which density a spectrum plot shows `plotSpectra()`, `plotSpectra2()`, `plotCDF()`, `plotCDF2()` and `animate()` @@ -386,9 +393,6 @@ plotCDF(NS_sim, biomass = FALSE, normalise = FALSE) ### Comparing two size distributions -For whole spectra use the functions in the table below; for any other rate array -use `plot2()` and `plotRelative()` from the array toolkit above. - | Function | Shows | |---|---| | `plotSpectra2(object1, object2, name1, name2)` | two abundance spectra overlaid | @@ -401,9 +405,9 @@ plotSpectraRelative(params, params2) # 2 (N2 - N1) / (N1 + N2) plotCDF2(sim, sim2, "Unfished", "Fished") ``` -### Animating spectra through time +## Animating through time -`animate()` plays a spectrum or rate array through the course of a simulation +`animate()` plays a spectrum or array through the course of a simulation (`animateSpectra()` is a retained alias). ```r @@ -412,6 +416,8 @@ animate(getFMort(sim)) # an ArrayTimeBySpeciesBySize over time animate(NResource(sim)) # an ArrayTimeByResourceBySize over time ``` +`animate()` accepts most of the common arguments from `plot()`. + ## Dedicated plot functions Besides the spectrum plots above, mizer has a dedicated `plot…()` function for diff --git a/man/addPlot.Rd b/man/addPlot.Rd index 270d1151f..deb3aeec0 100644 --- a/man/addPlot.Rd +++ b/man/addPlot.Rd @@ -78,6 +78,12 @@ refer to the existing minimum or maximum.} length (\code{"l"}), using the allometric weight-length relationship. Not available for the resource classes, because the weight-length relationship is a species parameter.} +\item{\code{per_log_size}}{For an array that holds a density, whether to plot +it per logarithmic size (\code{TRUE}) rather than per size (\code{FALSE}). The +default, \code{NULL}, plots the density as it stands. Unlike \code{size_axis} +this needs no weight-length relationship, so it is available for the +resource classes too. An error for an array that does not hold a +density.} } \strong{For \code{ArrayTimeBySpecies} methods:} diff --git a/man/animate.Rd b/man/animate.Rd index a0489e299..243211437 100644 --- a/man/animate.Rd +++ b/man/animate.Rd @@ -69,6 +69,13 @@ limit at that end. Default is \code{c(NA, NA)}.} biomass densities are transformed to match the chosen axis. The \code{ArrayTimeByResourceBySize} method supports only \code{"w"}.} +\item{per_log_size}{For an array that holds a density, whether to animate it +per logarithmic size (\code{TRUE}) rather than per size (\code{FALSE}). The default, +\code{NULL}, animates the density as it stands. Unlike \code{size_axis} this needs no +weight-length relationship, so the \code{ArrayTimeByResourceBySize} method takes +it too. An error for an array that does not hold a density. The \code{MizerSim} +method has its own \code{per_log_size}, described below.} + \item{total}{A boolean value that determines whether the total over all selected species is plotted as an additional trace called \code{"Total"}. Default is \code{FALSE}. Not used by the \code{ArrayTimeByResourceBySize} method, diff --git a/man/check_per_log_size.Rd b/man/check_per_log_size.Rd new file mode 100644 index 000000000..f44340626 --- /dev/null +++ b/man/check_per_log_size.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ArraySpeciesBySize-class.R +\name{check_per_log_size} +\alias{check_per_log_size} +\title{Check that \code{per_log_size} applies to a mizer array} +\usage{ +check_per_log_size(x, per_log_size) +} +\arguments{ +\item{x}{A mizer array object.} + +\item{per_log_size}{The \code{per_log_size} argument of the plot method.} +} +\value{ +\code{per_log_size}, invisibly, if it applies. +} +\description{ +Expressing values per logarithmic size only means anything for a density, +so asking for it on anything else is an argument error rather than something +to be quietly ignored — which is what \code{...} used to do with it. +} +\keyword{internal} diff --git a/man/convert_density_units.Rd b/man/convert_density_units.Rd index e125dbd2a..8c8d00d8d 100644 --- a/man/convert_density_units.Rd +++ b/man/convert_density_units.Rd @@ -15,8 +15,15 @@ convert_density_units(units, from, to) The units expressed in the \code{to} measure. } \description{ -The size unit is swapped inside the two spellings mizer uses for a per-size -factor, \code{1/g} and \code{g^-1}. Units that state no per-size factor are returned -unchanged, since there is then nothing to identify as the size unit. +The per-size factor is found in either of the two spellings mizer uses for +it, \code{1/g} and \code{g^-1}, and is then swapped for the size unit of the target +measure. A density per \emph{logarithmic} size carries no size unit at all — a +number per log weight interval is just a number — so converting to one +removes the factor instead: \code{1/g} becomes dimensionless and \code{g^-1/year} +becomes \code{1/year}. +} +\details{ +Units that state no per-size factor are returned unchanged, since there is +then nothing to identify as the size unit. } \keyword{internal} diff --git a/man/convert_plot_density_axis.Rd b/man/convert_plot_density_axis.Rd index b9d4c3960..3d6f0c77b 100644 --- a/man/convert_plot_density_axis.Rd +++ b/man/convert_plot_density_axis.Rd @@ -9,6 +9,7 @@ convert_plot_density_axis( params, size_axis, density_wrt = NA_character_, + per_log_size = NULL, species_col = "Species", value_col = 2 ) @@ -24,6 +25,9 @@ column.} \item{density_wrt}{The measure the values are a density with respect to, see \link{density_measures}. \code{NA} (the default) means the values are not a density.} +\item{per_log_size}{Whether to express the values per logarithmic size. +\code{NULL} (the default) keeps whichever the values already are.} + \item{species_col}{Name of the column identifying the species. Default is \code{"Species"}.} @@ -42,8 +46,10 @@ them in the density measure that axis calls for (see \code{\link[=density_target_measure]{density_target_measure()}}). Values that are not a density are left alone. } \details{ -The Jacobian is a per-species quantity, so rows whose species is not one of -the model's species — the "Total" row, for instance — cannot be converted and -are dropped whenever a conversion is needed. +Anything involving a length is a per-species quantity, because the +weight-length relationship is, so rows whose species is not one of the +model's species — the "Total" row, for instance — are dropped when a length +is needed. Going from a density per size to one per logarithmic size needs no +length, and keeps those rows. } \keyword{internal} diff --git a/man/density_target_measure.Rd b/man/density_target_measure.Rd index bf56386b2..41f8de4b3 100644 --- a/man/density_target_measure.Rd +++ b/man/density_target_measure.Rd @@ -2,25 +2,29 @@ % Please edit documentation in R/plots.R \name{density_target_measure} \alias{density_target_measure} -\title{The density measure a plot against a given size axis calls for} +\title{The density measure a plot calls for} \usage{ -density_target_measure(density_wrt, size_axis) +density_target_measure(density_wrt, size_axis, per_log_size = NULL) } \arguments{ \item{density_wrt}{The measure the values are a density with respect to, see \link{density_measures}.} \item{size_axis}{Either \code{"w"} (weight) or \code{"l"} (length).} + +\item{per_log_size}{Whether to express the values per logarithmic size. +\code{NULL} (the default) keeps whichever the values already are.} } \value{ The density measure to express the values in, or \code{NA_character_} if the values are not a density. } \description{ -Plotting against a length axis turns a density with respect to weight into a -density with respect to length, and a density with respect to logarithmic -weight into one with respect to logarithmic length. Whether the density is -per size or per logarithmic size is a property of the values and is left -alone; only the size variable follows the axis. +A density is expressed with respect to two independent choices: the size +variable, which follows \code{size_axis}, and whether it is per size or per +logarithmic size, which follows \code{per_log_size}. Plotting against a length +axis therefore turns a density with respect to weight into one with respect +to length, and a density with respect to logarithmic weight into one with +respect to logarithmic length. } \keyword{internal} diff --git a/man/plot.ArrayResourceBySize.Rd b/man/plot.ArrayResourceBySize.Rd index 5a1be0653..84b7f6bfc 100644 --- a/man/plot.ArrayResourceBySize.Rd +++ b/man/plot.ArrayResourceBySize.Rd @@ -12,6 +12,7 @@ log = NULL, wlim = c(NA, NA), ylim = c(NA, NA), + per_log_size = NULL, y_ticks = 6, ... ) @@ -39,6 +40,11 @@ minimum or maximum.} limits for the value (y) axis. Use \code{NA} to refer to the existing minimum or maximum.} +\item{per_log_size}{For an array that holds a density, whether to plot it +per logarithmic size (\code{TRUE}) rather than per size (\code{FALSE}). The default, +\code{NULL}, plots the density as it stands. An error for an array that does not +hold a density.} + \item{y_ticks}{The approximate number of ticks desired on the y axis.} \item{...}{Unused.} diff --git a/man/plot.ArraySpeciesBySize.Rd b/man/plot.ArraySpeciesBySize.Rd index a79d24181..897cf7ff1 100644 --- a/man/plot.ArraySpeciesBySize.Rd +++ b/man/plot.ArraySpeciesBySize.Rd @@ -17,6 +17,7 @@ llim = c(NA, NA), ylim = c(NA, NA), size_axis = c("w", "l"), + per_log_size = NULL, total = FALSE, background = TRUE, y_ticks = 6, @@ -62,6 +63,12 @@ minimum or maximum.} \item{size_axis}{Whether to plot size as weight (\code{"w"}, default) or length (\code{"l"}), using the allometric weight-length relationship.} +\item{per_log_size}{For an array that holds a density, whether to plot it per +logarithmic size (\code{TRUE}) rather than per size (\code{FALSE}). The default, +\code{NULL}, plots the density as it stands. Unlike \code{size_axis} this needs no +weight-length relationship, so it is available for the resource classes +too. An error for an array that does not hold a density.} + \item{total}{A boolean value that determines whether the total over all selected species is plotted as well. Default is \code{FALSE}.} diff --git a/man/plot.ArrayTimeBySpeciesBySize.Rd b/man/plot.ArrayTimeBySpeciesBySize.Rd index b129481b2..d196ba3b5 100644 --- a/man/plot.ArrayTimeBySpeciesBySize.Rd +++ b/man/plot.ArrayTimeBySpeciesBySize.Rd @@ -18,6 +18,7 @@ llim = c(NA, NA), ylim = c(NA, NA), size_axis = c("w", "l"), + per_log_size = NULL, total = FALSE, background = TRUE, y_ticks = 6, @@ -66,6 +67,11 @@ minimum or maximum.} \item{size_axis}{Whether to plot size as weight (\code{"w"}, default) or length (\code{"l"}), using the allometric weight-length relationship.} +\item{per_log_size}{For an array that holds a density, whether to plot it +per logarithmic size (\code{TRUE}) rather than per size (\code{FALSE}). The default, +\code{NULL}, plots the density as it stands. An error for an array that does not +hold a density.} + \item{total}{A boolean value that determines whether the total over all selected species is plotted as well. Default is \code{FALSE}.} diff --git a/man/plot.Rd b/man/plot.Rd index 07e41b212..e8ff6da2e 100644 --- a/man/plot.Rd +++ b/man/plot.Rd @@ -72,6 +72,11 @@ refer to the existing minimum or maximum.} \item{\code{size_axis}}{Whether to plot size as weight (\code{"w"}, default) or length (\code{"l"}), using the allometric weight-length relationship. Densities are transformed to match the chosen axis.} +\item{\code{per_log_size}}{For an array that holds a density, whether to plot it +per logarithmic size (\code{TRUE}) rather than per size (\code{FALSE}). The +default, \code{NULL}, plots the density as it stands. Unlike \code{size_axis} this +needs no weight-length relationship, so it is available for the resource +classes too. An error for an array that does not hold a density.} } Additional argument for \code{\link[=plot.ArrayTimeBySpecies]{plot.ArrayTimeBySpecies()}}: diff --git a/man/plot2.Rd b/man/plot2.Rd index 625caae06..932ef589f 100644 --- a/man/plot2.Rd +++ b/man/plot2.Rd @@ -88,6 +88,12 @@ refer to the existing minimum or maximum.} length (\code{"l"}), using the allometric weight-length relationship. Not available for the resource classes, because the weight-length relationship is a species parameter.} +\item{\code{per_log_size}}{For an array that holds a density, whether to plot +it per logarithmic size (\code{TRUE}) rather than per size (\code{FALSE}). The +default, \code{NULL}, plots the density as it stands. Unlike \code{size_axis} +this needs no weight-length relationship, so it is available for the +resource classes too. An error for an array that does not hold a +density.} } \strong{For \code{ArrayTimeBySpecies} methods:} diff --git a/man/plotComparisonDataFrame.Rd b/man/plotComparisonDataFrame.Rd index 836bb3e8f..aecaacaa8 100644 --- a/man/plotComparisonDataFrame.Rd +++ b/man/plotComparisonDataFrame.Rd @@ -20,7 +20,8 @@ plotComparisonDataFrame( highlight = NULL, legend_var = "Legend", size_axis = NULL, - density_wrt = NA_character_ + density_wrt = NA_character_, + per_log_size = NULL ) } \arguments{ @@ -53,6 +54,9 @@ line colour.} \item{density_wrt}{The measure the values are a density with respect to, see \link{density_measures}. \code{NA} (the default) means the values are not a density and are left alone when the size axis changes.} + +\item{per_log_size}{Whether to express a density per logarithmic size. +\code{NULL} (the default) keeps whichever the values already are.} } \value{ A \code{mizer_plot} (ggplot2) object. diff --git a/tests/testthat/test-ArrayResourceBySize-class.R b/tests/testthat/test-ArrayResourceBySize-class.R index f8a4ca793..033b52125 100644 --- a/tests/testthat/test-ArrayResourceBySize-class.R +++ b/tests/testthat/test-ArrayResourceBySize-class.R @@ -226,3 +226,23 @@ test_that("animate dispatches on ArrayTimeByResourceBySize", { expect_error(animate(n_resource_small, size_axis = "l"), "length axis is not available") }) + +test_that("plot() can express a resource density per logarithmic size", { + resource <- initialNResource(NS_params_small) + expect_identical(array_type(resource), "density") + by_weight <- plot(resource, return_data = TRUE) + + # The resource has no length axis, but per log weight needs no + # weight-length relationship and so is available + per_log <- plot(resource, per_log_size = TRUE, return_data = TRUE) + expect_equal(per_log[[2]], by_weight[[2]] * by_weight$w) + expect_identical(plot(resource, per_log_size = TRUE)$scales$ + get_scales("y")$name, + "Number density in log weight") + expect_identical(plot(resource)$scales$get_scales("y")$name, + "Number density [1/g]") + + # A length axis is still refused, and so is per_log_size on a non-density + expect_error(plot(resource_level(NS_params_small), per_log_size = TRUE), + "only applies to an array that holds a density") +}) diff --git a/tests/testthat/test-ArraySpeciesBySize-class.R b/tests/testthat/test-ArraySpeciesBySize-class.R index 1a2d79878..94e643e9d 100644 --- a/tests/testthat/test-ArraySpeciesBySize-class.R +++ b/tests/testthat/test-ArraySpeciesBySize-class.R @@ -234,6 +234,48 @@ test_that("array plots use the declared type", { by_weight[[2]]) }) +test_that("plot() can express a density per logarithmic size", { + density <- initialN(NS_params_small) + sp <- NS_params_small@species_params + by_weight <- plot(density, size_axis = "w", return_data = TRUE) + + # Per log weight is the density times the weight + per_log <- plot(density, per_log_size = TRUE, return_data = TRUE) + expect_identical(names(per_log)[[1]], "w") + expect_equal(per_log[[2]], by_weight[[2]] * by_weight$w) + + # Per log length is the density per length times the length + per_log_l <- plot(density, size_axis = "l", per_log_size = TRUE, + return_data = TRUE) + by_length <- plot(density, size_axis = "l", return_data = TRUE) + expect_identical(names(per_log_l)[[1]], "l") + expect_equal(per_log_l[[2]], by_length[[2]] * by_length$l) + + # per_log_size = FALSE is the density itself, the same as not asking + expect_equal(plot(density, per_log_size = FALSE, return_data = TRUE)[[2]], + by_weight[[2]]) + + # The label says which quantity is shown, since the units no longer do + expect_identical(plot(density, per_log_size = TRUE)$scales$ + get_scales("y")$name, + "Number density in log weight") + expect_identical(plot(density, size_axis = "l", per_log_size = TRUE)$scales$ + get_scales("y")$name, + "Number density in log length") + + # Asking for it on something that is not a density is an error, where it + # used to be swallowed by `...` + expect_error(plot(enc_small, per_log_size = TRUE), + "only applies to an array that holds a density") + expect_error(plot(density, per_log_size = "yes"), "not a flag") + + # No weight-length relationship is needed on a weight axis, so the total + # survives there + with_total <- plot(density, total = TRUE, per_log_size = TRUE, + return_data = TRUE) + expect_true(any(with_total$Species == "Total")) +}) + test_that("a proportion is plotted against the whole of [0, 1]", { feeding_level <- getFeedingLevel(NS_params_small) expect_identical(array_type(feeding_level), "proportion") diff --git a/tests/testthat/test-plots.R b/tests/testthat/test-plots.R index 918b02efe..b09e521c5 100644 --- a/tests/testthat/test-plots.R +++ b/tests/testthat/test-plots.R @@ -771,6 +771,74 @@ test_that("convert_plot_density_axis converts in both directions", { nrow(plot_dat)) }) +test_that("per_log_size chooses the measure independently of the size axis", { + # The size variable follows the axis, the log-ness follows per_log_size + expect_identical(density_target_measure("w", "w", TRUE), "log_w") + expect_identical(density_target_measure("w", "l", TRUE), "log_l") + expect_identical(density_target_measure("log_w", "w", FALSE), "w") + expect_identical(density_target_measure("log_w", "l", FALSE), "l") + # NULL keeps whichever the values already are + expect_identical(density_target_measure("w", "l", NULL), "l") + expect_identical(density_target_measure("log_w", "l", NULL), "log_l") + # Still nothing to do for values that are not a density + expect_identical(density_target_measure(NA, "w", TRUE), NA_character_) +}) + +test_that("convert_plot_density_axis expresses a density per logarithmic size", { + sp <- params@species_params + w <- params@w[1:5] + plot_dat <- data.frame(w = rep(w, 2), value = seq_len(10), + Species = rep(sp$species[1:2], each = 5)) + sp_idx <- match(as.character(plot_dat$Species), as.character(sp$species)) + b <- unname(sp$b[sp_idx]) + l <- w2l(plot_dat$w, sp[sp_idx, ]) + + # Per log weight is the density times the weight + per_log_w <- convert_plot_density_axis(plot_dat, params, "w", + density_wrt = "w", + per_log_size = TRUE) + expect_identical(names(per_log_w), c("w", "value", "Species")) + expect_equal(per_log_w$value, plot_dat$value * plot_dat$w) + + # Per log length is the density per length times the length + per_log_l <- convert_plot_density_axis(plot_dat, params, "l", + density_wrt = "w", + per_log_size = TRUE) + expect_identical(names(per_log_l), c("l", "value", "Species")) + expect_equal(per_log_l$value, plot_dat$value * b * plot_dat$w) + per_l <- convert_plot_density_axis(plot_dat, params, "l", + density_wrt = "w") + expect_equal(per_log_l$value, per_l$value * l) + + # per_log_size = FALSE undoes it again + log_dat <- plot_dat + log_dat$value <- per_log_w$value + expect_equal(convert_plot_density_axis(log_dat, params, "w", + density_wrt = "log_w", + per_log_size = FALSE)$value, + plot_dat$value) + + # Going to a logarithmic measure on a weight axis needs no length, so it + # keeps rows whose species has no weight-length relationship + with_total <- rbind(plot_dat, + data.frame(w = w, value = 1, Species = "Total")) + kept <- convert_plot_density_axis(with_total, params, "w", + density_wrt = "w", per_log_size = TRUE) + expect_identical(nrow(kept), nrow(with_total)) + expect_equal(kept$value, with_total$value * with_total$w) +}) + +test_that("convert_density_units drops the size unit for a logarithmic measure", { + # A number per log weight interval is just a number + expect_identical(convert_density_units("1/g", "w", "log_w"), "") + expect_identical(convert_density_units("1/cm", "l", "log_l"), "") + # and the rest of a compound unit survives + expect_identical(convert_density_units("g^-1/year", "w", "log_w"), + "1/year") + # Units with no per-size factor to remove are left alone + expect_identical(convert_density_units("g/year", "w", "log_w"), "g/year") +}) + test_that("convert_plot_spectrum_axis agrees with the density conversion", { plot_dat <- data.frame(w = params@w[1:5], value = seq_len(5), Species = params@species_params$species[[1]]) diff --git a/vignettes/cheatsheet-analysis-and-plotting.Rmd b/vignettes/cheatsheet-analysis-and-plotting.Rmd index 2c10c6e2e..99f8cce76 100644 --- a/vignettes/cheatsheet-analysis-and-plotting.Rmd +++ b/vignettes/cheatsheet-analysis-and-plotting.Rmd @@ -307,10 +307,14 @@ that declare themselves densities: | [`initialN(params)`](../reference/initialN-set.html), [`finalN(sim)`](../reference/finalN.html), [`N(sim)`](../reference/N.html), [`get_initial_n(params)`](../reference/get_initial_n.html) | consumer number density, per gram | | [`initialNResource(params)`](../reference/initialNResource-set.html), [`finalNResource(sim)`](../reference/finalN.html), [`NResource(sim)`](../reference/N.html) | resource number density, per gram | | [`resource_capacity(params)`](../reference/setResource.html) | resource carrying capacity, per gram | -| [`getFluxGradient(params)`](../reference/getFluxGradient.html) | rate of change of the number density, per gram per year | +| [`getFluxGradient(params)`](../reference/getFluxGradient.html) | rate of change of the flux, per gram per year | -The factors are built from the length-weight relationship $w = a\, l^b$ of each -species, taken from the `a` and `b` columns of [`species_params`](../reference/species_params.html): +Which density you get is set by two independent arguments, accepted by `plot()`, +`plot2()`, `addPlot()` and [`animate()`](../reference/animate.html) as well as by the spectrum plots: +`size_axis` chooses the size variable and `per_log_size` chooses whether the +values are per size or per logarithmic size. The factors are built from the +length-weight relationship $w = a\, l^b$ of each species, taken from the `a` and +`b` columns of [`species_params`](../reference/species_params.html): | Argument | Factor | |---|---|---| @@ -318,16 +322,15 @@ species, taken from the `a` and `b` columns of [`species_params`](../reference/s | `size_axis = "l"`, `per_log_size = FALSE` | $dw/dl = b\, w / l$ | | `size_axis = "l"`, `per_log_size = TRUE` | $dw / d\log l = b\, w$ | -Two things to watch: +```{r eval=FALSE} +plot(initialN(params), per_log_size = TRUE) # per log weight +plot(initialN(params), size_axis = "l", per_log_size = TRUE) # per log length +plot(initialNResource(params), per_log_size = TRUE) # resource too +``` -- **The resource is dropped from a length axis.** The resource spectrum has no - length-weight relationship, so [`plotSpectra(params, size_axis = "l")`](../reference/plotSpectra.html) shows the - species only. Use `size_axis = "w"` to see the resource. -- **`plot()` on a density array has no `per_log_size` argument.** Mizer arrays - are indexed by the weight grid, so an array that holds a density holds one per - gram; `plot()` honours `size_axis` but nothing switches an array between per - size and per log size. Use `plotSpectra()` when you want a per-log-size - spectrum. +`per_log_size` needs no weight-length relationship, only `size_axis` does, so +unlike a length axis it is available for the resource classes. Asking for it on +an array that does not hold a density is an error rather than being ignored. ### Plotting proportions @@ -347,7 +350,7 @@ wins. ## Plotting size spectra "Size spectrum" is just a different name for "density as a function of size". -`plotSpectra()` is the function you want for the plot size spectra. It +[`plotSpectra()`](../reference/plotSpectra.html) is the function you want for the plot size spectra. It plots the abundance or biomass density against size, one line per species, and unlike a plain `plot()` of a density array it also overlays the resource spectrum (`resource = TRUE`, the default) and any background species @@ -366,9 +369,13 @@ plotSpectra(sim, species = c("Cod", "Herring"), resource = FALSE) plotSpectra(sim, biomass = TRUE, size_axis = "l") # biomass density against length ``` +**The resource is dropped from a length axis.** The resource spectrum has no + length-weight relationship, so `plotSpectra(params, size_axis = "l")` shows the + species only. Use `size_axis = "w"` to see the resource. + ### Which density a spectrum plot shows -`plotSpectra()`, [`plotSpectra2()`](../reference/plotSpectra2.html), [`plotCDF()`](../reference/plotCDF.html), [`plotCDF2()`](../reference/plotCDF2.html) and [`animate()`](../reference/animate.html) +`plotSpectra()`, [`plotSpectra2()`](../reference/plotSpectra2.html), [`plotCDF()`](../reference/plotCDF.html), [`plotCDF2()`](../reference/plotCDF2.html) and `animate()` describe the plotted quantity with two independent logical arguments: | | `per_log_size = FALSE` | `per_log_size = TRUE` | @@ -401,9 +408,6 @@ plotCDF(NS_sim, biomass = FALSE, normalise = FALSE) ### Comparing two size distributions -For whole spectra use the functions in the table below; for any other rate array -use `plot2()` and `plotRelative()` from the array toolkit above. - | Function | Shows | |---|---| | [`plotSpectra2(object1, object2, name1, name2)`](../reference/plotSpectra2.html) | two abundance spectra overlaid | @@ -416,9 +420,11 @@ plotSpectraRelative(params, params2) # 2 (N2 - N1) / (N1 + N2) plotCDF2(sim, sim2, "Unfished", "Fished") ``` -### Animating spectra through time +--- + +## Animating through time -`animate()` plays a spectrum or rate array through the course of a simulation +`animate()` plays a spectrum or array through the course of a simulation ([`animateSpectra()`](../reference/animate.html) is a retained alias). ```{r eval=FALSE} @@ -427,6 +433,8 @@ animate(getFMort(sim)) # an ArrayTimeBySpeciesBySize over time animate(NResource(sim)) # an ArrayTimeByResourceBySize over time ``` +`animate()` accepts most of the common arguments from `plot()`. + --- ## Dedicated plot functions