Nothing
# A small model that is genuinely at its steady state, built once here.
residual_params <- suppressMessages(
steady(newTraitParams(no_sp = 2, no_w = 20, max_w_max = 100,
min_w = 1e-3, w_pp_cutoff = 5, ks = 4,
reproduction_level = 0.25, info_level = 0),
tol = 1e-6, t_max = 500, progress_bar = FALSE, info_level = 0)
)
# A copy that has been knocked off it, by scaling one species' abundance.
off_steady_params <- local({
p <- residual_params
initialN(p)[1, ] <- initialN(p)[1, ] * 3
p
})
# Component dynamics for the tests of what the drift does and does not cover.
# The state is grown by a fixed exponential factor each step, so its relative
# rate of change is a known constant, independent of `dt` to first order.
e <- globalenv()
e$drifting_component <- function(params, n_other, component, dt, ...) {
n_other[[component]] * exp(2 * dt)
}
e$settled_component <- function(params, n_other, component, ...) {
n_other[[component]]
}
e$opaque_component <- function(params, n_other, component, ...) {
n_other[[component]]
}
# A settled model carrying one component that never settles.
component_params <- setComponent(
residual_params, "drifter", initial_value = c(1, 2, 3),
dynamics_fun = "drifting_component")
# getSteadyResidual ----
test_that("getSteadyResidual() returns a labelled ArraySpeciesBySize", {
res <- getSteadyResidual(residual_params)
expect_s3_class(res, "ArraySpeciesBySize")
expect_identical(dim(res), dim(initialN(residual_params)))
expect_identical(dimnames(res), dimnames(initialN(residual_params)))
expect_identical(attr(res, "units"), "1/year")
expect_identical(attr(res, "value_name"), "Biomass drift contribution")
# Each value is an integral over its bin, so it is drawn at the bin centre.
expect_identical(attr(res, "representation"), "average")
# The resource and the other components travel as attributes.
expect_length(attr(res, "resource"), length(residual_params@w_full))
expect_type(attr(res, "other"), "list")
# The two measures are different quantities and have to say so, or `plot2()`
# would draw one on top of the other without complaint.
pc <- getSteadyResidual(residual_params, measure = "per_capita")
expect_identical(attr(pc, "value_name"), "Per-capita rate of change")
expect_identical(attr(pc, "representation"), "point")
})
test_that("getSteadyResidual() rejects an unknown measure", {
expect_error(getSteadyResidual(residual_params, measure = "cell"),
"should be one of")
})
test_that("getSteadyResidual() is ~zero at a steady state and not otherwise", {
expect_lt(max(abs(rowSums(getSteadyResidual(residual_params)))), 1e-3)
expect_gt(max(abs(rowSums(getSteadyResidual(off_steady_params)))), 0.1)
})
test_that("the biomass measure sums over sizes to the biomass drift", {
# This is the property the default measure exists for: the array says where
# a model is unsteady in the same currency that `isSteady()` uses to decide
# whether it is. It has to hold under both quadrature schemes, since the bin
# weights differ between them.
for (bin_average in c(FALSE, TRUE)) {
p <- off_steady_params
second_order_w(p) <- c(bin_average = bin_average)
res <- getSteadyResidual(p)
rates <- mizer:::steady_rates(p)
biomass <- sizeIntegral(p, weighting = w(p), n = rates$n)
dBdt <- sizeIntegral(p, weighting = w(p), n = rates$dNdt)
expect_equal(rowSums(res), as.numeric(dBdt) / as.numeric(biomass),
ignore_attr = TRUE)
# The resource attribute is the same measure on the resource grid, so it
# sums to the resource drift, and the largest of all of them is the
# scalar every steady-state check is stated against.
wdw <- w_full(p) * dw_full(p)
resource_drift <- sum(rates$dn_pp_dt * wdw) / sum(rates$n_pp * wdw)
expect_equal(sum(attr(res, "resource")), resource_drift)
expect_equal(max(abs(c(rowSums(res), sum(attr(res, "resource"))))),
mizer:::steady_biomass_drift(p))
}
})
test_that("the per-capita measure is NA exactly where there are no fish", {
res <- getSteadyResidual(off_steady_params, measure = "per_capita")
expect_identical(is.na(unclass(res)) & TRUE,
initialN(off_steady_params) == 0)
})
test_that("the biomass measure reports every class, including empty ones", {
# `dN/dt` is well defined in a class with no fish in it — it can be filling
# up — so the default measure has nothing to withdraw. Only a species with
# no biomass at all has no relative rate of change of it.
p <- off_steady_params
initialN(p)[1, ncol(initialN(p))] <- 0
res <- getSteadyResidual(p)
expect_false(anyNA(res))
empty <- p
initialN(empty)[1, ] <- 0
res <- getSteadyResidual(empty)
expect_true(all(is.na(res[1, ])))
expect_false(anyNA(res[-1, ]))
})
test_that("a size class holding a trace cannot dominate the biomass measure", {
# What #570 reported. A trace of fish in the largest size class has nothing
# growing into it, so its per-capita rate is minus its mortality and stays
# there for ever while the density falls through 1e-100 and beyond. The
# default measure needs no cutoff to disregard it: the class holds no mass,
# so it contributes none of the drift.
p <- residual_params
j <- length(w(p))
n <- initialN(p)
n[1, j] <- 1e-100
initialN(p) <- n
res <- getSteadyResidual(p)
pc <- getSteadyResidual(p, measure = "per_capita")
expect_equal(unname(pc[1, j]), unname(-getMort(p)[1, j]))
# `all.sizes = TRUE` isolates the biomass rule from the size-range one:
# this class is above its species' `w_max`, so the default summary would
# leave it out on that ground alone.
expect_lt(summary(pc, all.sizes = TRUE)$per_species$Min[1], -0.01)
# The same class, weighted by the biomass it holds, is nothing at all.
expect_lt(abs(res[1, j]), 1e-90)
expect_lt(max(abs(rowSums(res))), 1e-3)
})
test_that("the biomass-share cutoff is validated", {
params <- NS_params_small
n <- initialN(params)
invalid_cutoffs <- list(c(0, 1), NA_real_, Inf, -0.1, 1.1)
for (cutoff in invalid_cutoffs) {
expect_error(negligible_cells(params, n, cutoff = cutoff),
"must be a finite number between 0 and 1", fixed = TRUE)
}
})
test_that("the biomass-share cutoff uses the selected quadrature", {
for (bin_average in c(FALSE, TRUE)) {
params <- NS_params_small
second_order_w(params) <- c(bin_average = bin_average)
n <- initialN(params) * 0
j <- ncol(n)
# Give the first and last bins exactly half the species' biomass under
# the selected quadrature. The top-bin weight is one-sided, so this also
# distinguishes the two quadrature schemes rather than merely scaling
# every bin by the same constant.
wdw <- bin_average_weight(w(params), params) * dw(params)
n[1, c(1, j)] <- 1 / wdw[c(1, j)]
negligible <- negligible_cells(params, n, cutoff = 0.5)
# A cell exactly on the cutoff is kept; the strict cutoff removes only
# cells below it. Species with no biomass have no relevant cells.
expect_identical(unname(which(!negligible[1, ])), c(1L, j))
expect_true(all(negligible[-1, ]))
}
})
test_that("getSteadyResidual() predicts the drift that project() produces", {
# This is the property that defines the residual: it is dN/dt. A projection
# over a short step must reproduce it, with an error that is first order in
# the step size. Checking the convergence rate rather than a fixed
# tolerance is what makes this a test of the identity rather than of one
# arbitrary step length.
predicted <- getSteadyResidual(off_steady_params, measure = "per_capita")
err <- vapply(c(1e-4, 1e-5), function(dt) {
sim <- project(off_steady_params, t_max = 2 * dt, dt = dt, t_save = dt,
progress_bar = FALSE)
n0 <- N(sim)[1, , ]
observed <- (N(sim)[2, , ] - n0) / (n0 * dt)
sel <- !is.na(predicted) & n0 > 0 & abs(predicted) > 1e-3
stats::median(abs((observed[sel] - predicted[sel]) / predicted[sel]))
}, numeric(1))
# Ten times the step, ten times the error, to within a generous factor.
expect_equal(err[[1]] / err[[2]], 10, tolerance = 0.5)
})
test_that("getSteadyResidual() honours the effort it is given", {
# Needs a model that is actually fished; the trait fixture above is not.
p <- NS_params_small
higher <- initial_effort(p) + 1
# Passing the effort must be the same as storing it in the model ...
stored <- p
initial_effort(stored) <- higher
expect_equal(unclass(getSteadyResidual(p, effort = higher)),
unclass(getSteadyResidual(stored)),
ignore_attr = TRUE)
# ... and must actually change the answer.
expect_false(isTRUE(all.equal(
unclass(getSteadyResidual(p, effort = higher)),
unclass(getSteadyResidual(p)), check.attributes = FALSE)))
})
test_that("getSteadyResidual() works under the second-order scheme", {
p <- residual_params
second_order_w(p) <- TRUE
expect_s3_class(getSteadyResidual(p), "ArraySpeciesBySize")
expect_true(all(is.finite(getSteadyResidual(p))))
pc <- getSteadyResidual(p, measure = "per_capita")
expect_true(all(is.finite(pc[initialN(p) > 0])))
})
test_that("getSteadyResidual() rejects a non-positive dt", {
expect_error(getSteadyResidual(residual_params, dt = 0))
expect_error(getSteadyResidual(residual_params, dt = -1))
})
# steady_biomass_drift ----
test_that("steady_biomass_drift() separates settled from unsettled models", {
expect_lt(steady_biomass_drift(residual_params), steady_residual_tol())
expect_gt(steady_biomass_drift(off_steady_params), steady_residual_tol())
})
test_that("steady_biomass_drift() is the relative rate of biomass change", {
# Check it against the biomass change an actual projection produces, which
# is what the number claims to be. The resource counts too, and on this
# model it is in fact the worst offender: tripling a consumer triples the
# predation on the resource.
dt <- 1e-4
sim <- project(off_steady_params, t_max = 2 * dt, dt = dt, t_save = dt,
progress_bar = FALSE)
b <- getBiomass(sim)
wdw <- off_steady_params@w_full * off_steady_params@dw_full
rb <- as.numeric(NResource(sim) %*% wdw)
observed <- max(abs((b[2, ] - b[1, ]) / (b[1, ] * dt)),
abs((rb[[2]] - rb[[1]]) / (rb[[1]] * dt)))
expect_equal(steady_biomass_drift(off_steady_params), observed,
tolerance = 1e-2)
})
test_that("steady_biomass_drift() ignores fast cells that hold no biomass", {
# The point of weighting by biomass: the per-cell maximum is dominated by
# the fastest-relaxing size classes, which carry negligible mass. The two
# measures must therefore disagree by orders of magnitude on a settled
# model, and it is the biomass one that reads as settled.
cell_max <- max(abs(getSteadyResidual(residual_params,
measure = "per_capita")),
na.rm = TRUE)
expect_lt(steady_biomass_drift(residual_params), cell_max)
})
# steady_drift_report ----
test_that("steady_drift_report() names the state variable responsible", {
# Reporting the number without the name is what sent the reporter of #589
# to the wrong place. Whichever variable wins, the name has to be the one
# that achieves the reported drift.
for (p in list(residual_params, off_steady_params)) {
report <- steady_drift_report(p)
expect_identical(report$drift, steady_biomass_drift(p))
species <- abs(rowSums(getSteadyResidual(p)))
wdw <- w_full(p) * dw_full(p)
rates <- steady_rates(p)
resource <- abs(sum(rates$dn_pp_dt * wdw) / sum(rates$n_pp * wdw))
if (report$is_resource) {
expect_equal(report$drift, resource)
expect_gte(resource, max(species))
} else {
expect_equal(report$drift, species[[report$variable]])
expect_gte(species[[report$variable]], resource)
}
}
})
test_that("steady_drift_report() names a species when a species is worst", {
# Pinning the resource takes it out of the running, so a species must win.
# Tripling one consumer also triples the predation on the resource, which
# is why the resource is otherwise the worst offender on this model.
p <- off_steady_params
resource_dynamics(p) <- "resource_constant"
report <- steady_drift_report(p)
expect_false(report$is_resource)
expect_identical(report$variable,
as.character(species_params(p)$species[[1]]))
expect_identical(steady_variable_txt(report),
paste0("`", report$variable, "`"))
expect_identical(steady_variable_txt(report, quote = FALSE),
report$variable)
})
test_that("steady_drift_report() reports the resource as the resource", {
p <- residual_params
initialNResource(p) <- initialNResource(p) * 3
report <- steady_drift_report(p)
expect_true(report$is_resource)
expect_identical(steady_variable_txt(report), "the resource")
expect_identical(steady_variable_txt(report, quote = FALSE), "the resource")
})
test_that("steady_drift_report() covers only components that can move", {
# A component pinned with `constant_other()` cannot drift, so listing it
# would bury the ones that can. This is the same set that
# `warn_other_components_fixed()` names.
expect_length(steady_drift_report(residual_params)$other, 0)
p <- setComponent(residual_params, "fixed", initial_value = c(1, 2),
dynamics_fun = "constant_other")
expect_length(steady_drift_report(p)$other, 0)
p <- setComponent(p, "still", initial_value = c(1, 2),
dynamics_fun = "settled_component")
report <- steady_drift_report(p)
expect_named(report$other, "still")
expect_equal(report$other[["still"]], 0)
})
test_that("steady_drift_report() measures a component's rate of change", {
report <- steady_drift_report(component_params)
expect_named(report$other, "drifter")
# `exp(2 dt)` per step is a relative rate of 2 per year.
expect_equal(report$other[["drifter"]], 2, tolerance = 1e-3)
})
test_that("an unmeasurable component state is reported as such", {
p <- setComponent(residual_params, "opaque",
initial_value = list(a = "not numeric"),
dynamics_fun = "opaque_component")
report <- steady_drift_report(p)
expect_named(report$other, "opaque")
expect_true(is.na(report$other[["opaque"]]))
# It is not evidence of motion, so it is not reported as motion. The
# component's dynamics here return the state unchanged, and a non-numeric
# state yields `NA` either way, so calling that a drift would call this
# model unsteady for ever.
groups <- component_groups(report)
expect_length(groups$moving, 0)
expect_identical(groups$unmeasured, "opaque")
expect_identical(component_drift_txt(report), "")
expect_match(component_unmeasured_txt(report), "could not be determined")
expect_match(component_unmeasured_txt(report), "`opaque`")
})
test_that("an unmeasurable component does not make the model unsteady", {
p <- setComponent(residual_params, "opaque",
initial_value = list(a = "not numeric"),
dynamics_fun = "opaque_component")
expect_true(isSteady(p))
# The report says what could not be checked, and claims nothing beyond it.
w <- expect_warning(warn_if_not_steady(p, "Context."),
"could not be determined")
expect_no_match(conditionMessage(w), "is not at its steady state")
expect_no_match(conditionMessage(w), "but the model as a whole is not")
})
test_that("a moving component and an unmeasurable one are reported apart", {
p <- setComponent(component_params, "opaque",
initial_value = list(a = "not numeric"),
dynamics_fun = "opaque_component")
report <- steady_drift_report(p)
groups <- component_groups(report)
expect_named(groups$moving, "drifter")
expect_identical(groups$unmeasured, "opaque")
w <- expect_warning(warn_if_not_steady(p, "Context."),
"but the model as a whole is not")
expect_match(conditionMessage(w), "`drifter`")
expect_match(conditionMessage(w), "`opaque`")
})
test_that("the component sentence suits a caller that advances them", {
report <- steady_drift_report(component_params)
expect_match(component_drift_txt(report),
"steady-state machinery does not settle them")
# `project_until_settled()` does advance them, so the standing sentence
# would contradict what the run just did.
advanced <- component_drift_txt(report, advanced = TRUE)
expect_match(advanced, "this run does advance them")
expect_no_match(advanced, "does not settle them")
})
# The exclusion of other components ----
test_that("the biomass drift excludes the other components", {
# The heart of the change made for #589: a component with dynamics of its
# own no longer enters the scalar the tolerances are stated against, so a
# model whose consumers and resource have settled reads as steady even
# while the component moves at 2 per year.
expect_equal(steady_biomass_drift(component_params),
steady_biomass_drift(residual_params))
expect_true(isSteady(component_params))
expect_lt(steady_biomass_drift(component_params), steady_residual_tol())
})
test_that("the excluded component is still measured and reported", {
# Excluding it from the scalar must not mean not measuring it: the rate is
# still there for the user to find, and `steady_rates()` still steps the
# component to get it.
other <- attr(getSteadyResidual(component_params), "other")
expect_named(other, "drifter")
expect_equal(other$drifter, rep(2, 3), tolerance = 1e-3)
expect_equal(max(abs(other$drifter)),
steady_drift_report(component_params)$other[["drifter"]])
})
test_that("a drifting component is named even when the consumers are steady", {
# The loud half of the bargain. Taking the components out of the scalar is
# the workaround that hid a 2-3/year drift in the report behind #589; what
# makes it safe here is that mizer says so itself, and names the component.
expect_warning(warn_if_not_steady(component_params, "Context."),
"drifter")
expect_warning(warn_if_not_steady(component_params, "Context."),
"not included in the biomass drift")
# And it says that the consumers are not the problem, rather than sending
# the user off to look at species that have settled.
expect_warning(warn_if_not_steady(component_params, "Context."),
"consumers and the resource in this model are at their steady state")
})
test_that("a drifting component is named alongside a drifting species", {
p <- component_params
initialN(p)[1, ] <- initialN(p)[1, ] * 3
w <- expect_warning(warn_if_not_steady(p, "Context."),
"not at its steady state")
expect_warning(warn_if_not_steady(p, "Context."), "drifter")
})
test_that("a settled component is not named", {
p <- setComponent(residual_params, "still", initial_value = c(1, 2),
dynamics_fun = "settled_component")
expect_identical(component_drift_txt(steady_drift_report(p)), "")
expect_silent(warn_if_not_steady(p, "Context."))
})
test_that("steady_total_drift() folds the components back in", {
# The one caller that needs them: `project_until_settled()`, where the
# components are live rather than pinned.
report <- steady_drift_report(component_params)
expect_equal(steady_total_drift(report), 2, tolerance = 1e-3)
expect_gt(steady_total_drift(report), report$drift)
# With nothing to fold in it is just the drift.
expect_identical(steady_total_drift(steady_drift_report(residual_params)),
steady_biomass_drift(residual_params))
})
# warn_if_not_steady ----
test_that("warn_if_not_steady() fires only when the model is off steady state", {
expect_warning(warn_if_not_steady(off_steady_params, "Context."),
"not at its steady state")
expect_silent(warn_if_not_steady(residual_params, "Context."))
})
test_that("warn_if_not_steady() includes the context and is silenced by info_level", {
expect_warning(warn_if_not_steady(off_steady_params, "Sentinel phrase."),
"Sentinel phrase")
withr::local_options(mizer_info_level = 0)
expect_silent(warn_if_not_steady(off_steady_params, "Context."))
})
# isSteady ----
test_that("isSteady() returns a single boolean", {
expect_true(isSteady(residual_params))
expect_false(isSteady(off_steady_params))
expect_type(isSteady(residual_params), "logical")
expect_length(isSteady(residual_params), 1L)
})
test_that("isSteady() respects custom tol argument", {
expect_true(isSteady(off_steady_params, tol = 10))
expect_false(isSteady(residual_params, tol = 1e-10))
})
test_that("isSteady() respects effort argument", {
p <- NS_params_small
higher <- initial_effort(p) + 1
# Different effort changes steadiness if model was settled at initial_effort
expect_equal(isSteady(p, effort = initial_effort(p)),
isSteady(p))
})
test_that("isSteady() works under the second-order scheme", {
p <- residual_params
second_order_w(p) <- TRUE
# Changing quadrature scheme moves model off its original steady state
expect_false(isSteady(p))
# Re-settling under second-order scheme restores steady state
p <- suppressMessages(steady(p, tol = 1e-5, t_max = 500,
progress_bar = FALSE, info_level = 0))
expect_true(isSteady(p))
})
test_that("isSteady() dispatches via S3", {
dummy <- structure(list(), class = "DummyModel")
isSteady.DummyModel <- function(params, ...) TRUE # nolint: object_name_linter.
expect_true(isSteady(dummy))
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.