Skip to content

Let mizer arrays say what kind of value they hold - #538

Merged
gustavdelius merged 3 commits into
masterfrom
array-type-attribute
Aug 16, 2026
Merged

Let mizer arrays say what kind of value they hold#538
gustavdelius merged 3 commits into
masterfrom
array-type-attribute

Conversation

@gustavdelius

Copy link
Copy Markdown
Member

Summary

Mizer arrays now carry a type attribute saying what kind of quantity they hold — "value" (the default), "density" or "proportion" — replacing the density_wrt attribute added earlier in this development cycle (unreleased, so nothing external depended on it).

Why the change. density_wrt could name any of four density measures ("w", "log_w", "l", "log_l"), but three 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; per-log-size quantities are computed on the fly inside plot data frames (plotSpectra() multiplies by w^power) and never stored. Carrying four values where two occur repeated the mistake the attribute was introduced to fix — the old units == "1/g" branch that never changed an outcome.

Meanwhile a distinction that does earn its keep was being 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 proportion-valued arrays were unknown to every plot.

What each type does

type Behaviour
"value" nothing special (default)
"density" multiplied by the Jacobian on a length axis, units restated 1/g1/cm
"proportion" linear y axis covering the whole of [0, 1]

The density-measure vocabulary stays as internal plumbing: plotSpectra(per_log_size = TRUE, size_axis = "l") genuinely needs the log_w measure and its b Jacobian, so only the array-facing attribute narrowed. array_density_wrt() survives as the one-line bridge between the two.

A "proportion" range is only ever widened to include the data, never narrowed to [0, 1]getCriticalFeedingLevel() and resource_level() can legitimately exceed 1. An explicit ylim always wins, and a logarithmic axis is left alone.

Arrays that declare no type still fall back to the old test on value_name / units, so existing code, extension packages and saved objects are unaffected.

Behaviour changes for users

  • plot() of a feeding level, critical feeding level, maturity(), repro_prop(), psi() or resource_level() now shows the full [0, 1] y range — the range plotFeedingLevel() has always used, so the dedicated function and the array plot finally agree.
  • plot(resource_level(params)) gets a linear y axis instead of a logarithmic one. Pass log_y = TRUE for the old axis.
  • Bug fix: plotFeedingLevel(include_critical = TRUE) no longer draws a critical feeding level above 1 off the top of the plot.
  • Extension packages: plotComparisonDataFrame() and the internal animate_plotly() take a single density_wrt argument in place of spectrum_power/spectrum_per_log_size, and the internal array_spectrum_power() is gone. The power interface of plotSpectra() and friends is unchanged.

All of this is recorded in NEWS.md and in inst/skills/upgrade-mizer-code/SKILL.md, with four new rows in its symptom index.

Testing

devtools::test(): 4232 passing, 0 failures. New tests cover the type attribute and its legacy inference, subsetting and slicing across all five array classes, proportion_ylim() (widening, explicit limits, log axes, non-finite values), values above 1 staying visible, and the plotFeedingLevel() fix. Two snapshot files were re-accepted for the attribute rename only. No new lints on any changed line.

Note on scope

Two things in this branch predate the work and came along with it:

  • inst/skills/analyse-and-plot/SKILL.md and quick-reference.md carry an in-progress restructuring of the plotting cheatsheet that was already in the working tree; my type and proportion sections sit on top of it, and vignettes/cheatsheet-analysis-and-plotting.Rmd is the regenerated result.
  • man/getMeanWeight.Rd was stale against its committed roxygen source and got resynced by devtools::document().

The docs/ website build in the working tree is deliberately not included.

🤖 Generated with Claude Code

gustavdelius and others added 3 commits August 15, 2026 18:58
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 <noreply@anthropic.com>
`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 <noreply@anthropic.com>
@gustavdelius

Copy link
Copy Markdown
Member Author

Added a second commit: per_log_size on the array plot methods (18d2d5b).

size_axis and per_log_size are the same operation — both change the measure a density is expressed with respect to, and both run through density_measure_jacobian() — but only the first was available on an array. plotSpectra() owned the other and takes a MizerParams/MizerSim rather than an array, so an arbitrary density array (getFluxGradient(), a hand-modified initialN(), a slice of N(sim)) could not be viewed per log size at all. And per_log_size = TRUE was silently swallowed by ..., returning the per-size plot with no warning.

plot(), plot2(), addPlot() and animate() now accept it for any array whose type is "density":

plot(initialN(params), per_log_size = TRUE)                   # x w
plot(initialN(params), size_axis = "l", per_log_size = TRUE)  # x b*w
plot(initialNResource(params), per_log_size = TRUE)           # the resource too

Three things worth noting:

  • The resource classes get it. per_log_size needs no weight-length relationship — only size_axis does — so unlike a length axis it works for NResource(), resource_capacity() and friends. That is where per-log-size viewing is most standard.
  • The "Total" row survives on a weight axis, since no per-species quantity is involved. Only conversions that need a length drop it.
  • biomass deliberately stays with plotSpectra(). Multiplying by w to count grams instead of individuals changes the quantity, not the measure, and only means anything for a number density. per_log_size changes the measure and applies to any density, which is why it belongs next to size_axis.

Asking for per_log_size on a non-density is now an error rather than being ignored.

This also fixed a latent bug the feature made reachable: convert_density_units() has to remove the per-size factor when converting to a logarithmic measure rather than swap it, so 1/g becomes dimensionless and g^-1/year becomes 1/year. The y label says which quantity is shown — "Number density in log weight" / "in log length", matching spectra_y_label() — since the units no longer distinguish it.

Suite is at 4266 passing, 0 failures. No new lints.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant