/.")
-runs <- run_assessments(
- tier = translation_tier,
- database = database,
- parallel = parallel,
- workers = workers,
- save_results = TRUE,
- cache = TRUE,
- dashboard = FALSE, # Render previews below, after saving the complete batch.
- timeout = timeout
-)
-cli::cli_inform("Saving combined assessment results.")
-.assessment_save_rds(runs, file.path(cache_root, "assessment_runs.rds"))
-
-# Exploratory dashboards do not change aggregate comparison eligibility.
-preview_errors <- list()
-if (dashboard) for (id in names(runs)) {
- result <- runs[[id]]
- if (!inherits(result$fit, "tam_fit")) next
- output <- file.path(.assessment_cache(id, translation_tier, database), "dashboard.html")
- cli::cli_inform("Rendering dashboard: {id}")
- error <- tryCatch({
- view_assessment(result$fit, result, output_file = output, open_file = FALSE)
- runs[[id]]$dashboard_file <- output
- NULL
- }, error = conditionMessage)
- if (!is.null(error)) preview_errors[[id]] <- error
-}
-if (dashboard) .assessment_save_rds(runs, file.path(cache_root, "assessment_runs.rds"))
-.assessment_save_rds(preview_errors, file.path(cache_root, "dashboard_errors.rds"))
-
-index_assessment_dashboards(translation_tier, database)
-
-cli::cli_inform("Building overview figures.")
-source(file.path(root, "scripts", "translation", "stats_plots.R"))
-source(file.path(root, "scripts", "translation", "trend_plots.R"))
-cli::cli_inform("Assessment batch complete. Inspect results/fit_diagnostics.csv for failures and timeouts.")
diff --git a/analysis/comp_assessments/scripts/translation/trend_plots.R b/analysis/comp_assessments/scripts/trend_plots.R
similarity index 97%
rename from analysis/comp_assessments/scripts/translation/trend_plots.R
rename to analysis/comp_assessments/scripts/trend_plots.R
index e6f23b7f..704a861f 100644
--- a/analysis/comp_assessments/scripts/translation/trend_plots.R
+++ b/analysis/comp_assessments/scripts/trend_plots.R
@@ -13,7 +13,7 @@ source(file.path(root, "R", "read_database.R"))
source(file.path(root, "R", "translation_registry.R"))
if (!exists("translation_tier", inherits = FALSE)) translation_tier <- "curated"
results_dir <- .assessment_results(translation_tier)
-plot_database <- .saio_metadata(tier = translation_tier)
+plot_database <- if (exists("database", inherits = FALSE)) database else read_database()
.assessment_check_results_revision(results_dir, plot_database)
translation_ids <- assessment_translations(translation_tier)$assessment_id
source(file.path(root, "R", "plot_stock_order.R"))
@@ -25,9 +25,9 @@ ts <- read_csv(
)
batch_runs <- .assessment_cached_runs(translation_tier, plot_database)
-assessments <- .saio_read_table("assessments.csv", revision = plot_database$commit)
+assessments <- plot_database$assessments
-stocks <- .saio_read_table("stocks.csv", revision = plot_database$commit)
+stocks <- plot_database$stocks
ts <- ts |> filter(assessment_id %in% translation_ids)
diff --git a/analysis/comp_assessments/scripts/update_database_cache.R b/analysis/comp_assessments/scripts/update_database_cache.R
new file mode 100644
index 00000000..956f955a
--- /dev/null
+++ b/analysis/comp_assessments/scripts/update_database_cache.R
@@ -0,0 +1,5 @@
+# Run manually from the tinyAM repository root after updating SAIO.
+# This copies committed CSV files only; it never fetches or changes SAIO.
+saio_repo <- "../saio" # Edit this path if the two repositories are not siblings.
+source("analysis/comp_assessments/R/update_database_cache.R")
+update_database_cache(saio_repo)
diff --git a/analysis/comp_assessments/tests/test_3pn4rs_source_inputs.R b/analysis/comp_assessments/tests/test_3pn4rs_source_inputs.R
index dcdd690d..bf0356db 100644
--- a/analysis/comp_assessments/tests/test_3pn4rs_source_inputs.R
+++ b/analysis/comp_assessments/tests/test_3pn4rs_source_inputs.R
@@ -1,8 +1,6 @@
source("analysis/comp_assessments/R/read_database.R")
root <- file.path("analysis", "comp_assessments")
-inputs <- read.csv(file.path(saio_path(), "database", "inputs.csv"),
- stringsAsFactors = FALSE
-)
+inputs <- read_database()$inputs
inputs <- inputs[inputs$assessment_id == "dfo_cod_3pn4rs_2025", ]
index <- inputs[inputs$type == "index", ]
weights <- inputs[inputs$type == "catch_weight", ]
@@ -58,7 +56,7 @@ stopifnot(
)
source(file.path(root, "R", "run_assessment.R"))
-source_data <- read_assessment("dfo_cod_3pn4rs_2025", read_database(tier = "preliminary"))
+source_data <- read_assessment("dfo_cod_3pn4rs_2025", read_database())
before <- source_data
obs <- database_to_tam_obs(
"dfo_cod_3pn4rs_2025", source_data$inputs,
@@ -73,7 +71,7 @@ stopifnot(
nrow(obs$weight) == 30L * 10L,
nrow(obs$maturity) == 30L * 10L,
all(is.finite(obs$pop_covariates$M_assumption)),
- identical(tinyAM::check_obs(obs), TRUE),
+ identical(tinyAM::check_tam_data(obs), TRUE),
identical(source_data, before),
source_data$assessment$inputs_status == "partial"
)
diff --git a/analysis/comp_assessments/tests/test_age_subsets.R b/analysis/comp_assessments/tests/test_age_subsets.R
index 4a0a17b4..8e310759 100644
--- a/analysis/comp_assessments/tests/test_age_subsets.R
+++ b/analysis/comp_assessments/tests/test_age_subsets.R
@@ -53,7 +53,7 @@ testthat::test_that("observations select biology and retain older catch in the n
years = full$years, ages = 0:4,
surveys = c("HERAS", "GERAS", "N20", "IBTS/BITSQ1"), assumptions = source_data$assumptions
)
- testthat::expect_true(tinyAM::check_obs(obs))
+ testthat::expect_true(tinyAM::check_tam_data(obs))
for (component in names(obs)) testthat::expect_true(all(obs[[component]]$age %in% 0:4))
testthat::expect_equal(nrow(obs$weight), 35L * 5L)
old_plus <- subset(full$obs$catch, age >= 4)
diff --git a/analysis/comp_assessments/tests/test_asmfc_striped_bass_2018_source.R b/analysis/comp_assessments/tests/test_asmfc_striped_bass_2018_source.R
index 67769a16..b6ab5957 100644
--- a/analysis/comp_assessments/tests/test_asmfc_striped_bass_2018_source.R
+++ b/analysis/comp_assessments/tests/test_asmfc_striped_bass_2018_source.R
@@ -1,7 +1,7 @@
.libPaths(c("C:/Users/paulr/AppData/Local/R/win-library/4.4", .libPaths()))
pkgload::load_all(".", quiet = TRUE)
source("analysis/comp_assessments/R/run_assessment.R")
-db <- read_database(tier = "preliminary")
+db <- read_database()
record <- read_assessment("asmfc_striped_bass_atlantic_2018", db)
years <- 1982:2017
inputs <- record$inputs
@@ -82,6 +82,6 @@ stopifnot(
nrow(total_biomass) == length(years),
!anyDuplicated(total_biomass$year),
setequal(total_biomass$year, years),
- tinyAM::check_obs(translated$obs)
+ tinyAM::check_tam_data(translated$obs)
)
cat("2018 striped-bass source extraction and tinyAM observation preparation passed.\n")
diff --git a/analysis/comp_assessments/tests/test_basin_terminology.R b/analysis/comp_assessments/tests/test_basin_terminology.R
index c3b84fa3..2e550802 100644
--- a/analysis/comp_assessments/tests/test_basin_terminology.R
+++ b/analysis/comp_assessments/tests/test_basin_terminology.R
@@ -16,8 +16,8 @@ source("analysis/comp_assessments/R/plot_stock_order.R")
plot_stocks <- assessment_plot_stocks(db$assessments, db$stocks,
db$assessments$assessment_id[db$assessments$is_current])
stopifnot(identical(as.character(plot_stocks$basin), plot_stocks$ocean))
-plot_code <- readLines("analysis/comp_assessments/scripts/translation/trend_plots.R")
-heatmap_code <- readLines("analysis/comp_assessments/scripts/translation/stats_plots.R")
+plot_code <- readLines("analysis/comp_assessments/scripts/trend_plots.R")
+heatmap_code <- readLines("analysis/comp_assessments/scripts/stats_plots.R")
stopifnot(any(grepl("assessment_plot_stocks(", plot_code, fixed = TRUE)),
any(grepl("assessment_plot_stocks(", heatmap_code, fixed = TRUE)))
stopifnot(!any(grepl('"North Pacific"', heatmap_code, fixed = TRUE)))
diff --git a/analysis/comp_assessments/tests/test_batch_controls.R b/analysis/comp_assessments/tests/test_batch_controls.R
index c62e643c..c845d8a3 100644
--- a/analysis/comp_assessments/tests/test_batch_controls.R
+++ b/analysis/comp_assessments/tests/test_batch_controls.R
@@ -1,81 +1,55 @@
pkgload::load_all(".", quiet = TRUE)
source("analysis/comp_assessments/R/run_assessment.R")
-# Exercise the actual top-level orchestration without fits, exports or rendering.
-script <- parse("analysis/comp_assessments/scripts/translation/run_all.R")
-for (dashboards in c(FALSE, TRUE)) {
- e <- new.env(parent = globalenv())
- e$source <- function(...) invisible(NULL)
- e$requireNamespace <- function(...) TRUE
- e$read_committed_database <- function(...) list(commit = paste(rep("a", 40), collapse = ""))
- e$run_assessments <- function(...) {
- e$batch_args <- list(...)
- list(example = list(fit = structure(list(), class = "tam_fit")))
- }
- e$.assessment_save_rds <- function(...) invisible(NULL)
- e$index_assessment_dashboards <- function(...) invisible(NULL)
- e$previews <- 0L
- e$view_assessment <- function(...) e$previews <- e$previews + 1L
- for (expr in script) {
- if (is.call(expr) && identical(expr[[1L]], quote(`<-`)) &&
- identical(expr[[2L]], quote(dashboard))) expr[[3L]] <- dashboards
- eval(expr, e)
- }
- stopifnot(e$previews == as.integer(dashboards), !e$batch_args$dashboard,
- identical(e$batch_args$timeout, 1200), isTRUE(e$batch_args$cache))
-}
-
-missing_packages <- new.env(parent = globalenv())
-missing_packages$requireNamespace <- function(...) FALSE
-missing_packages$run_assessments <- function(...) stop("Fitting must not begin.")
-missing_error <- tryCatch({
- for (expr in script) eval(expr, missing_packages)
- NULL
-}, error = identity)
-stopifnot(inherits(missing_error, "error"),
- grepl("Install the batch's analysis packages before fitting", conditionMessage(missing_error), fixed = TRUE))
+# The script exposes one caller-owned future plan and a separate render stage.
+script <- paste(readLines("analysis/comp_assessments/scripts/run_all.R"), collapse = "\n")
+stopifnot(grepl("future::plan(future::multisession, workers = 10)", script, fixed = TRUE),
+ grepl("timeout <- 20 * 60", script, fixed = TRUE),
+ grepl("render_assessment_dashboards(runs)", script, fixed = TRUE),
+ !any(c("parallel", "workers", "dashboard") %in% names(formals(run_assessments))),
+ !"dashboard" %in% names(formals(run_assessment)),
+ identical(formals(run_assessments)$timeout, quote(Inf)))
database <- read_database()
-id <- "ices_cod_north_sea_2025"
+ids <- c("ices_cod_north_sea_2025", "ices_herring_north_sea_2026")
+old_plan <- future::plan()
+future::plan(future::sequential)
progress_text <- character()
-withCallingHandlers(run_assessments(id, database, fit = FALSE, progress = TRUE),
+withCallingHandlers(run_assessments(ids[1], database, fit = FALSE, progress = TRUE),
message = function(m) progress_text <<- c(progress_text, conditionMessage(m)))
-stopifnot(any(grepl(paste(id, "started"), progress_text, fixed = TRUE)),
- any(grepl(paste(id, "not_fitted"), progress_text, fixed = TRUE)))
+stopifnot(any(grepl(paste(ids[1], "started"), progress_text, fixed = TRUE)),
+ any(grepl(paste(ids[1], "not_fitted"), progress_text, fixed = TRUE)))
for (bad in list(0, -1, NA_real_, c(1, 2), "10")) {
- stopifnot(inherits(tryCatch(run_assessments(id, database, fit = FALSE,
+ stopifnot(inherits(tryCatch(run_assessments(ids[1], database, fit = FALSE,
timeout = bad, progress = FALSE), error = identity), "error"))
}
-stopifnot(inherits(tryCatch(run_assessments(id, database, fit = FALSE,
+stopifnot(inherits(tryCatch(run_assessments(ids[1], database, fit = FALSE,
progress = NA), error = identity), "error"))
empty_database <- database
empty_database$assessments <- empty_database$assessments[FALSE, , drop = FALSE]
stopifnot(length(run_assessments(database = empty_database, progress = FALSE)) == 0L)
-# A hard deadline must become a diagnostic and leave no live child blocking the
-# next job. A tiny limit expires during R startup, without running a model.
-timed <- run_assessments(id, database, fit = FALSE, timeout = .01, progress = FALSE)
-stopifnot(timed[[id]]$diagnostics$status == "timed_out",
- is.null(timed[[id]]$fit), grepl("settings were not changed", timed[[id]]$diagnostics$reason),
- is.finite(timed[[id]]$diagnostics$elapsed_seconds))
-
-ids <- c(id, "ices_herring_north_sea_2026")
-for (parallel in c(FALSE, TRUE)) {
- ran <- run_assessments(ids, database, fit = FALSE, parallel = parallel,
- workers = 2L, timeout = 60, progress = FALSE)
- stopifnot(identical(names(ran), ids),
- all(vapply(ran, function(x) x$diagnostics$status == "not_fitted", logical(1))))
+# Both plans retain all attempts and the same prepared observations.
+sequential <- run_assessments(ids, database, fit = FALSE, progress = FALSE)
+future::plan(future::multisession, workers = 2)
+parallel <- run_assessments(ids, database, fit = FALSE, progress = FALSE)
+if (!isTRUE(all.equal(lapply(sequential, `[[`, "obs"), lapply(parallel, `[[`, "obs"), tolerance = 0))) {
+ print(lapply(parallel, function(x) x$diagnostics[, c("status", "reason")]))
}
-
-limited <- run_assessments(ids, database, fit = FALSE, parallel = TRUE,
- workers = 2L, timeout = .01, progress = FALSE)
+stopifnot(inherits(future::plan(), "multisession"),
+ identical(names(sequential), names(parallel)),
+ isTRUE(all.equal(lapply(sequential, `[[`, "obs"), lapply(parallel, `[[`, "obs"), tolerance = 0)),
+ all(vapply(parallel, function(x) x$diagnostics$status == "not_fitted", logical(1))))
+# A tiny deadline expires in startup without running or retuning a model.
+limited <- run_assessments(ids, database, fit = FALSE, timeout = .01, progress = FALSE)
stopifnot(identical(names(limited), ids),
- all(vapply(limited, function(x) x$diagnostics$status == "timed_out", logical(1))))
+ all(vapply(limited, function(x) x$diagnostics$status == "timed_out" &&
+ grepl("settings were not changed", x$diagnostics$reason), logical(1))))
+future::plan(future::sequential)
-# Passing a real fitted RTMB object back from a supervised process must preserve
-# the estimates, reporting tables, diagnostics and standalone tidying workflow.
-direct <- run_assessment(id, database)
-supervised <- run_assessments(id, database, timeout = 60, progress = FALSE)[[id]]
+# A fitted RTMB result must survive the supervised-process return path.
+direct <- run_assessment(ids[1], database)
+supervised <- run_assessments(ids[1], database, timeout = 120, progress = FALSE)[[ids[1]]]
stopifnot(inherits(supervised$fit, "tam_fit"),
isTRUE(all.equal(direct$fit$opt$objective, supervised$fit$opt$objective)),
isTRUE(all.equal(direct$fit$parameter_values, supervised$fit$parameter_values)),
@@ -84,7 +58,7 @@ stopifnot(inherits(supervised$fit, "tam_fit"),
tidied <- tinyAM::tidy_tam(model_list = list(supervised = supervised$fit))
stopifnot(tidied$process_residuals$status$status == "ok")
-# Cache each stock before the next serial job is allowed to start.
+# Every completed attempt is checkpointed before the next sequential job starts.
original_runner <- run_assessment
original_root <- .assessment_root
test_root <- tempfile("assessment_checkpoint_")
@@ -100,14 +74,12 @@ run_assessment <- function(assessment_id, ...) {
}
tryCatch({
cached <- run_assessments(ids, database, fit = FALSE, cache = TRUE, progress = FALSE)
- stopifnot(all(file.exists(file.path(.assessment_cache(ids, "curated", database), "result.rds"))),
- all(vapply(cached, function(x) x$diagnostics$status == "not_fitted", logical(1))))
+ stopifnot(all(file.exists(file.path(.assessment_cache(ids, "curated", database), "result.rds"))))
}, finally = {
run_assessment <- original_runner
.assessment_root <- original_root
unlink(test_root, recursive = TRUE)
})
-cat("Batch controls, subprocess deadlines and immediate checkpoints passed.\n")
checkpoint <- tempfile(fileext = ".rds")
.assessment_save_rds(list(version = 1), checkpoint)
@@ -116,4 +88,40 @@ stopifnot(inherits(interrupted, "error"), identical(readRDS(checkpoint), list(ve
.assessment_save_rds(list(version = 2), checkpoint)
stopifnot(identical(readRDS(checkpoint), list(version = 2)))
unlink(checkpoint)
-cat("Interrupted checkpoint preservation and replacement passed.\n")
+
+# Rendering never refits, rereads the source, changes eligibility or loses failures.
+local({
+ old_root <- .assessment_root; old_view <- view_assessment
+ old_reader <- read_database; old_runner <- run_assessment
+ output <- tempfile("assessment_render_"); dir.create(output)
+ .assessment_root <<- output
+ on.exit({.assessment_root <<- old_root; view_assessment <<- old_view
+ read_database <<- old_reader; run_assessment <<- old_runner
+ unlink(output, recursive = TRUE)}, add = TRUE)
+ read_database <<- function(...) stop("Rendering must not reread source data")
+ run_assessment <<- function(...) stop("Rendering must not refit")
+ view_assessment <<- function(fit, assessment, output_file, open_file) {
+ if (assessment$assessment_id == "failed") stop("Example render error")
+ writeLines("Rendered", output_file)
+ }
+ result <- function(id, convergence) list(assessment_id = id,
+ translation_tier = "curated", source = database,
+ fit = structure(list(is_converged = convergence), class = "tam_fit"),
+ diagnostics = data.frame(database_key = database$database_key),
+ summary = data.frame(marker = "unchanged"))
+ failed_source <- database; failed_source$translation_tier <- "curated"
+ empty <- .failed_assessment_run("absent", failed_source, simpleError("No fitted model"))
+ runs <- list(absent = empty, good = result("good", TRUE),
+ uncertain = result("uncertain", FALSE), failed = result("failed", TRUE))
+ original <- serialize(runs, NULL)
+ rendered <- render_assessment_dashboards(runs)
+ stopifnot(identical(rendered$status, c("skipped_no_fit", "rendered", "troubleshooting", "render_failed")),
+ rendered$error[1] == "No fitted model", rendered$error[4] == "Example render error", identical(serialize(runs, NULL), original),
+ file.exists(file.path(output, "results", "curated", "index.html")),
+ file.exists(file.path(.assessment_cache_root("curated", database), "dashboard_manifest.csv")))
+ all_failed <- render_assessment_dashboards(list(absent = empty))
+ stopifnot(identical(all_failed$status, "skipped_no_fit"),
+ file.exists(file.path(.assessment_cache_root("curated", failed_source), "dashboard_manifest.csv")))
+})
+future::plan(old_plan)
+cat("Caller-owned future plans, deadlines, checkpoints and separate dashboard status checks passed.\n")
diff --git a/analysis/comp_assessments/tests/test_bc_canary_translation.R b/analysis/comp_assessments/tests/test_bc_canary_translation.R
index c0130ce6..a7734fad 100644
--- a/analysis/comp_assessments/tests/test_bc_canary_translation.R
+++ b/analysis/comp_assessments/tests/test_bc_canary_translation.R
@@ -23,7 +23,7 @@ stopifnot(identical(record, original), is.null(x$fit),
identical(x$comparison_aggregates, "ssb"),
sum(x$dat$is_proj) == 1L, max(x$dat$years) == 2023L,
is.finite(tinyAM::nll_fun(tinyAM::make_par(x$dat), x$dat)))
-tinyAM::check_obs(x$obs)
+tinyAM::check_tam_data(x$obs)
# The biomass-to-number denominator includes both sexes before selection.
fixture <- record$inputs[rep(1L, 10L), ]
diff --git a/analysis/comp_assessments/tests/test_bc_petrale_readiness.R b/analysis/comp_assessments/tests/test_bc_petrale_readiness.R
index c94456be..27b78a38 100644
--- a/analysis/comp_assessments/tests/test_bc_petrale_readiness.R
+++ b/analysis/comp_assessments/tests/test_bc_petrale_readiness.R
@@ -10,8 +10,7 @@ for (fit in c(FALSE, TRUE)) {
stock$env$fit_stage == "translation", is.null(stock$env$obs), is.null(stock$env$fit),
is.null(stock$env$fit_model), is.null(stock$env$retry_start),
is.null(stock$env$start_par), identical(record, original))
- result <- run_assessment("dfo_petrale_bc_2024", database, fit = fit,
- dashboard = TRUE, cache = FALSE)
+ result <- run_assessment("dfo_petrale_bc_2024", database, fit = fit, cache = FALSE)
stopifnot(result$diagnostics$status == "translation_failed", is.null(result$fit),
is.null(result$obs), is.null(result$ref), is.null(result$summary),
is.null(result$differences), is.null(result$dashboard_file),
@@ -20,8 +19,7 @@ for (fit in c(FALSE, TRUE)) {
nrow(result$audit) == nrow(record$assumptions),
!"dfo_petrale_bc_2024" %in% database$assessments$assessment_id[database$assessments$is_current])
}
-summary <- read.csv("analysis/comp_assessments/results/comparison_summary.csv")
-series <- read.csv("analysis/comp_assessments/results/population_timeseries.csv")
-stopifnot(!"dfo_petrale_bc_2024" %in% summary$assessment_id,
- !"dfo_petrale_bc_2024" %in% series$assessment_id)
+tables <- .assessment_result_tables(list(result), save_results = FALSE)
+stopifnot(nrow(attr(tables, "comparison_summary")) == 0L,
+ nrow(attr(tables, "population_timeseries")) == 0L)
cat("BC petrale remains source-blocked in preparation and fitting; no invented model or comparisons are produced.\n")
diff --git a/analysis/comp_assessments/tests/test_bc_pop_translation.R b/analysis/comp_assessments/tests/test_bc_pop_translation.R
index 3bd42aa3..14b4ea20 100644
--- a/analysis/comp_assessments/tests/test_bc_pop_translation.R
+++ b/analysis/comp_assessments/tests/test_bc_pop_translation.R
@@ -8,13 +8,12 @@ for (fit in c(FALSE, TRUE)) {
gsub("[[:space:]]+", " ", conditionMessage(x$error)), fixed = TRUE),
x$env$fit_stage == "translation", is.null(x$env$fit), is.null(x$env$obs),
is.null(x$env$fit_model), is.null(x$env$retry_start), identical(record, original))
- result <- run_assessment("dfo_pop_bc_2023", fit = fit, dashboard = TRUE, cache = FALSE)
+ result <- run_assessment("dfo_pop_bc_2023", fit = fit, cache = FALSE)
stopifnot(result$diagnostics$status == "translation_failed",
is.null(result$fit), is.null(result$ref), is.null(result$summary),
is.null(result$dashboard_file), identical(record, original))
}
-summary <- read.csv("analysis/comp_assessments/results/comparison_summary.csv")
-series <- read.csv("analysis/comp_assessments/results/population_timeseries.csv")
-stopifnot(!"dfo_pop_bc_2023" %in% summary$assessment_id,
- !"dfo_pop_bc_2023" %in% series$assessment_id)
+tables <- .assessment_result_tables(list(result), save_results = FALSE)
+stopifnot(nrow(attr(tables, "comparison_summary")) == 0L,
+ nrow(attr(tables, "population_timeseries")) == 0L)
cat("BC POP is blocked before fitting/comparison; source records remain intact and no stale comparison rows remain.\n")
diff --git a/analysis/comp_assessments/tests/test_coastal_hake_translation.R b/analysis/comp_assessments/tests/test_coastal_hake_translation.R
index a464c9be..6a97a4cf 100644
--- a/analysis/comp_assessments/tests/test_coastal_hake_translation.R
+++ b/analysis/comp_assessments/tests/test_coastal_hake_translation.R
@@ -6,8 +6,10 @@ s <- .test_stock(x)
stopifnot(identical(x,original),is.null(s$fit),identical(s$years,1975:2024),
identical(s$ages,2:15),!"start_par" %in% names(s$fit_call),
all(s$obs$maturity$spawn_time==0),all(s$obs$index$samp_time==.5),
- s$settings$rec_settings$sd==1.4,identical(s$dat$rec$curve$type,"bh"),
- identical(s$dat$rec$curve$lag,2L),length(s$dat$M_terms)==1L,
+ s$settings$rec_settings$sd==1.4,
+ identical(s$settings$rec_settings$process,"iid"),
+ identical(deparse(s$settings$rec_settings$mu_form),"~1"),
+ is.null(s$dat$rec$curve),length(s$dat$M_terms)==1L,
abs(unique(s$dat$log_mu_supplied_m)+1.60944)<1e-12,
identical(levels(s$obs$index$q_block),as.character(2:6)),
all(s$obs$index$q_block[s$obs$index$age>=6]=="6"),
@@ -29,21 +31,8 @@ ref <- database_to_tam_ref(x$assessment$assessment_id,s$comparison_outputs,
comparison_definitions=s$comparison_definitions)
stopifnot(nrow(ref$pop$recruitment)==50,all(ref$pop$recruitment$unit=="million fish"),
is.null(ref$pop$F) || all(is.na(ref$pop$F$est)))
-defaults <- tinyAM::make_par(s$dat)
-partial <- s$retry_start
-e <- new.env(parent=environment(partial))
-e$source <- x
-zero <- e$source$outputs$measure=="numbers_at_age" &
- e$source$outputs$year==1977 & e$source$outputs$age==2
-stopifnot(sum(zero)==1)
-e$source$outputs$value[zero] <- 0
-environment(partial) <- e
-start <- partial()
-merged <- tinyAM:::.merge_start_par(defaults,start)
-missing <- setdiff(names(defaults$log_r),names(start$log_r))
-matched <- intersect(names(start$log_r),names(merged$log_r))
-stopifnot(length(missing)>0,all(is.finite(unlist(start))),
- identical(merged$log_r[missing],defaults$log_r[missing]),
- identical(merged$log_f,defaults$log_f),identical(merged$log_q,defaults$log_q),
- all(merged$log_r[matched]==start$log_r[matched]))
+# The settled recipe uses one direct call, constant-mean age-2 recruitment,
+# and bounded effective q; it no longer has a source-state retry.
+stopifnot(is.null(s$retry_start), is.null(s$fit_model),
+ identical(s$settings$index_settings$q_link, "logit"))
cat("Coastal hake native-percent conversion, fecundity/sex scaling, q sharing, plus biology and fit-disabled preparation passed.\n")
diff --git a/analysis/comp_assessments/tests/test_dfo_cod_2j3kl_translation.R b/analysis/comp_assessments/tests/test_dfo_cod_2j3kl_translation.R
index 728e6520..e920f8fb 100644
--- a/analysis/comp_assessments/tests/test_dfo_cod_2j3kl_translation.R
+++ b/analysis/comp_assessments/tests/test_dfo_cod_2j3kl_translation.R
@@ -19,10 +19,7 @@ all_maturity <- source_data$inputs[
drop = FALSE
]
-tinyAM::check_obs(obs)
-readiness <- read.csv(file.path(.assessment_results("curated"), "observation_readiness.csv"),
- stringsAsFactors = FALSE
-)
+tinyAM::check_tam_data(obs)
dat <- do.call(tinyAM::prepare_tam, c(
list(data = obs, years = translated$years, ages = translated$ages),
translated$settings
@@ -73,9 +70,6 @@ stopifnot(
),
identical(source_data$inputs$year, source_year),
identical(source_data$inputs$year_basis, source_basis),
- readiness$maturity_rows[readiness$assessment_id == "dfo_cod_2j3kl_2025"] == 1065L,
- readiness$maturity_full_year_age_grid[readiness$assessment_id == "dfo_cod_2j3kl_2025"],
- !any(grepl("maturity_cohort", names(readiness))),
audit$tinyam_support[audit$setting == "maturity_at_age"] == "supported",
dat$N_settings$process == "off",
dat$N_settings$init == "exp",
diff --git a/analysis/comp_assessments/tests/test_dfo_cod_3ps_translation.R b/analysis/comp_assessments/tests/test_dfo_cod_3ps_translation.R
index 0ac3a721..de4b5097 100644
--- a/analysis/comp_assessments/tests/test_dfo_cod_3ps_translation.R
+++ b/analysis/comp_assessments/tests/test_dfo_cod_3ps_translation.R
@@ -1,17 +1,10 @@
-source("analysis/comp_assessments/R/read_database.R")
+source("analysis/comp_assessments/R/run_assessment.R")
+database <- read_database()
root <- file.path("analysis", "comp_assessments")
-source_data <- read.csv(file.path(saio_path(), "database", "inputs.csv"),
- na.strings = c("", "NA"), stringsAsFactors = FALSE
-)
-outputs <- read.csv(file.path(saio_path(), "database", "outputs.csv"),
- na.strings = c("", "NA"), stringsAsFactors = FALSE
-)
-assessments <- read.csv(file.path(saio_path(), "database", "assessments.csv"),
- na.strings = c("", "NA"), stringsAsFactors = FALSE
-)
-assumptions <- read.csv(file.path(saio_path(), "database", "assumptions.csv"),
- na.strings = c("", "NA"), stringsAsFactors = FALSE
-)
+source_data <- database$inputs
+outputs <- database$outputs
+assessments <- database$assessments
+assumptions <- database$assumptions
id <- "dfo_cod_3ps_2024"
script_path <- .assessment_script(id)
script_lines <- readLines(script_path, warn = FALSE)
diff --git a/analysis/comp_assessments/tests/test_dfo_cod_4t4vn_translation.R b/analysis/comp_assessments/tests/test_dfo_cod_4t4vn_translation.R
index 9d29c1f9..815410bd 100644
--- a/analysis/comp_assessments/tests/test_dfo_cod_4t4vn_translation.R
+++ b/analysis/comp_assessments/tests/test_dfo_cod_4t4vn_translation.R
@@ -22,7 +22,7 @@ obs <- translated$obs
stopifnot(identical(translated$years, 1971:2018))
stopifnot(identical(translated$ages, 3:12))
-stopifnot(check_obs(obs))
+stopifnot(check_tam_data(obs))
expected_key <- paste(landings_at_age$year, landings_at_age$age, sep = ":")
observed_catch <- obs$catch[!is.na(obs$catch$obs), , drop = FALSE]
catch_key <- paste(observed_catch$year, observed_catch$age, sep = ":")
@@ -83,21 +83,4 @@ stopifnot(all(known_m$year == 2018L), setequal(known_m$age, 5:12))
stopifnot(all(known_m$est[known_m$age %in% 5:8] == 0.81))
stopifnot(all(known_m$est[known_m$age %in% 9:12] == 0.85))
-readiness_env <- new.env(parent = globalenv())
-sys.source(
- file.path(saio_path(), "scripts", "database", "003_observation_readiness.R"),
- envir = readiness_env
-)
-readiness <- utils::read.csv(
- file.path(.assessment_results("curated"), "observation_readiness.csv"),
- stringsAsFactors = FALSE
-)
-southern_gulf <- readiness[
- readiness$assessment_id == "dfo_cod_4t4vn_2019", ,
- drop = FALSE
-]
-stopifnot(nrow(southern_gulf) == 1L)
-stopifnot(southern_gulf$catch_at_age_rows == 0L)
-stopifnot(southern_gulf$landings_at_age_rows == 480L)
-stopifnot(isTRUE(southern_gulf$database_to_tam_obs_succeeds))
-stopifnot(isTRUE(southern_gulf$tinyAM_check_obs_passes))
+# The local observation-readiness stage uses the same prepared-input checks.
diff --git a/analysis/comp_assessments/tests/test_dfo_eastern_georges_bank_cod_source.R b/analysis/comp_assessments/tests/test_dfo_eastern_georges_bank_cod_source.R
index 6f95622c..6aa8123e 100644
--- a/analysis/comp_assessments/tests/test_dfo_eastern_georges_bank_cod_source.R
+++ b/analysis/comp_assessments/tests/test_dfo_eastern_georges_bank_cod_source.R
@@ -103,8 +103,7 @@ stopifnot(
sum(all_outputs$measure == "fishing_mortality_intensity") == 2L,
all(is.na(all_outputs$se)), all(is.na(all_outputs$lwr)), all(is.na(all_outputs$upr)),
all(nzchar(all_outputs$source_reference)),
- !any(grepl("sensitivity|Run 5|Run 6|Run 9", q$source_reference)),
- file.exists(file.path(saio_path(), "source_reviews", "ck_nefsc_trac_cert_morhua_gb.md"))
+ !any(grepl("sensitivity|Run 5|Run 6|Run 9", q$source_reference))
)
before <- framework
error <- tryCatch(database_to_tam_obs(ids[1], framework$inputs,
diff --git a/analysis/comp_assessments/tests/test_dfo_herring_fall_translation.R b/analysis/comp_assessments/tests/test_dfo_herring_fall_translation.R
index f933dad3..5dfa528f 100644
--- a/analysis/comp_assessments/tests/test_dfo_herring_fall_translation.R
+++ b/analysis/comp_assessments/tests/test_dfo_herring_fall_translation.R
@@ -2,7 +2,7 @@ source("analysis/comp_assessments/tests/helper_stock.R")
pkgload::load_all(".", quiet = TRUE)
source("analysis/comp_assessments/R/run_assessment.R")
-database <- read_database(tier = "preliminary")
+database <- read_database()
source_record <- read_assessment("dfo_herring_4tvn_fall_2024", database)
source_original <- source_record
translated <- .test_stock(source_record)
@@ -29,7 +29,7 @@ cpue_index <- obs$index$survey %in% c(
stopifnot(
identical(translated$years, 1978:2023),
identical(translated$ages, 2:11),
- check_obs(obs),
+ check_tam_data(obs),
nrow(obs$catch) == length(1978:2023) * length(2:11),
all(is.finite(obs$catch$obs)),
!anyNA(expected_catch_index),
diff --git a/analysis/comp_assessments/tests/test_dfo_herring_sgsl_translation.R b/analysis/comp_assessments/tests/test_dfo_herring_sgsl_translation.R
index bed1c151..ce409b14 100644
--- a/analysis/comp_assessments/tests/test_dfo_herring_sgsl_translation.R
+++ b/analysis/comp_assessments/tests/test_dfo_herring_sgsl_translation.R
@@ -14,7 +14,7 @@ obs <- translated$obs
stopifnot(
identical(translated$years, 1978:2023),
identical(translated$ages, 2:11),
- tinyAM::check_obs(obs),
+ tinyAM::check_tam_data(obs),
nrow(obs$catch) == 460L,
nrow(obs$index) == 526L,
nrow(obs$weight) == 460L,
diff --git a/analysis/comp_assessments/tests/test_dfo_northern_mackerel_translation.R b/analysis/comp_assessments/tests/test_dfo_northern_mackerel_translation.R
index 22313f9e..af0cb6d7 100644
--- a/analysis/comp_assessments/tests/test_dfo_northern_mackerel_translation.R
+++ b/analysis/comp_assessments/tests/test_dfo_northern_mackerel_translation.R
@@ -6,7 +6,7 @@ source(file.path(root, "R", "read_database.R"))
source(file.path(root, "R", "database_to_tam_obs.R"))
assessment_id <- "dfo_northern_mackerel_2025"
-source_data <- read_assessment(assessment_id, read_database(tier = "preliminary"))
+source_data <- read_assessment(assessment_id, read_database())
original <- source_data
translated <- .test_stock(source_data)
obs <- translated$obs
@@ -16,7 +16,7 @@ stopifnot(
identical(translated$years, 1969:2025),
identical(translated$ages, 1:10),
translated$age_plus_group == 10,
- tinyAM::check_obs(obs),
+ tinyAM::check_tam_data(obs),
nrow(obs$catch) == 570L,
nrow(obs$index) == 0L,
nrow(obs$weight) == 570L,
diff --git a/analysis/comp_assessments/tests/test_ebs_cod_translation.R b/analysis/comp_assessments/tests/test_ebs_cod_translation.R
index ac26cac5..26f13e35 100644
--- a/analysis/comp_assessments/tests/test_ebs_cod_translation.R
+++ b/analysis/comp_assessments/tests/test_ebs_cod_translation.R
@@ -7,7 +7,8 @@ stopifnot(identical(x,original),is.null(s$fit),identical(s$years,2000:2023),
identical(s$ages,1:12),!"start_par" %in% names(s$fit_call),
all(s$obs$index$samp_time==.5),all(s$obs$maturity$spawn_time==0),
all(s$obs$pop_covariates$M_assumption==.3866),
- s$settings$rec_settings$sd==.6908,all(is.finite(s$obs$weight$obs)),
+ is.null(s$settings$rec_settings$sd),
+ identical(s$settings$rec_settings$process,"iid"),all(is.finite(s$obs$weight$obs)),
all(s$obs$weight$obs>0),all(s$obs$maturity$obs>=0 & s$obs$maturity$obs<=.5),
all(abs(rowSums(s$key)-1)<1e-12))
raw <- x$inputs
@@ -26,7 +27,7 @@ coverage <- vapply(split(length_rows,length_rows$year),function(z)
stopifnot(min(coverage)>.994,all(coverage<1),
all(s$comparison_definitions$F$status=="approximate"),
s$comparison_definitions$ssb$status=="approximate")
-stopifnot(s$dat$F_settings$process == "rw")
+stopifnot(s$dat$F_settings$process == "cor_rw")
# The reviewed shared-level alternative must not add free projection means.
candidate_obs <- s$obs
candidate_obs$catch$F_year <- factor(candidate_obs$catch$year)
diff --git a/analysis/comp_assessments/tests/test_ebs_translation.R b/analysis/comp_assessments/tests/test_ebs_translation.R
index a6ba22af..a5902e65 100644
--- a/analysis/comp_assessments/tests/test_ebs_translation.R
+++ b/analysis/comp_assessments/tests/test_ebs_translation.R
@@ -18,7 +18,7 @@ stopifnot(
)
settings <- translated$settings
-tinyAM::check_obs(obs)
+tinyAM::check_tam_data(obs)
dat <- do.call(tinyAM::prepare_tam, c(
list(data = obs, years = translated$years, ages = translated$ages),
settings
@@ -30,16 +30,6 @@ age1_ats <- obs$index[
drop = FALSE
]
ats_old_age1 <- age1_ats[age1_ats$year < 2024, , drop = FALSE]
-high_age <- obs$index[obs$index$age >= 9, , drop = FALSE]
-high_age_keys <- unique(high_age[c("survey", "age", "q_key")])
-keys_by_age <- aggregate(
- q_key ~ survey + age, high_age_keys,
- function(x) length(unique(as.character(x)))
-)
-keys_by_survey <- aggregate(
- q_key ~ survey, high_age_keys,
- function(x) length(unique(as.character(x)))
-)
m_values <- unique(obs$pop_covariates[c("age", "M_assumption")])
m_values <- m_values[order(m_values$age), ]
@@ -49,20 +39,16 @@ stopifnot(
translated$age_plus_group == 15,
identical(dat$years, 1964:2024),
identical(dat$ages, 1:15),
- identical(settings$N_settings$process, "iid"),
+ identical(settings$N_settings$process, "off"),
identical(settings$N_settings$init, "exp"),
- identical(settings$F_settings$process, "rw"),
+ identical(settings$F_settings$process, "cor_rw"),
identical(settings$M_settings$process, "off"),
identical(settings$M_settings$mu_form, NULL),
grepl("M_assumption",
paste(deparse(settings$M_settings$mu_supplied), collapse = "")),
- grepl("q_key", paste(deparse(settings$index_settings$q_form), collapse = "")),
+ grepl("logistic(age, by = survey)", paste(deparse(settings$index_settings$q_form), collapse = ""), fixed = TRUE),
grepl("survey", paste(deparse(settings$index_settings$sd_form), collapse = "")),
- grepl("age", paste(deparse(settings$catch_settings$sd_form), collapse = ""), fixed = TRUE),
- grepl("I(age^2)",
- paste(deparse(settings$catch_settings$sd_form), collapse = ""),
- fixed = TRUE
- ),
+ identical(deparse(settings$catch_settings$sd_form), "~1"),
identical(
survey_names,
sort(c("NMFS bottom-trawl VAST", "NMFS acoustic-trawl"))
@@ -70,16 +56,14 @@ stopifnot(
!"NMFS acoustic-trawl age-1 index" %in% survey_names,
nrow(ats_old_age1) > 0L,
!any(age1_ats$year == 2024),
- nrow(high_age_keys) > 0L,
- all(high_age$q_age_block == "9+"),
- all(keys_by_age$q_key == 1L),
- all(keys_by_survey$q_key == 1L),
- length(unique(as.character(high_age_keys$q_key))) == length(survey_names),
+ any(obs$index$age >= 9),
+ ncol(dat$q_modmat) == length(survey_names),
all(m_values$age == 1:15),
isTRUE(all.equal(m_values$M_assumption, c(0.9, 0.45, rep(0.3, 13)))),
identical(translated$comparison_age_groups$N[["10+"]], 10:15),
identical(translated$comparison_age_groups$biomass_at_age[["3+"]], 3:15),
- identical(translated$comparison_definitions$ssb$status, "approximate")
+ identical(translated$comparison_definitions$ssb$status, "matched"),
+ all(obs$maturity$spawn_time == 0.25)
)
cat("EBS pollock translation tests passed.\n")
diff --git a/analysis/comp_assessments/tests/test_expansion_01_translations.R b/analysis/comp_assessments/tests/test_expansion_01_translations.R
index 7bc8c0b8..70039531 100644
--- a/analysis/comp_assessments/tests/test_expansion_01_translations.R
+++ b/analysis/comp_assessments/tests/test_expansion_01_translations.R
@@ -1,6 +1,6 @@
source("analysis/comp_assessments/R/run_assessment.R")
source("analysis/comp_assessments/tests/helper_stock.R")
-database <- read_database(tier = "preliminary")
+database <- read_database()
testthat::test_that("the expansion register keeps all twenty distinct source targets visible", {
batch <- read.csv("analysis/comp_assessments/expansion_batch_01.csv", stringsAsFactors = FALSE)
@@ -9,8 +9,8 @@ testthat::test_that("the expansion register keeps all twenty distinct source tar
testthat::expect_false(anyDuplicated(batch$assessment_id) > 0L)
testthat::expect_true(all(batch$assessment_id %in% database$assessments$assessment_id))
# The batch register records its original source review; do not relabel it
- # when current default reads advance to SAIO main.
- testthat::expect_true(all(batch$data_revision == .saio_snapshot_revision("preliminary")))
+ # when the selected local snapshot is refreshed.
+ testthat::expect_true(all(batch$data_revision == "6f08c06c49e4b73591427a1495bfb4568caf49f0"))
testthat::expect_true(all(nzchar(batch$limitation)))
translations <- assessment_translations("preliminary")
selected <- translations[match(batch$assessment_id, translations$assessment_id), ]
diff --git a/analysis/comp_assessments/tests/test_goa_cod_translation.R b/analysis/comp_assessments/tests/test_goa_cod_translation.R
index 6a209701..843bb17b 100644
--- a/analysis/comp_assessments/tests/test_goa_cod_translation.R
+++ b/analysis/comp_assessments/tests/test_goa_cod_translation.R
@@ -11,8 +11,7 @@ obs <- translated$obs
settings <- translated$settings
stock <- .assessment_stock(source_data, do_fit = FALSE, silent = TRUE)$env
-annual_years <- sort(unique(stats::na.omit(stock$age_key$year)))
-pooled_key <- stock$age_key[is.na(stock$age_key$year), , drop = FALSE]
+pooled_key <- stock$age_key
pooled_catch <- stock$.goa_age_shares(
stock$inputs, pooled_key, "catch", "Trawl fishery", "",
stock$years, stock$ages
@@ -27,23 +26,19 @@ trawl_shares <- stock$catch_shares[
]
pooled_catch_match <- match(share_key(trawl_shares), share_key(pooled_catch))
-tinyAM::check_obs(obs)
+tinyAM::check_tam_data(obs)
stopifnot(
identical(source_data$inputs, inputs_before),
identical(translated$years, 2007:2025),
identical(translated$ages, 1:10),
translated$age_plus_group == 10,
- identical(annual_years, 2007:2024),
+ !"year" %in% names(pooled_key),
nrow(pooled_key) > 0L,
!anyNA(pooled_catch_match),
- any(abs(
- trawl_shares$proportion -
- pooled_catch$proportion[pooled_catch_match]
- ) > 1e-6),
- isTRUE(all.equal(
- trawl_shares$proportion[trawl_shares$year == 2025],
- pooled_catch$proportion[pooled_catch$year == 2025]
- )),
+ isTRUE(all.equal(trawl_shares$proportion,
+ pooled_catch$proportion[pooled_catch_match])),
+ all(abs(aggregate(age_given_length ~ type + fleet + survey +
+ length_bin_lower + length_bin_upper, pooled_key, sum)$age_given_length - 1) < 1e-12),
isTRUE(all.equal(stock$index_shares, pooled_index)),
nrow(obs$catch) == 19L * 10L,
nrow(obs$index) == 10L * 10L,
@@ -51,8 +46,8 @@ stopifnot(
all(is.finite(obs$index$obs)),
identical(settings$N_settings$process, "off"),
identical(settings$N_settings$init, "exp"),
- identical(settings$F_settings$process, "iid"),
- grepl("age", paste(deparse(settings$F_settings$mu_form), collapse = "")),
+ identical(settings$F_settings$process, "cor_rw"),
+ is.null(settings$F_settings$mu_form),
identical(settings$M_settings$process, "off"),
identical(settings$M_settings$mu_form, NULL),
grepl(
@@ -60,10 +55,10 @@ stopifnot(
paste(deparse(settings$M_settings$mu_supplied), collapse = "")
),
grepl(
- "q_block",
- paste(deparse(settings$index_settings$q_form), collapse = "")
+ "logistic(age)",
+ paste(deparse(settings$index_settings$q_form), collapse = ""), fixed = TRUE
),
- length(unique(obs$index$q_block)) > 1L,
+ grepl("rw(year)", paste(deparse(settings$index_settings$q_form), collapse = ""), fixed = TRUE),
all(is.finite(obs$pop_covariates$M_assumption)),
all(obs$pop_covariates$M_assumption[obs$pop_covariates$year %in% 2014:2016] == 0.84),
all(obs$pop_covariates$M_assumption[!obs$pop_covariates$year %in% 2014:2016] == 0.50),
diff --git a/analysis/comp_assessments/tests/test_goa_pollock_translation.R b/analysis/comp_assessments/tests/test_goa_pollock_translation.R
index 6be388c2..99b242c4 100644
--- a/analysis/comp_assessments/tests/test_goa_pollock_translation.R
+++ b/analysis/comp_assessments/tests/test_goa_pollock_translation.R
@@ -3,7 +3,7 @@ source("analysis/comp_assessments/R/read_database.R")
source("analysis/comp_assessments/R/run_assessment.R")
assessment <- run_assessment("afsc_pollock_goa_2024", fit = FALSE)
-comparison_outputs <- assessment$comparison$comparison_outputs
+comparison_outputs <- assessment$source$outputs
f <- comparison_outputs[comparison_outputs$measure == "fishing_mortality_at_age" &
comparison_outputs$source_type == "derived_source_output", ]
@@ -14,8 +14,8 @@ stopifnot(
nrow(f) == 371L,
all(f$year %in% 1970:2023),
all(f$age %in% 1:8),
- identical(assessment$comparison$comparison_definitions$F$status, "approximate"),
- any(grepl("partial reconstruction", assessment$background, fixed = TRUE))
+ is.null(assessment$comparison$comparison_definitions$F),
+ any(grepl("official F-at-age is unavailable", assessment$background, fixed = TRUE))
)
-cat("GOA pollock translation retains the approximate, partial F comparison without fitting.\n")
+cat("GOA pollock translation preserves source-derived F records and the documented native-F limitation without fitting.\n")
diff --git a/analysis/comp_assessments/tests/test_goa_translation.R b/analysis/comp_assessments/tests/test_goa_translation.R
index 278cd737..bdcb0567 100644
--- a/analysis/comp_assessments/tests/test_goa_translation.R
+++ b/analysis/comp_assessments/tests/test_goa_translation.R
@@ -10,7 +10,7 @@ source_data <- read_assessment("afsc_pollock_goa_2024", database)
translated <- .test_stock(source_data)
obs <- translated$obs
-tinyAM::check_obs(obs)
+tinyAM::check_tam_data(obs)
stopifnot(
any(is.finite(obs$catch$obs[obs$catch$age == 1])),
any(is.finite(obs$catch$obs[obs$catch$age == 2])),
diff --git a/analysis/comp_assessments/tests/test_herring_translation.R b/analysis/comp_assessments/tests/test_herring_translation.R
index dbdc0363..e14094b8 100644
--- a/analysis/comp_assessments/tests/test_herring_translation.R
+++ b/analysis/comp_assessments/tests/test_herring_translation.R
@@ -5,12 +5,12 @@ pkgload::load_all(".", quiet = TRUE)
source(file.path(root, "R", "read_database.R"))
source(file.path(root, "R", "database_to_tam_obs.R"))
-database <- read_committed_database()
-source_data <- read_committed_assessment("ices_herring_north_sea_2026", database)
+database <- read_database()
+source_data <- read_assessment("ices_herring_north_sea_2026", database)
translated <- .test_stock(source_data)
obs <- translated$obs
-tinyAM::check_obs(obs)
+tinyAM::check_tam_data(obs)
dat <- do.call(tinyAM::prepare_tam, c(
list(data = obs, years = translated$years, ages = translated$ages),
translated$settings
diff --git a/analysis/comp_assessments/tests/test_ices_bluewhiting_translation.R b/analysis/comp_assessments/tests/test_ices_bluewhiting_translation.R
index bc6aa3dc..18401672 100644
--- a/analysis/comp_assessments/tests/test_ices_bluewhiting_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_bluewhiting_translation.R
@@ -8,14 +8,18 @@ source(file.path(root, "R", "database_to_tam_obs.R"))
assessment_id <- "ices_bluewhiting_northeast_atlantic_2026"
database <- read_database()
source_data <- read_assessment(assessment_id, database)
+before <- source_data
translated <- .test_stock(source_data)
source_q <- source_data$outputs[
source_data$outputs$type == "catchability" & source_data$outputs$measure == "q", , drop = FALSE
]
-comparison_q <- translated$comparison_outputs[
- translated$comparison_outputs$type == "catchability" &
- translated$comparison_outputs$measure == "q", , drop = FALSE
-]
+expected_q <- vapply(c("1", "2", "3", "4", "5-8"), function(group) {
+ rows <- if (group == "5-8") source_q[source_q$age_group %in% group, , drop = FALSE] else
+ source_q[as.character(source_q$age) %in% group, , drop = FALSE]
+ unique(rows$value) * 1000
+}, numeric(1))
+names(expected_q) <- names(translated$start_par$log_q)
+
dat <- do.call(tinyAM::prepare_tam, c(
list(data = translated$obs, years = translated$years, ages = translated$ages),
translated$settings
@@ -23,7 +27,7 @@ dat <- do.call(tinyAM::prepare_tam, c(
par <- tinyAM::make_par(dat)
stopifnot(
- tinyAM::check_obs(translated$obs),
+ tinyAM::check_tam_data(translated$obs),
identical(translated$years, 1981:2026),
identical(translated$ages, 1:10),
nrow(translated$obs$catch) == 460L,
@@ -37,9 +41,9 @@ stopifnot(
identical(names(translated$start_par$log_sd_index), names(par$log_sd_index)),
length(unique(translated$obs$index$q_key)) == 5L,
setequal(levels(translated$obs$index$sd_group), c("1", "2", "3", "4-6", "7-8")),
- isTRUE(all.equal(comparison_q$value, source_q$value * 1000)),
- all(comparison_q$unit == "unitless"),
- all(grepl("Converted q by 1000", comparison_q$notes))
+ isTRUE(all.equal(exp(translated$start_par$log_q), expected_q)),
+ identical(source_data, before),
+ all(is.finite(translated$start_par$log_q))
)
cat("Blue whiting translation structure passed.\n")
diff --git a/analysis/comp_assessments/tests/test_ices_cod_celtic_sea_2026_translation.R b/analysis/comp_assessments/tests/test_ices_cod_celtic_sea_2026_translation.R
index 8b0def45..8fc8e85b 100644
--- a/analysis/comp_assessments/tests/test_ices_cod_celtic_sea_2026_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_cod_celtic_sea_2026_translation.R
@@ -15,7 +15,7 @@ source_F <- .translation_start_surface(
)
stopifnot(
- tinyAM::check_obs(obs),
+ tinyAM::check_tam_data(obs),
identical(translated$years, 1980:2025),
identical(translated$ages, 1:7),
translated$age_plus_group == 7L,
diff --git a/analysis/comp_assessments/tests/test_ices_cod_faroe_plateau_2025_translation.R b/analysis/comp_assessments/tests/test_ices_cod_faroe_plateau_2025_translation.R
index d03ebe37..e0867e53 100644
--- a/analysis/comp_assessments/tests/test_ices_cod_faroe_plateau_2025_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_cod_faroe_plateau_2025_translation.R
@@ -1,10 +1,10 @@
root <- file.path("analysis", "comp_assessments")
pkgload::load_all(".", quiet = TRUE)
-source(file.path(root, "R", "read_database.R"))
+source(file.path(root, "R", "run_assessment.R"))
source(file.path(root, "R", "database_to_tam_obs.R"))
id <- "ices_cod_faroe_plateau_2025"
-source_data <- read_assessment(id, read_database(tier = "preliminary"))
+source_data <- read_assessment(id, read_database())
env <- new.env(parent = globalenv())
env$source <- source_data
env$do_fit <- FALSE
diff --git a/analysis/comp_assessments/tests/test_ices_cod_iceland_2026_translation.R b/analysis/comp_assessments/tests/test_ices_cod_iceland_2026_translation.R
index bf1975de..8b71b442 100644
--- a/analysis/comp_assessments/tests/test_ices_cod_iceland_2026_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_cod_iceland_2026_translation.R
@@ -1,7 +1,7 @@
root <- file.path("analysis", "comp_assessments")
-source(file.path(root, "R", "read_database.R"))
+source(file.path(root, "R", "run_assessment.R"))
source(file.path(root, "R", "database_to_tam_obs.R"))
-source_data <- read_assessment("ices_cod_iceland_2026", read_database(tier = "preliminary"))
+source_data <- read_assessment("ices_cod_iceland_2026", read_database())
env <- new.env(parent = globalenv())
env$source <- source_data
env$do_fit <- FALSE
diff --git a/analysis/comp_assessments/tests/test_ices_cod_north_sea_translation.R b/analysis/comp_assessments/tests/test_ices_cod_north_sea_translation.R
index bfe80946..000236f0 100644
--- a/analysis/comp_assessments/tests/test_ices_cod_north_sea_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_cod_north_sea_translation.R
@@ -5,13 +5,13 @@ pkgload::load_all(".", quiet = TRUE)
source(file.path(root, "R", "read_database.R"))
source(file.path(root, "R", "database_to_tam_obs.R"))
-database <- read_committed_database()
-source_data <- read_committed_assessment("ices_cod_north_sea_2025", database)
+database <- read_database()
+source_data <- read_assessment("ices_cod_north_sea_2025", database)
source_inputs <- source_data$inputs
translated <- .test_stock(source_data)
obs <- translated$obs
-tinyAM::check_obs(obs)
+tinyAM::check_tam_data(obs)
dat <- do.call(tinyAM::prepare_tam, c(
list(data = obs, years = translated$years, ages = translated$ages),
translated$settings
@@ -32,7 +32,9 @@ comparison_total <- comparison[comparison$measure == "total_numbers", ]
source_n <- source_data$outputs[source_data$outputs$measure == "numbers_at_age", ]
sum_n <- stats::aggregate(value ~ year + age, source_n, sum)
source_total <- source_data$outputs[
- source_data$outputs$measure == "total_numbers", ]
+ source_data$outputs$measure == "total_numbers" &
+ source_data$outputs$source_type == "derived_source_output" &
+ is.na(source_data$outputs$region), ]
sum_total <- stats::aggregate(value ~ year, source_n, sum)
source_ssb <- source_data$outputs[source_data$outputs$measure == "SSB", ]
sum_ssb <- stats::aggregate(value ~ year, source_ssb, sum)
@@ -95,14 +97,7 @@ stopifnot(
as.numeric(source_total$value),
sum_total$value[match(source_total$year, sum_total$year)]
)),
- nrow(comparison_total) == 43L,
- identical(as.integer(comparison_total$year), 1983:2025),
- all(comparison_total$source_type == "derived_source_output"),
- all(is.na(comparison_total$age)),
- isTRUE(all.equal(
- as.numeric(comparison_total$value),
- sum_total$value[match(comparison_total$year, sum_total$year)]
- )),
+ nrow(comparison_total) == 0L, # Compare the complete N-at-age sums directly below.
all(is.na(comparison$region)),
isTRUE(all.equal(
comparison$value[comparison$measure == "numbers_at_age"],
diff --git a/analysis/comp_assessments/tests/test_ices_cod_northern_coastal_2026_source.R b/analysis/comp_assessments/tests/test_ices_cod_northern_coastal_2026_source.R
index bdeebb24..1a80d450 100644
--- a/analysis/comp_assessments/tests/test_ices_cod_northern_coastal_2026_source.R
+++ b/analysis/comp_assessments/tests/test_ices_cod_northern_coastal_2026_source.R
@@ -3,7 +3,7 @@ pkgload::load_all(".", quiet = TRUE)
source(file.path(root, "R", "run_assessment.R"))
id <- "ices_cod_northern_coastal_2026"
-database <- read_database(tier = "preliminary")
+database <- read_database()
assessment <- read_assessment(id, database)
inputs <- assessment$inputs
outputs <- assessment$outputs
diff --git a/analysis/comp_assessments/tests/test_ices_cod_west_scotland_2022_translation.R b/analysis/comp_assessments/tests/test_ices_cod_west_scotland_2022_translation.R
index a9237bde..4bf1dedd 100644
--- a/analysis/comp_assessments/tests/test_ices_cod_west_scotland_2022_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_cod_west_scotland_2022_translation.R
@@ -1,6 +1,6 @@
pkgload::load_all(".", quiet = TRUE)
source("analysis/comp_assessments/R/run_assessment.R")
-record <- read_assessment("ices_cod_west_scotland_2022", read_database(tier = "preliminary"))
+record <- read_assessment("ices_cod_west_scotland_2022", read_database())
before <- record
stock <- .assessment_stock(record, do_fit = FALSE, silent = TRUE)
stopifnot(is.null(stock$error), is.null(stock$env$fit), identical(record, before))
diff --git a/analysis/comp_assessments/tests/test_ices_cod_western_baltic_2025_translation.R b/analysis/comp_assessments/tests/test_ices_cod_western_baltic_2025_translation.R
index 42a9fa16..44a2f1ef 100644
--- a/analysis/comp_assessments/tests/test_ices_cod_western_baltic_2025_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_cod_western_baltic_2025_translation.R
@@ -1,7 +1,7 @@
pkgload::load_all(".", quiet = TRUE)
source("analysis/comp_assessments/R/run_assessment.R")
-database <- read_database(tier = "preliminary")
+database <- read_database()
source_data <- read_assessment("ices_cod_western_baltic_2025", database)
translation <- new.env(parent = environment())
translation$source <- source_data
@@ -32,7 +32,7 @@ stopifnot(
length(unique(index$q_key)) == 10L,
all(!is.na(index$sd_block)),
nrow(obs$pop_covariates) == length(years) * length(ages),
- setequal(levels(obs$pop_covariates$f_age_group), c("age0", "age1", "age2", "age3_4", "age5plus")),
+ setequal(levels(obs$catch$f_age_group), c("age0", "age1", "age2", "age3_4", "age5plus")),
all(obs$maturity$spawn_time == 0)
)
diff --git a/analysis/comp_assessments/tests/test_ices_cod_western_baltic_2026_translation.R b/analysis/comp_assessments/tests/test_ices_cod_western_baltic_2026_translation.R
index af63b90d..d45ae9b5 100644
--- a/analysis/comp_assessments/tests/test_ices_cod_western_baltic_2026_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_cod_western_baltic_2026_translation.R
@@ -1,7 +1,7 @@
pkgload::load_all(".", quiet = TRUE)
source("analysis/comp_assessments/R/run_assessment.R")
-database <- read_database(tier = "preliminary")
+database <- read_database()
source_data <- read_assessment("ices_cod_western_baltic_2026", database)
translation <- new.env(parent = environment())
translation$source <- source_data
diff --git a/analysis/comp_assessments/tests/test_ices_haddock_celtic_sea_source.R b/analysis/comp_assessments/tests/test_ices_haddock_celtic_sea_source.R
index 4168c4b4..7b266bd7 100644
--- a/analysis/comp_assessments/tests/test_ices_haddock_celtic_sea_source.R
+++ b/analysis/comp_assessments/tests/test_ices_haddock_celtic_sea_source.R
@@ -4,7 +4,7 @@ source(file.path(root, "R", "read_database.R"))
source(file.path(root, "R", "database_to_tam_obs.R"))
assessment_id <- "ices_haddock_celtic_sea_7b_k_2026"
-database <- read_database(tier = "preliminary")
+database <- read_database()
source_record <- read_assessment(assessment_id, database)
inputs <- source_record$inputs
outputs <- source_record$outputs
diff --git a/analysis/comp_assessments/tests/test_ices_haddock_celtic_sea_translation.R b/analysis/comp_assessments/tests/test_ices_haddock_celtic_sea_translation.R
index 68d84a79..24a8f83a 100644
--- a/analysis/comp_assessments/tests/test_ices_haddock_celtic_sea_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_haddock_celtic_sea_translation.R
@@ -22,7 +22,7 @@ zero_source_catch <- source_record$inputs[
]
zero_source_rows <- match(weight_key(zero_source_catch), weight_key(obs$catch))
-tinyAM::check_obs(obs)
+tinyAM::check_tam_data(obs)
stopifnot(
identical(translated$years, 1993:2025),
identical(translated$ages, 0:8),
diff --git a/analysis/comp_assessments/tests/test_ices_haddock_faroe_2025_translation.R b/analysis/comp_assessments/tests/test_ices_haddock_faroe_2025_translation.R
index 4e9f9452..4d9b90ba 100644
--- a/analysis/comp_assessments/tests/test_ices_haddock_faroe_2025_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_haddock_faroe_2025_translation.R
@@ -1,7 +1,7 @@
pkgload::load_all(".", quiet = TRUE)
source("analysis/comp_assessments/R/run_assessment.R")
-database <- read_database(tier = "preliminary")
+database <- read_database()
assessment <- read_assessment("ices_haddock_faroe_2025", database)
env <- new.env(parent = environment())
env$source <- assessment
diff --git a/analysis/comp_assessments/tests/test_ices_haddock_iceland_2026_source.R b/analysis/comp_assessments/tests/test_ices_haddock_iceland_2026_source.R
index e0f23428..1f229fd2 100644
--- a/analysis/comp_assessments/tests/test_ices_haddock_iceland_2026_source.R
+++ b/analysis/comp_assessments/tests/test_ices_haddock_iceland_2026_source.R
@@ -71,13 +71,7 @@ stopifnot(
other_assessment <- database$assessments[
database$assessments$assessment_id == "ices_haddock_iceland_2025", ]
stopifnot(nrow(other_assessment) == 1L, !isTRUE(other_assessment$is_current))
-review <- paste(readLines(file.path(saio_path(), "source_reviews", "ices_haddock_iceland_2026.md"), warn = FALSE), collapse = " ")
-stopifnot(
- grepl("4,000 g", review, fixed = TRUE),
- grepl("role in the assessment projections is not specified", review, fixed = TRUE),
- grepl("Fbar", review, fixed = TRUE),
- grepl("interval coverage", review, fixed = TRUE),
- grepl("not used to fill gaps", review, fixed = TRUE)
-)
+# Source-review text is validated in SAIO; this test checks the cached source records.
+
cat("Icelandic haddock 2026 source records passed.\n")
diff --git a/analysis/comp_assessments/tests/test_ices_haddock_iceland_2026_translation.R b/analysis/comp_assessments/tests/test_ices_haddock_iceland_2026_translation.R
index 1541f788..53351bd7 100644
--- a/analysis/comp_assessments/tests/test_ices_haddock_iceland_2026_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_haddock_iceland_2026_translation.R
@@ -5,7 +5,7 @@ source(file.path(root, "R", "read_database.R"))
source(file.path(root, "R", "database_to_tam_obs.R"))
assessment_id <- "ices_haddock_iceland_2026"
-database <- read_database(tier = "preliminary")
+database <- read_database()
source_data <- read_assessment(assessment_id, database)
original <- source_data
translated <- .test_stock(source_data)
diff --git a/analysis/comp_assessments/tests/test_ices_haddock_irish_sea_7a_2025_source.R b/analysis/comp_assessments/tests/test_ices_haddock_irish_sea_7a_2025_source.R
index 3958a437..081d2d90 100644
--- a/analysis/comp_assessments/tests/test_ices_haddock_irish_sea_7a_2025_source.R
+++ b/analysis/comp_assessments/tests/test_ices_haddock_irish_sea_7a_2025_source.R
@@ -3,7 +3,7 @@ source("analysis/comp_assessments/R/database_to_tam_obs.R")
assessment_id <- "ices_haddock_irish_sea_7a_2025"
advice_id <- "ices_haddock_irish_sea_7a_2026_summary"
-database <- read_database(tier = "preliminary")
+database <- read_database()
assessment <- read_assessment(assessment_id, database)
i <- assessment$inputs
o <- assessment$outputs
@@ -58,7 +58,7 @@ stopifnot(
)
source("analysis/comp_assessments/R/run_assessment.R")
-prepared <- run_assessment(assessment_id, fit = FALSE, dashboard = FALSE, cache = FALSE)
+prepared <- run_assessment(assessment_id, fit = FALSE, cache = FALSE)
stopifnot(
identical(prepared$diagnostics$status, "not_fitted"),
!is.null(prepared$obs),
diff --git a/analysis/comp_assessments/tests/test_ices_haddock_rockall_translation.R b/analysis/comp_assessments/tests/test_ices_haddock_rockall_translation.R
index bae013df..00d893eb 100644
--- a/analysis/comp_assessments/tests/test_ices_haddock_rockall_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_haddock_rockall_translation.R
@@ -1,9 +1,9 @@
root <- file.path("analysis", "comp_assessments")
-source(file.path(root, "R", "read_database.R"))
+source(file.path(root, "R", "run_assessment.R"))
source(file.path(root, "R", "database_to_tam_obs.R"))
.translation_abort <- function(message) stop(message, call. = FALSE)
id <- "ices_haddock_rockall_2026"
-source_data <- read_assessment(id, read_database(tier = "preliminary"))
+source_data <- read_assessment(id, read_database())
env <- new.env(parent = globalenv())
env$source <- source_data
env$do_fit <- FALSE
diff --git a/analysis/comp_assessments/tests/test_ices_herring_gulf_riga_2026_translation.R b/analysis/comp_assessments/tests/test_ices_herring_gulf_riga_2026_translation.R
index 9ab0e355..a9fb7433 100644
--- a/analysis/comp_assessments/tests/test_ices_herring_gulf_riga_2026_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_herring_gulf_riga_2026_translation.R
@@ -1,7 +1,7 @@
pkgload::load_all(".", quiet = TRUE)
source("analysis/comp_assessments/R/run_assessment.R")
-database <- read_database(tier = "preliminary")
+database <- read_database()
source_record <- read_assessment("ices_herring_gulf_riga_2026", database)
stock <- .assessment_stock(source_record, do_fit = FALSE, silent = TRUE)
stopifnot(is.null(stock$error))
diff --git a/analysis/comp_assessments/tests/test_ices_herring_iceland_summer_translation.R b/analysis/comp_assessments/tests/test_ices_herring_iceland_summer_translation.R
index 4bed1353..a365edb1 100644
--- a/analysis/comp_assessments/tests/test_ices_herring_iceland_summer_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_herring_iceland_summer_translation.R
@@ -4,7 +4,7 @@ pkgload::load_all(".", quiet = TRUE)
source(file.path(root, "R", "run_assessment.R"))
assessment_id <- "ices_herring_iceland_summer_2026"
-source_data <- read_assessment(assessment_id, read_database(tier = "preliminary"))
+source_data <- read_assessment(assessment_id, read_database())
stock_env <- new.env(parent = environment(.assessment_stock))
stock_env$source <- source_data
stock_env$do_fit <- FALSE
@@ -21,7 +21,7 @@ stopifnot(
sum(is.na(stock_env$obs$catch$obs)) == 4L,
all(stock_env$obs$catch$obs[!is.na(stock_env$obs$catch$obs)] > 0),
all(stock_env$obs$index$obs > 0),
- identical(levels(stock_env$obs$pop_covariates$F_age_block),
+ identical(levels(stock_env$obs$catch$F_age_block),
c(paste0("age", 2:6), "age7plus")),
identical(levels(stock_env$obs$index$q_age),
c("age2to3", "age4", "age5", "age6", "age7plus")),
diff --git a/analysis/comp_assessments/tests/test_ices_herring_norwegian_spring_2026_translation.R b/analysis/comp_assessments/tests/test_ices_herring_norwegian_spring_2026_translation.R
index 75d186a6..85b7c4d5 100644
--- a/analysis/comp_assessments/tests/test_ices_herring_norwegian_spring_2026_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_herring_norwegian_spring_2026_translation.R
@@ -4,14 +4,14 @@ pkgload::load_all(".", quiet = TRUE)
source(file.path(root, "R", "run_assessment.R"))
assessment_id <- "ices_herring_norwegian_spring_2026"
-database <- read_database(tier = "preliminary")
+database <- read_database()
result <- run_assessment(assessment_id, database = database, fit = FALSE)
obs <- result$obs
stopifnot(
identical(result$diagnostics$status, "not_fitted"),
is.null(result$fit),
- tinyAM::check_obs(obs),
+ tinyAM::check_tam_data(obs),
identical(sort(unique(as.integer(obs$catch$year))), 1988:2025),
identical(sort(unique(as.integer(obs$catch$age))), 2:12),
nrow(obs$catch) == 38L * 11L,
diff --git a/analysis/comp_assessments/tests/test_ices_herring_norwegian_spring_translation.R b/analysis/comp_assessments/tests/test_ices_herring_norwegian_spring_translation.R
index 212c12fd..fa6035a0 100644
--- a/analysis/comp_assessments/tests/test_ices_herring_norwegian_spring_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_herring_norwegian_spring_translation.R
@@ -36,7 +36,7 @@ n_source <- source_data$outputs[source_data$outputs$type == "population" &
source_data$outputs$age == "2", ]
stopifnot(
- tinyAM::check_obs(translated$obs),
+ tinyAM::check_tam_data(translated$obs),
identical(translated$years, 1988:2024),
identical(translated$ages, 2:12),
nrow(translated$obs$catch) == 37L * 11L,
diff --git a/analysis/comp_assessments/tests/test_ices_herring_west_scotland_ireland_translation.R b/analysis/comp_assessments/tests/test_ices_herring_west_scotland_ireland_translation.R
index 1c78d81e..3cf95b46 100644
--- a/analysis/comp_assessments/tests/test_ices_herring_west_scotland_ireland_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_herring_west_scotland_ireland_translation.R
@@ -3,7 +3,7 @@ pkgload::load_all(".", quiet = TRUE)
source(file.path(root, "R", "run_assessment.R"))
assessment_id <- "ices_herring_west_scotland_ireland_2020"
-database <- read_database(tier = "preliminary")
+database <- read_database()
source_data <- read_assessment(assessment_id, database)
translated <- run_assessment(
assessment_id, database = database, fit = FALSE, silent = TRUE
@@ -40,7 +40,7 @@ stopifnot(
identical(translated$diagnostics$status, "not_fitted"),
is.null(translated$fit),
inherits(obs, "tam_data"),
- tinyAM::check_obs(obs),
+ tinyAM::check_tam_data(obs),
sum(is.finite(obs$catch$obs)) == 567L,
isTRUE(all.equal(obs$catch$obs, expected_catch, tolerance = 1e-12)),
identical(unique(source_catch$fleet), c("Catch 6aN", "Catch 6aS/7bc")),
diff --git a/analysis/comp_assessments/tests/test_ices_norway_pout_translation.R b/analysis/comp_assessments/tests/test_ices_norway_pout_translation.R
index 2187069e..1f079053 100644
--- a/analysis/comp_assessments/tests/test_ices_norway_pout_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_norway_pout_translation.R
@@ -101,7 +101,7 @@ common_ssb <- comparison[
common_ssb <- stats::aggregate(value ~ year, common_ssb, sum)
stopifnot(
- tinyAM::check_obs(translated$obs),
+ tinyAM::check_tam_data(translated$obs),
identical(translated$years, 1984:2024),
identical(translated$ages, 0:3),
nrow(translated$obs$catch) == 41L * 4L,
@@ -148,10 +148,7 @@ stopifnot(
isTRUE(all.equal(common_ssb$value, as.numeric(ssb_q1$value))),
nrow(comparison[comparison$measure == "biomass_at_age", ]) == 41L * 4L,
nrow(source_q) == 13L,
- nrow(comparison_q) == 13L,
- isTRUE(all.equal(
- sort(as.numeric(comparison_q$value)), sort(as.numeric(source_q$value))
- )),
+ nrow(comparison_q) == 0L, # Native quarterly q is not an annual-q comparison.
all(is.na(comparison$se[comparison$source_type == "derived_common_definition"]))
)
diff --git a/analysis/comp_assessments/tests/test_ices_plaice_baltic_21_32_2026_source.R b/analysis/comp_assessments/tests/test_ices_plaice_baltic_21_32_2026_source.R
index d5cafcb4..84bf8d38 100644
--- a/analysis/comp_assessments/tests/test_ices_plaice_baltic_21_32_2026_source.R
+++ b/analysis/comp_assessments/tests/test_ices_plaice_baltic_21_32_2026_source.R
@@ -4,7 +4,7 @@ root <- "analysis/comp_assessments"
source(file.path(root, "R", "read_database.R"))
assessment_id <- "ices_plaice_baltic_21_32_2026"
-database <- read_database(tier = "preliminary")
+database <- read_database()
source_record <- read_assessment(assessment_id, database)
inputs <- source_record$inputs
outputs <- source_record$outputs
@@ -180,11 +180,7 @@ stopifnot(
all(as.numeric(maturity$value) >= 0 & as.numeric(maturity$value) <= 1)
)
-review <- paste(readLines(file.path(saio_path(), "source_reviews", "ices_plaice_baltic_21_32_2026.md"), warn = FALSE), collapse = " ")
-stopifnot(
- grepl("fbarRange=3:6", review, fixed = TRUE),
- grepl("14,910,084", review, fixed = TRUE),
- grepl("95% confidence", review, fixed = TRUE)
-)
+# Source-review text is validated in SAIO; this test checks the cached source records.
+
cat("ICES Baltic plaice source records passed.\n")
diff --git a/analysis/comp_assessments/tests/test_ices_plaice_baltic_21_32_2026_translation.R b/analysis/comp_assessments/tests/test_ices_plaice_baltic_21_32_2026_translation.R
index 519e25eb..7214a300 100644
--- a/analysis/comp_assessments/tests/test_ices_plaice_baltic_21_32_2026_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_plaice_baltic_21_32_2026_translation.R
@@ -5,7 +5,7 @@ source(file.path(root, "R", "read_database.R"))
source(file.path(root, "R", "database_to_tam_obs.R"))
assessment_id <- "ices_plaice_baltic_21_32_2026"
-database <- read_database(tier = "preliminary")
+database <- read_database()
source_record <- read_assessment(assessment_id, database)
translated <- .test_stock(source_record)
obs <- translated$obs
diff --git a/analysis/comp_assessments/tests/test_ices_plaice_english_channel_7d_2026_source.R b/analysis/comp_assessments/tests/test_ices_plaice_english_channel_7d_2026_source.R
index b0e065fc..d2a97b3f 100644
--- a/analysis/comp_assessments/tests/test_ices_plaice_english_channel_7d_2026_source.R
+++ b/analysis/comp_assessments/tests/test_ices_plaice_english_channel_7d_2026_source.R
@@ -8,11 +8,11 @@ source(file.path(root, "R", "database_to_tam_obs.R"))
assessment_id <- "ices_plaice_english_channel_7d_2026"
stock_id <- "ck_ices_wgnssk_platessa_ns_7d"
-database <- read_database(tier = "preliminary")
+database <- read_database()
source <- read_assessment(assessment_id, database)
inputs <- source$inputs
outputs <- source$outputs
-cache <- file.path(saio_path(), "source_cache", "ices_plaice_7d_2026")
+cache <- file.path("analysis", "comp_assessments", "source_cache", "ices_plaice_7d_2026")
data_env <- new.env(parent = baseenv())
load(file.path(cache, "run/data.RData"), envir = data_env)
dat <- data_env$dat
diff --git a/analysis/comp_assessments/tests/test_ices_plaice_english_channel_7d_2026_translation.R b/analysis/comp_assessments/tests/test_ices_plaice_english_channel_7d_2026_translation.R
index a1434b61..da723300 100644
--- a/analysis/comp_assessments/tests/test_ices_plaice_english_channel_7d_2026_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_plaice_english_channel_7d_2026_translation.R
@@ -4,7 +4,7 @@ source(file.path(root, "R", "run_assessment.R"))
source(file.path(root, "R", "read_database.R"))
source(file.path(root, "R", "database_to_tam_obs.R"))
-database <- read_database(tier = "preliminary")
+database <- read_database()
source <- read_assessment("ices_plaice_english_channel_7d_2026", database)
model <- .test_stock(source)
diff --git a/analysis/comp_assessments/tests/test_ices_plaice_irish_sea_7a_2026_translation.R b/analysis/comp_assessments/tests/test_ices_plaice_irish_sea_7a_2026_translation.R
index 7d4ff0b1..f85b48d1 100644
--- a/analysis/comp_assessments/tests/test_ices_plaice_irish_sea_7a_2026_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_plaice_irish_sea_7a_2026_translation.R
@@ -4,7 +4,7 @@ source(file.path(root, "R", "run_assessment.R"))
source(file.path(root, "R", "read_database.R"))
source(file.path(root, "R", "database_to_tam_obs.R"))
-database <- read_database(tier = "preliminary")
+database <- read_database()
source_record <- read_assessment("ices_plaice_irish_sea_7a_2026", database)
translated <- .test_stock(source_record)
diff --git a/analysis/comp_assessments/tests/test_ices_plaice_north_sea_translation.R b/analysis/comp_assessments/tests/test_ices_plaice_north_sea_translation.R
index 10ac82c3..20e9711d 100644
--- a/analysis/comp_assessments/tests/test_ices_plaice_north_sea_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_plaice_north_sea_translation.R
@@ -17,7 +17,7 @@ par <- tinyAM::make_par(dat)
inputs <- source_data$inputs
outputs <- source_data$outputs
stopifnot(
- tinyAM::check_obs(translated$obs),
+ tinyAM::check_tam_data(translated$obs),
identical(translated$years, 1957:2025),
identical(translated$ages, 1:10),
identical(source_data$assessment$terminal_year, 2025L),
diff --git a/analysis/comp_assessments/tests/test_ices_saithe_faroe_2025_translation.R b/analysis/comp_assessments/tests/test_ices_saithe_faroe_2025_translation.R
index d0c05765..fb32e8ab 100644
--- a/analysis/comp_assessments/tests/test_ices_saithe_faroe_2025_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_saithe_faroe_2025_translation.R
@@ -1,7 +1,7 @@
pkgload::load_all(".", quiet = TRUE)
source("analysis/comp_assessments/R/run_assessment.R")
-database <- read_database(tier = "preliminary")
+database <- read_database()
source_data <- read_assessment("ices_saithe_faroe_2025", database)
env <- new.env(parent = environment())
env$source <- source_data
diff --git a/analysis/comp_assessments/tests/test_ices_saithe_iceland_2026_source.R b/analysis/comp_assessments/tests/test_ices_saithe_iceland_2026_source.R
index a6dcf111..14fe9bb6 100644
--- a/analysis/comp_assessments/tests/test_ices_saithe_iceland_2026_source.R
+++ b/analysis/comp_assessments/tests/test_ices_saithe_iceland_2026_source.R
@@ -4,7 +4,7 @@ root <- "analysis/comp_assessments"
source(file.path(root, "R", "read_database.R"))
assessment_id <- "ices_saithe_iceland_2026"
-database <- read_database(tier = "preliminary")
+database <- read_database()
source_data <- read_assessment(assessment_id, database)
inputs <- source_data$inputs
outputs <- source_data$outputs
@@ -76,13 +76,7 @@ stopifnot(
!any(outputs$measure == "Fbar" & outputs$year == 2026L)
)
-review <- paste(readLines(file.path(saio_path(), "source_reviews", "ices_saithe_iceland_2026.md"), warn = FALSE), collapse = " ")
-stopifnot(
- grepl("28 blank age-year cells", review, fixed = TRUE),
- grepl("Table 2 `F` column's age range and calculation are not defined", review, fixed = TRUE),
- grepl("Through 2025, `HR` agrees to rounding", review, fixed = TRUE),
- grepl("exact 2026 production settings are not fully published", review, fixed = TRUE),
- grepl("0.20 as a month-midpoint approximation", review, fixed = TRUE)
-)
+# Source-review text is validated in SAIO; this test checks the cached source records.
+
cat("Icelandic saithe 2026 source records passed.\n")
\ No newline at end of file
diff --git a/analysis/comp_assessments/tests/test_ices_saithe_iceland_2026_translation.R b/analysis/comp_assessments/tests/test_ices_saithe_iceland_2026_translation.R
index cb3dbc0a..ede170f4 100644
--- a/analysis/comp_assessments/tests/test_ices_saithe_iceland_2026_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_saithe_iceland_2026_translation.R
@@ -5,7 +5,7 @@ source(file.path(root, "R", "read_database.R"))
source(file.path(root, "R", "database_to_tam_obs.R"))
assessment_id <- "ices_saithe_iceland_2026"
-database <- read_database(tier = "preliminary")
+database <- read_database()
source_data <- read_assessment(assessment_id, database)
original_inputs <- source_data$inputs
translated <- .test_stock(source_data)
diff --git a/analysis/comp_assessments/tests/test_ices_saithe_north_sea_translation.R b/analysis/comp_assessments/tests/test_ices_saithe_north_sea_translation.R
index 6900cdba..26b35ebe 100644
--- a/analysis/comp_assessments/tests/test_ices_saithe_north_sea_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_saithe_north_sea_translation.R
@@ -40,7 +40,7 @@ comparison_f_10plus <- translated$comparison_outputs[
]
stopifnot(
- tinyAM::check_obs(translated$obs),
+ tinyAM::check_tam_data(translated$obs),
identical(translated$years, 1967:2025),
identical(translated$ages, 3:8),
identical(translated$age_plus_group, 8),
diff --git a/analysis/comp_assessments/tests/test_ices_saithe_northeast_arctic_translation.R b/analysis/comp_assessments/tests/test_ices_saithe_northeast_arctic_translation.R
index 96c969a7..6d863ed4 100644
--- a/analysis/comp_assessments/tests/test_ices_saithe_northeast_arctic_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_saithe_northeast_arctic_translation.R
@@ -6,7 +6,7 @@ source(file.path(root, "R", "database_to_tam_obs.R"))
source(file.path(root, "R", "database_to_tam_ref.R"))
assessment_id <- "ices_saithe_northeast_arctic_2026"
-database <- read_database(tier = "preliminary")
+database <- read_database()
source_data <- read_assessment(assessment_id, database)
translated <- .test_stock(source_data)
dat <- do.call(tinyAM::prepare_tam, c(
@@ -40,7 +40,7 @@ ref <- database_to_tam_ref(
)
stopifnot(
- tinyAM::check_obs(translated$obs),
+ tinyAM::check_tam_data(translated$obs),
identical(translated$years, 1960:2025),
identical(translated$ages, 3:12),
identical(translated$age_plus_group, 12L),
diff --git a/analysis/comp_assessments/tests/test_ices_sole_20_24_2025_translation.R b/analysis/comp_assessments/tests/test_ices_sole_20_24_2025_translation.R
index 8dd3d24a..b52135c1 100644
--- a/analysis/comp_assessments/tests/test_ices_sole_20_24_2025_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_sole_20_24_2025_translation.R
@@ -4,14 +4,14 @@ source(file.path(root, "R", "run_assessment.R"))
source(file.path(root, "R", "read_database.R"))
source(file.path(root, "R", "database_to_tam_obs.R"))
-database <- read_database(tier = "preliminary")
+database <- read_database()
source_data <- read_assessment("ices_sole_20_24_2025", database)
translated <- .test_stock(source_data)
obs <- translated$obs
dat <- translated$dat
survey_names <- c("Q4Solesurvey1-9+", "Gillnet", "Trawl")
-tinyAM::check_obs(obs)
+tinyAM::check_tam_data(obs)
stopifnot(
identical(translated$years, 1984:2024),
diff --git a/analysis/comp_assessments/tests/test_ices_sole_celtic_7fg_2026_source.R b/analysis/comp_assessments/tests/test_ices_sole_celtic_7fg_2026_source.R
index 83cc2122..c7601fa3 100644
--- a/analysis/comp_assessments/tests/test_ices_sole_celtic_7fg_2026_source.R
+++ b/analysis/comp_assessments/tests/test_ices_sole_celtic_7fg_2026_source.R
@@ -4,7 +4,7 @@ source("analysis/comp_assessments/R/read_database.R")
source("analysis/comp_assessments/R/database_to_tam_obs.R")
assessment_id <- "ices_sole_celtic_7fg_2026"
-database <- read_database(tier = "preliminary")
+database <- read_database()
assessment <- read_assessment(assessment_id, database)
inputs <- assessment$inputs
outputs <- assessment$outputs
diff --git a/analysis/comp_assessments/tests/test_ices_sole_eastern_channel_7d_2026_translation.R b/analysis/comp_assessments/tests/test_ices_sole_eastern_channel_7d_2026_translation.R
index 59946257..fd929e06 100644
--- a/analysis/comp_assessments/tests/test_ices_sole_eastern_channel_7d_2026_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_sole_eastern_channel_7d_2026_translation.R
@@ -2,7 +2,7 @@ pkgload::load_all(".", quiet = TRUE)
source("analysis/comp_assessments/R/run_assessment.R")
assessment_id <- "ices_sole_eastern_channel_7d_2026"
-database <- read_database(tier = "preliminary")
+database <- read_database()
source_record <- read_assessment(assessment_id, database)
original_inputs <- source_record$inputs
original_outputs <- source_record$outputs
@@ -12,7 +12,7 @@ prepared <- stock$env
stopifnot(
is.null(stock$error),
is.null(prepared$fit),
- isTRUE(tinyAM::check_obs(prepared$obs)),
+ isTRUE(tinyAM::check_tam_data(prepared$obs)),
identical(prepared$years, 1982:2025),
identical(prepared$ages, 1:11),
identical(prepared$age_plus_group, 11L),
diff --git a/analysis/comp_assessments/tests/test_ices_sole_north_sea_translation.R b/analysis/comp_assessments/tests/test_ices_sole_north_sea_translation.R
index 22fbb430..134d6cc2 100644
--- a/analysis/comp_assessments/tests/test_ices_sole_north_sea_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_sole_north_sea_translation.R
@@ -5,14 +5,14 @@ source(file.path(root, "R", "read_database.R"))
source(file.path(root, "R", "database_to_tam_obs.R"))
assessment_id <- "ices_sole_north_sea_2025"
-database <- read_database(tier = "preliminary")
+database <- read_database()
source_data <- read_assessment(assessment_id, database)
source_inputs <- source_data$inputs
translated <- .test_stock(source_data)
obs <- translated$obs
dat <- translated$dat
-tinyAM::check_obs(obs)
+tinyAM::check_tam_data(obs)
source_catch <- source_inputs[
source_inputs$type == "catch" &
@@ -41,13 +41,10 @@ expected_m <- c(.754, .465, .368, .321, .295, .278, .267, .260, .254, .248)
actual_m <- vapply(1:10, function(age) {
unique(obs$pop_covariates$M_assumption[obs$pop_covariates$age == age])
}, numeric(1))
-readiness <- read.csv(
- file.path(.assessment_results("curated"), "observation_readiness.csv"),
- stringsAsFactors = FALSE, check.names = FALSE
-)
-sole_readiness <- readiness[
- readiness$assessment_id == assessment_id, , drop = FALSE
-]
+grid <- expand.grid(year = translated$years, age = translated$ages)
+key <- function(x) paste(x$year, x$age)
+catch_weights <- source_inputs[source_inputs$type == "catch_weight" &
+ source_inputs$year %in% translated$years & source_inputs$age %in% translated$ages, ]
stopifnot(
identical(translated$years, 1957:2024),
@@ -78,12 +75,11 @@ stopifnot(
is.null(translated$start_par),
!"relative_biomass_index" %in% obs$index$measure,
identical(source_data$inputs, source_inputs),
- nrow(sole_readiness) == 1L,
- isTRUE(sole_readiness$weight_full_year_age_grid),
- isTRUE(sole_readiness$catch_weight_full_year_age_grid),
- isTRUE(sole_readiness$maturity_full_year_age_grid),
- isTRUE(sole_readiness$M_numerical_values_available),
- !isTRUE(sole_readiness$M_estimated_in_source)
+ setequal(key(obs$weight), key(grid)),
+ setequal(key(obs$maturity), key(grid)),
+ setequal(key(catch_weights), key(grid)),
+ all(is.finite(obs$weight$obs)),
+ all(is.finite(obs$pop_covariates$M_assumption))
)
cat("North Sea sole translation structure passed.\n")
diff --git a/analysis/comp_assessments/tests/test_ices_sole_western_channel_7e_2026_translation.R b/analysis/comp_assessments/tests/test_ices_sole_western_channel_7e_2026_translation.R
index 34209038..7930a31a 100644
--- a/analysis/comp_assessments/tests/test_ices_sole_western_channel_7e_2026_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_sole_western_channel_7e_2026_translation.R
@@ -1,6 +1,6 @@
source("analysis/comp_assessments/tests/helper_stock.R")
source("analysis/comp_assessments/R/run_assessment.R")
-database <- read_database(tier = "preliminary")
+database <- read_database()
assessment_id <- "ices_sole_western_channel_7e_2026"
assessment <- read_assessment(assessment_id, database)
trans <- .test_stock(assessment)
diff --git a/analysis/comp_assessments/tests/test_ices_sprat_baltic_translation.R b/analysis/comp_assessments/tests/test_ices_sprat_baltic_translation.R
index 1beeac88..460d587c 100644
--- a/analysis/comp_assessments/tests/test_ices_sprat_baltic_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_sprat_baltic_translation.R
@@ -29,7 +29,7 @@ expected_total_numbers <- tapply(
)
stopifnot(
- tinyAM::check_obs(translated$obs),
+ tinyAM::check_tam_data(translated$obs),
all(translated$obs$maturity$spawn_time == .4),
identical(translated$years, 1974:2025),
identical(translated$ages, 1:8),
diff --git a/analysis/comp_assessments/tests/test_ices_turbot_north_sea_2025_benchmark_translation.R b/analysis/comp_assessments/tests/test_ices_turbot_north_sea_2025_benchmark_translation.R
index 1d362758..d7f8b118 100644
--- a/analysis/comp_assessments/tests/test_ices_turbot_north_sea_2025_benchmark_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_turbot_north_sea_2025_benchmark_translation.R
@@ -5,13 +5,13 @@ source(file.path(root, "R", "read_database.R"))
source(file.path(root, "R", "database_to_tam_obs.R"))
assessment_id <- "ices_turbot_north_sea_2025_benchmark"
-source_data <- read_assessment(assessment_id, read_database(tier = "preliminary"))
+source_data <- read_assessment(assessment_id, read_database())
translated <- .test_stock(source_data)
obs <- translated$obs
dat <- translated$dat
inputs <- source_data$inputs
-tinyAM::check_obs(obs)
+tinyAM::check_tam_data(obs)
catch <- inputs[
inputs$type == "catch" & inputs$measure == "numbers_at_age",
@@ -64,7 +64,7 @@ stopifnot(
length(unique(obs$index$q_key)) == 13L,
length(unique(obs$catch$sd_group)) == 3L,
length(unique(obs$index$sd_group)) == 5L,
- length(unique(obs$pop_covariates$F_sd_group)) == 4L,
+ length(unique(obs$catch$F_sd_group)) == 4L,
all(obs$index$q_key[obs$index$survey == "BTS+COAST" & obs$index$age %in% 6:7] ==
obs$index$q_key[obs$index$survey == "BTS+COAST" & obs$index$age == 6][1]),
all(obs$index$q_key[obs$index$survey == "BSAS" & obs$index$age %in% 7:8] ==
diff --git a/analysis/comp_assessments/tests/test_ices_whiting_irish_sea_2025_source.R b/analysis/comp_assessments/tests/test_ices_whiting_irish_sea_2025_source.R
index 941f6b05..4d584918 100644
--- a/analysis/comp_assessments/tests/test_ices_whiting_irish_sea_2025_source.R
+++ b/analysis/comp_assessments/tests/test_ices_whiting_irish_sea_2025_source.R
@@ -1,7 +1,7 @@
source("analysis/comp_assessments/tests/helper_stock.R")
source("analysis/comp_assessments/R/read_database.R")
-database <- read_database(tier = "preliminary")
+database <- read_database()
benchmark_id <- "ices_whiting_irish_sea_2025_benchmark"
summary_id <- "ices_whiting_irish_sea_2025_sag_summary"
benchmark <- read_assessment(benchmark_id, database)
diff --git a/analysis/comp_assessments/tests/test_ices_whiting_north_sea_translation.R b/analysis/comp_assessments/tests/test_ices_whiting_north_sea_translation.R
index 809ebe03..b8bb5217 100644
--- a/analysis/comp_assessments/tests/test_ices_whiting_north_sea_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_whiting_north_sea_translation.R
@@ -24,7 +24,7 @@ m_surface <- translated$obs$pop_covariates
m_2022 <- m_surface[m_surface$year == 2022, c("age", "M_assumption")]
stopifnot(
- tinyAM::check_obs(translated$obs),
+ tinyAM::check_tam_data(translated$obs),
identical(translated$years, 1978:2026),
identical(translated$ages, 0:8),
nrow(translated$obs$catch) == 49L * 9L,
diff --git a/analysis/comp_assessments/tests/test_ices_whiting_west_scotland_source.R b/analysis/comp_assessments/tests/test_ices_whiting_west_scotland_source.R
index 97c7d0e1..14ed656d 100644
--- a/analysis/comp_assessments/tests/test_ices_whiting_west_scotland_source.R
+++ b/analysis/comp_assessments/tests/test_ices_whiting_west_scotland_source.R
@@ -1,7 +1,7 @@
source("analysis/comp_assessments/tests/helper_stock.R")
source("analysis/comp_assessments/R/read_database.R")
-database <- read_database(tier = "preliminary")
+database <- read_database()
assessment_id <- "ices_whiting_west_scotland_2025"
assessment <- database$assessments[
database$assessments$assessment_id == assessment_id, , drop = FALSE
diff --git a/analysis/comp_assessments/tests/test_ices_whiting_west_scotland_translation.R b/analysis/comp_assessments/tests/test_ices_whiting_west_scotland_translation.R
index 82d2634d..8e3da617 100644
--- a/analysis/comp_assessments/tests/test_ices_whiting_west_scotland_translation.R
+++ b/analysis/comp_assessments/tests/test_ices_whiting_west_scotland_translation.R
@@ -3,7 +3,7 @@ source("analysis/comp_assessments/R/run_assessment.R")
source("analysis/comp_assessments/R/database_to_tam_obs.R")
source("analysis/comp_assessments/R/read_database.R")
-database <- read_database(tier = "preliminary")
+database <- read_database()
assessment_id <- "ices_whiting_west_scotland_2025"
source_data <- read_assessment(assessment_id, database)
translated <- .test_stock(source_data)
@@ -21,7 +21,7 @@ matched <- match(key(index), weight_key)
has_weight <- !is.na(matched)
stopifnot(
- tinyAM::check_obs(obs),
+ tinyAM::check_tam_data(obs),
identical(translated$years, 1981:2025),
identical(translated$ages, 0:7),
identical(translated$age_plus_group, 7L),
diff --git a/analysis/comp_assessments/tests/test_irish_sea_cod_translation.R b/analysis/comp_assessments/tests/test_irish_sea_cod_translation.R
index bd1d6632..d1b07644 100644
--- a/analysis/comp_assessments/tests/test_irish_sea_cod_translation.R
+++ b/analysis/comp_assessments/tests/test_irish_sea_cod_translation.R
@@ -2,7 +2,7 @@ source("analysis/comp_assessments/tests/helper_stock.R")
pkgload::load_all(".", quiet = TRUE)
source("analysis/comp_assessments/R/run_assessment.R")
-database <- read_database(tier = "preliminary")
+database <- read_database()
source_data <- read_assessment("ices_cod_irish_sea_2026", database)
translated <- .test_stock(source_data)
obs <- translated$obs
@@ -12,7 +12,7 @@ index <- obs$index
excluded <- catch$year %in% 2003:2005
stopifnot(
- tinyAM::check_obs(obs),
+ tinyAM::check_tam_data(obs),
identical(translated$years, 1968:2018),
identical(translated$ages, 0:6),
nrow(catch) == 51L * 7L,
diff --git a/analysis/comp_assessments/tests/test_jrn_afwg_haddock_2026_translation.R b/analysis/comp_assessments/tests/test_jrn_afwg_haddock_2026_translation.R
index 4078e75a..f6076dd6 100644
--- a/analysis/comp_assessments/tests/test_jrn_afwg_haddock_2026_translation.R
+++ b/analysis/comp_assessments/tests/test_jrn_afwg_haddock_2026_translation.R
@@ -1,10 +1,10 @@
pkgload::load_all(".", quiet = TRUE, helpers = FALSE)
root <- file.path("analysis", "comp_assessments")
-source(file.path(root, "R", "read_database.R"))
+source(file.path(root, "R", "run_assessment.R"))
source(file.path(root, "R", "database_to_tam_obs.R"))
assessment_id <- "jrn_afwg_haddock_northeast_arctic_2026"
-source_data <- read_assessment(assessment_id, read_database(tier = "preliminary"))
+source_data <- read_assessment(assessment_id, read_database())
env <- new.env(parent = globalenv())
env$source <- source_data
env$do_fit <- FALSE
@@ -53,7 +53,7 @@ stopifnot(
all(is.finite(env$obs$pop_covariates$M_assumption)),
all(env$obs$pop_covariates$M_assumption[env$obs$pop_covariates$age >= 7] == 0.2),
identical(levels(env$obs$catch$catch_sd_group), c("age3", "age4plus")),
- identical(levels(env$obs$pop_covariates$F_sd_group), c("age3", "age4plus")),
+ identical(levels(env$obs$catch$F_sd_group), c("age3", "age4plus")),
ncol(prepared$process_sd$F$matrix) == 2L,
identical(prepared$F_settings$mean_ages, 4:7),
env$age_plus_group == 13L,
diff --git a/analysis/comp_assessments/tests/test_jrn_beaked_redfish_2026_source.R b/analysis/comp_assessments/tests/test_jrn_beaked_redfish_2026_source.R
index 69ebe8e1..77b1d0e8 100644
--- a/analysis/comp_assessments/tests/test_jrn_beaked_redfish_2026_source.R
+++ b/analysis/comp_assessments/tests/test_jrn_beaked_redfish_2026_source.R
@@ -1,6 +1,6 @@
pkgload::load_all(".", quiet = TRUE)
source("analysis/comp_assessments/R/run_assessment.R")
-record <- read_assessment("jrn_redfish_beaked_2026", read_database(tier = "preliminary"))
+record <- read_assessment("jrn_redfish_beaked_2026", read_database())
inputs <- record$inputs
outputs <- record$outputs
stopifnot(
@@ -41,12 +41,12 @@ catch_weight <- inputs[inputs$type == "catch_weight", ]
maturity <- inputs[inputs$type == "maturity", ]
M <- inputs[inputs$type == "M", ]
stopifnot(
- identical(weight$age, 6:19), all(is.na(weight$year)),
+ isTRUE(all.equal(weight$age, 6:19, tolerance = 0)), all(is.na(weight$year)),
identical(weight$value, c(.141, .188, .237, .286, .334, .381, .424, .465, .503, .537, .569, .597, .623, .755)),
identical(catch_weight$value, weight$value),
- identical(maturity$age, 6:19), all(is.na(maturity$year)),
+ isTRUE(all.equal(maturity$age, 6:19, tolerance = 0)), all(is.na(maturity$year)),
identical(maturity$value, c(.02, .04, .08, .14, .24, .37, .49, .58, .66, .73, .77, .81, .85, 1)),
- identical(M$age, 2:19), all(M$value == .05), all(is.na(M$year))
+ isTRUE(all.equal(M$age, 2:19, tolerance = 0)), all(M$value == .05), all(is.na(M$year))
)
russian <- select_input("numbers_at_age", survey = "Russian autumn groundfish")
eco <- select_input("numbers_at_age", survey = "Ecosystem survey")
@@ -114,6 +114,6 @@ prepared <- run_assessment(record$assessment$assessment_id, fit = FALSE)
stopifnot(
prepared$diagnostics$status == "not_fitted",
is.null(prepared$fit),
- isTRUE(tinyAM::check_obs(prepared$obs))
+ isTRUE(tinyAM::check_tam_data(prepared$obs))
)
cat("Beaked redfish source coverage, fixed biology, missing cells, year basis, units and rounded F passed.\n")
diff --git a/analysis/comp_assessments/tests/test_jrn_beaked_redfish_2026_translation.R b/analysis/comp_assessments/tests/test_jrn_beaked_redfish_2026_translation.R
index 86c9ea03..d49f5a2e 100644
--- a/analysis/comp_assessments/tests/test_jrn_beaked_redfish_2026_translation.R
+++ b/analysis/comp_assessments/tests/test_jrn_beaked_redfish_2026_translation.R
@@ -2,7 +2,7 @@ pkgload::load_all(".", quiet = TRUE)
source("analysis/comp_assessments/R/run_assessment.R")
assessment_id <- "jrn_redfish_beaked_2026"
-database <- read_database(tier = "preliminary")
+database <- read_database()
record <- read_assessment(assessment_id, database)
original_inputs <- record$inputs
original_outputs <- record$outputs
@@ -22,7 +22,7 @@ stopifnot(
deparse(~ 0 + survey + survey:age)),
identical(deparse(prepared$index_settings$sd_form),
deparse(~ 0 + survey)),
- isTRUE(tinyAM::check_obs(obs)),
+ isTRUE(tinyAM::check_tam_data(obs)),
nrow(obs$catch) == length(years) * length(ages),
identical(as.integer(sort(unique(obs$catch$year))), years),
identical(as.integer(sort(unique(obs$catch$age))), ages),
diff --git a/analysis/comp_assessments/tests/test_nefsc_atlantic_mackerel_translation.R b/analysis/comp_assessments/tests/test_nefsc_atlantic_mackerel_translation.R
index c72dbabb..427eee3f 100644
--- a/analysis/comp_assessments/tests/test_nefsc_atlantic_mackerel_translation.R
+++ b/analysis/comp_assessments/tests/test_nefsc_atlantic_mackerel_translation.R
@@ -36,7 +36,7 @@ expected_recruitment <- outputs$value[
]
stopifnot(
- tinyAM::check_obs(translated$obs),
+ tinyAM::check_tam_data(translated$obs),
identical(translated$years, 1968:2016),
identical(translated$ages, 1:10),
translated$age_plus_group == 10L,
@@ -77,7 +77,8 @@ stopifnot(
outputs$value[outputs$measure == "Fbar" & outputs$year == 2016] == 0.468,
outputs$age[outputs$measure == "recruitment" & outputs$year == 2016] == 1,
egg_ssb$value[egg_ssb$year == 1979] == 1131094,
- nrow(summary_outputs) == 30L,
+ sum(summary_outputs$measure %in% c("SSB", "Fbar", "recruitment") &
+ summary_outputs$year %in% 2015:2024) == 30L,
summary_outputs$value[summary_outputs$measure == "SSB" & summary_outputs$year == 2024] == 94702,
summary_outputs$lwr[summary_outputs$measure == "SSB" & summary_outputs$year == 2024] == 52539,
summary_outputs$upr[summary_outputs$measure == "SSB" & summary_outputs$year == 2024] == 170702,
diff --git a/analysis/comp_assessments/tests/test_nefsc_atlantic_pollock_translation.R b/analysis/comp_assessments/tests/test_nefsc_atlantic_pollock_translation.R
index ce654b75..7a7c03c6 100644
--- a/analysis/comp_assessments/tests/test_nefsc_atlantic_pollock_translation.R
+++ b/analysis/comp_assessments/tests/test_nefsc_atlantic_pollock_translation.R
@@ -1,7 +1,7 @@
source("analysis/comp_assessments/tests/helper_stock.R")
source("analysis/comp_assessments/R/run_assessment.R")
source("analysis/comp_assessments/R/read_database.R")
-database <- read_database(tier = "preliminary")
+database <- read_database()
source_data <- read_assessment("nefsc_pollock_5yz_2024", database)
translated <- .test_stock(source_data)
@@ -30,7 +30,7 @@ expected_index <- aggregate_index$value *
survey_weights$value[survey_weights$age == 1]
stopifnot(
- tinyAM::check_obs(obs),
+ tinyAM::check_tam_data(obs),
identical(years, 1970:2023),
identical(ages, 1:9),
translated$age_plus_group == 9L,
diff --git a/analysis/comp_assessments/tests/test_nefsc_cod_georges_bank_2024_translation.R b/analysis/comp_assessments/tests/test_nefsc_cod_georges_bank_2024_translation.R
index a038137e..9c799da5 100644
--- a/analysis/comp_assessments/tests/test_nefsc_cod_georges_bank_2024_translation.R
+++ b/analysis/comp_assessments/tests/test_nefsc_cod_georges_bank_2024_translation.R
@@ -1,5 +1,5 @@
source("analysis/comp_assessments/R/run_assessment.R")
-database <- read_database(tier = "preliminary")
+database <- read_database()
record <- read_assessment("nefsc_cod_georges_bank_2024", database)
original <- record
for (fit in c(FALSE, TRUE)) {
@@ -12,7 +12,7 @@ for (fit in c(FALSE, TRUE)) {
is.null(stock$env$retry_start), is.null(stock$env$start_par),
identical(record, original))
result <- run_assessment("nefsc_cod_georges_bank_2024", database,
- fit = fit, dashboard = TRUE, cache = FALSE)
+ fit = fit, cache = FALSE)
stopifnot(result$diagnostics$status == "translation_failed",
is.null(result$fit), is.null(result$obs), is.null(result$ref),
is.null(result$summary), is.null(result$differences),
diff --git a/analysis/comp_assessments/tests/test_nefsc_gom_cod_2021_translation.R b/analysis/comp_assessments/tests/test_nefsc_gom_cod_2021_translation.R
index 95a5114e..a186663e 100644
--- a/analysis/comp_assessments/tests/test_nefsc_gom_cod_2021_translation.R
+++ b/analysis/comp_assessments/tests/test_nefsc_gom_cod_2021_translation.R
@@ -10,7 +10,7 @@ stopifnot(
is.null(stock$env$fit),
identical(as.integer(stock$env$years), 1982:2019),
identical(as.integer(stock$env$ages), 1:9),
- tinyAM::check_obs(result$obs),
+ tinyAM::check_tam_data(result$obs),
nrow(result$obs$catch) == 38L * 9L,
nrow(result$obs$weight) == 38L * 9L,
nrow(result$obs$maturity) == 38L * 9L,
diff --git a/analysis/comp_assessments/tests/test_nefsc_scup_translation.R b/analysis/comp_assessments/tests/test_nefsc_scup_translation.R
index c3965f8f..24e1dcf6 100644
--- a/analysis/comp_assessments/tests/test_nefsc_scup_translation.R
+++ b/analysis/comp_assessments/tests/test_nefsc_scup_translation.R
@@ -45,7 +45,7 @@ ssb_check <- ssb$value[!is.na(ssb$year) & ssb$year == 2014 & ssb_age == 3]
stopifnot(
identical(x$diagnostics$status, "not_fitted"),
is.null(x$fit),
- isTRUE(tinyAM::check_obs(x$obs)),
+ isTRUE(tinyAM::check_tam_data(x$obs)),
nrow(biomass) == 31L * 8L,
nrow(ssb) == 31L * 8L,
identical(sort(unique(as.integer(biomass$year))), 1984:2014),
diff --git a/analysis/comp_assessments/tests/test_nefsc_snema_yellowtail_source.R b/analysis/comp_assessments/tests/test_nefsc_snema_yellowtail_source.R
index 28ff3042..5ef1ec4d 100644
--- a/analysis/comp_assessments/tests/test_nefsc_snema_yellowtail_source.R
+++ b/analysis/comp_assessments/tests/test_nefsc_snema_yellowtail_source.R
@@ -20,7 +20,7 @@ x <- framework$inputs
catch <- x[x$type == "catch", ]
stopifnot(nrow(catch) == 300, all(catch$unit == "thousand fish"),
identical(sort(unique(catch$year)), 1973:2022),
- identical(sort(unique(catch$age)), 1:6),
+ isTRUE(all.equal(sort(unique(catch$age)), 1:6, tolerance = 0)),
sum(is.na(catch$value)) == 18,
all(is.na(catch$value[catch$year >= 2020])),
catch$value[catch$year == 1975 & catch$age == 1] == 8037,
@@ -35,16 +35,16 @@ spring <- indices[indices$season == "spring", ]
fall <- indices[indices$season == "fall", ]
winter <- indices[indices$season == "winter", ]
stopifnot(identical(sort(unique(spring$year)), setdiff(1968:2022, 2020L)),
- identical(sort(unique(spring$age)), 0:11),
+ isTRUE(all.equal(sort(unique(spring$age)), 0:11, tolerance = 0)),
spring$value[spring$year == 1968 & spring$age == 3] == 47.65,
spring$value[spring$year == 2004 & spring$age == 7] == .019,
identical(sort(unique(fall$year)), setdiff(1963:2022, c(2017L, 2020L))),
- identical(sort(unique(fall$age)), 1:10),
+ isTRUE(all.equal(sort(unique(fall$age)), 1:10, tolerance = 0)),
fall$value[fall$year == 1984 & fall$age == 4] == 1.865,
fall$value[fall$year == 1997 & fall$age == 4] == 1.459,
all(grepl("incomplete age-length key", fall$notes[fall$year == 2013 & fall$age %in% c(2:7, 9)])),
identical(sort(unique(winter$year)), 1992:2007),
- identical(sort(unique(winter$age)), 1:8),
+ isTRUE(all.equal(sort(unique(winter$age)), 1:8, tolerance = 0)),
winter$value[winter$year == 2006 & winter$age == 2] == 18.736,
winter$value[winter$year == 1996 & winter$age == 8] == .015,
any(indices$value == 0), !anyNA(indices$value))
@@ -69,7 +69,7 @@ stopifnot(nrow(cv) == 240, all(is.na(cv$age)), all(cv$basis == "relative_scale")
!any(x$type %in% c("weight", "maturity")),
all(is.na(x$year[x$type == "M"])),
all(is.na(x$year_basis[x$type == "M"])),
- identical(x$age[x$type == "M"], 1:6),
+ isTRUE(all.equal(x$age[x$type == "M"], 1:6, tolerance = 0)),
identical(x$value[x$type == "M"], c(.97, .45, .37, .34, .33, .33)),
all(x$unit[x$type == "M"] == "per year"),
all(grepl("ages 6 and 7", x$notes[x$type == "M"], fixed = TRUE)),
@@ -126,24 +126,5 @@ for (id in c(framework$assessment$assessment_id, update$assessment$assessment_id
blocked <- run_assessment(id, fit = FALSE)
stopifnot(blocked$diagnostics$status == "blocked_no_stock_spec", is.null(blocked$fit))
}
-validate_in_memory <- function(tables) {
- env <- new.env(parent = globalenv())
- env$read.csv <- function(file, ...) tables[[sub(".csv", "", basename(file), fixed = TRUE)]]
- tryCatch({
- invisible(capture.output(sys.source(file.path(saio_path(), "scripts", "database", "002_validate_database.R"), envir = env)))
- NULL
- }, error = identity)
-}
-at <- which(db$inputs$assessment_id == framework$assessment$assessment_id &
- db$inputs$measure == "biomass_index_cv" & db$inputs$season == "spring" & db$inputs$year == 2022)
-stopifnot(length(at) == 1L)
-wrong <- db
-wrong$inputs$age[at] <- 2L
-error <- validate_in_memory(wrong)
-stopifnot(inherits(error, "error"), grepl("without an age", conditionMessage(error), fixed = TRUE))
-wrong <- db
-wrong$inputs <- wrong$inputs[!(wrong$inputs$assessment_id == framework$assessment$assessment_id &
- wrong$inputs$measure == "total_biomass" & wrong$inputs$season == "spring" & wrong$inputs$year == 2022), ]
-error <- validate_in_memory(wrong)
-stopifnot(inherits(error, "error"), grepl("corresponding total_biomass", conditionMessage(error), fixed = TRUE))
+# Canonical schema rejection tests run in SAIO, which owns its validator.
cat("SNEMA yellowtail source tables, selected-run separation, units, missing values and unresolved translation gaps passed.\n")
diff --git a/analysis/comp_assessments/tests/test_nefsc_summer_flounder_translation.R b/analysis/comp_assessments/tests/test_nefsc_summer_flounder_translation.R
index ecc7041a..f0ddb1dd 100644
--- a/analysis/comp_assessments/tests/test_nefsc_summer_flounder_translation.R
+++ b/analysis/comp_assessments/tests/test_nefsc_summer_flounder_translation.R
@@ -27,7 +27,7 @@ expected_catch <- published_catch$value[match(
)]
stopifnot(
- tinyAM::check_obs(translated$obs),
+ tinyAM::check_tam_data(translated$obs),
identical(translated$years, 1982:2016),
identical(translated$ages, 0:7),
nrow(translated$obs$catch) == 35L * 8L,
diff --git a/analysis/comp_assessments/tests/test_nefsc_white_hake_2025_translation.R b/analysis/comp_assessments/tests/test_nefsc_white_hake_2025_translation.R
index e798c0c1..89715811 100644
--- a/analysis/comp_assessments/tests/test_nefsc_white_hake_2025_translation.R
+++ b/analysis/comp_assessments/tests/test_nefsc_white_hake_2025_translation.R
@@ -1,7 +1,7 @@
pkgload::load_all(".", quiet = TRUE)
source("analysis/comp_assessments/R/run_assessment.R")
-database <- read_database(tier = "preliminary")
+database <- read_database()
source_before <- read_assessment("nefsc_white_hake_2025", database)
result <- run_assessment("nefsc_white_hake_2025", database, fit = FALSE, silent = TRUE)
obs <- result$obs
@@ -60,7 +60,7 @@ stopifnot(
stopifnot(
result$diagnostics$status == "not_fitted",
inherits(obs, "list"),
- tinyAM::check_obs(obs),
+ tinyAM::check_tam_data(obs),
isTRUE(all.equal(as.numeric(sort(unique(obs$weight$year))), 1963:2024)),
isTRUE(all.equal(as.numeric(sort(unique(obs$weight$age))), 1:9)),
nrow(obs$weight) == 62L * 9L,
diff --git a/analysis/comp_assessments/tests/test_nefsc_winter_flounder_translation.R b/analysis/comp_assessments/tests/test_nefsc_winter_flounder_translation.R
index a25f532b..648db69b 100644
--- a/analysis/comp_assessments/tests/test_nefsc_winter_flounder_translation.R
+++ b/analysis/comp_assessments/tests/test_nefsc_winter_flounder_translation.R
@@ -2,14 +2,14 @@ source("analysis/comp_assessments/tests/helper_stock.R")
root <- file.path("analysis", "comp_assessments")
source(file.path(root, "R", "run_assessment.R"))
-database <- read_database(tier = "preliminary")
+database <- read_database()
source_data <- read_assessment("nefsc_winter_flounder_gb_2022_vpa", database)
original_inputs <- source_data$inputs
translated <- .test_stock(source_data)
obs <- translated$obs
index <- obs$index
-tinyAM::check_obs(obs)
+tinyAM::check_tam_data(obs)
stopifnot(
identical(translated$years, 1982:2021),
diff --git a/analysis/comp_assessments/tests/test_pacific_M_priors.R b/analysis/comp_assessments/tests/test_pacific_M_priors.R
index ec776e5e..299338ae 100644
--- a/analysis/comp_assessments/tests/test_pacific_M_priors.R
+++ b/analysis/comp_assessments/tests/test_pacific_M_priors.R
@@ -5,16 +5,6 @@ for (id in "joint_hake_coastal_2025") {
record <- read_assessment(id, database)
original <- record
spec <- .test_stock(record)
- baseline_script <- tempfile(fileext = ".R")
- status <- system2("git", c("show", shQuote(paste0(
- "86e44bf:analysis/comp_assessments/scripts/translation/stocks/", id, ".R"
- ))), stdout = baseline_script)
- stopifnot(status == 0)
- baseline <- .test_stock(record, script = baseline_script)
- unlink(baseline_script)
- for (table in c("catch", "index", "weight", "maturity")) {
- stopifnot(identical(spec$obs[[table]], baseline$obs[[table]]))
- }
stopifnot(attr(spec$obs, "translation")$M$status != "fixed_numerical_input")
dat <- spec$dat
par <- tinyAM::make_par(dat)
@@ -31,7 +21,7 @@ for (id in "joint_hake_coastal_2025") {
}
center <- exp(unique(dat$log_mu_supplied_m))
stopifnot(abs(log(center) + 1.60944) < 1e-12, term$sd == .1,
- !"mu_m" %in% names(spec$retry_start()))
+ is.null(spec$retry_start))
}
cat("One shared M level receives one fixed-SD Gaussian penalty; source prior scales and input preservation passed.\n")
diff --git a/analysis/comp_assessments/tests/test_plot_stock_order.R b/analysis/comp_assessments/tests/test_plot_stock_order.R
index bb137e57..1c3792e6 100644
--- a/analysis/comp_assessments/tests/test_plot_stock_order.R
+++ b/analysis/comp_assessments/tests/test_plot_stock_order.R
@@ -1,9 +1,9 @@
source("analysis/comp_assessments/R/plot_stock_order.R")
source("analysis/comp_assessments/R/run_assessment.R")
root <- "analysis/comp_assessments"
-revision <- .saio_revision()
-assessments <- .saio_read_table("assessments.csv", revision = revision)
-stocks <- .saio_read_table("stocks.csv", revision = revision)
+database <- read_database()
+assessments <- database$assessments
+stocks <- database$stocks
ids <- intersect(assessment_translations("curated")$assessment_id, assessments$assessment_id)
info <- assessment_plot_stocks(assessments, stocks, ids)
stopifnot(nrow(info) == length(ids), !anyDuplicated(info$assessment_id),
diff --git a/analysis/comp_assessments/tests/test_run_assessment.R b/analysis/comp_assessments/tests/test_run_assessment.R
index fd9fabd6..88019bae 100644
--- a/analysis/comp_assessments/tests/test_run_assessment.R
+++ b/analysis/comp_assessments/tests/test_run_assessment.R
@@ -5,9 +5,9 @@ source(file.path(root, "R", "run_assessment.R"))
result <- run_assessment("dfo_cod_2j3kl_2025", fit = FALSE)
stopifnot(
result$source$assessment$assessment_id == "dfo_cod_2j3kl_2025",
- result$source$database_source == "saio_committed",
- result$source$database_selection == "latest_main",
- result$source$commit == .saio_revision(),
+ result$source$database_source == "saio_snapshot",
+ result$source$database_selection == "cached_snapshot",
+ result$source$commit == .assessment_snapshot(),
is.data.frame(result$obs$catch),
is.data.frame(result$obs$index),
is.data.frame(result$audit),
diff --git a/analysis/comp_assessments/tests/test_run_assessments.R b/analysis/comp_assessments/tests/test_run_assessments.R
index 413ee3cf..679da404 100644
--- a/analysis/comp_assessments/tests/test_run_assessments.R
+++ b/analysis/comp_assessments/tests/test_run_assessments.R
@@ -39,7 +39,9 @@ stopifnot(
"assessment_id" %in% names(attr(serial, "comparison_summary"))
)
-previous_plan <- future::plan("list")[[1L]]
+previous_plan <- future::plan()
+future::plan(future::multisession, workers = 2)
+configured_plan <- future::plan("list")[[1L]]
previous_workers <- future::nbrOfWorkers()
shared_outputs <- file.path(.assessment_results("curated"), c(
"fit_diagnostics.csv", "comparison_summary.csv"
@@ -48,14 +50,14 @@ shared_outputs_existed <- file.exists(shared_outputs)
cache_existed <- dir.exists(file.path(.assessment_results("curated"), "cache"))
parallel <- run_assessments(
assessment_ids,
- database = database, parallel = TRUE, workers = 2,
+ database = database,
fit = FALSE
)
stopifnot(
identical(names(parallel), assessment_ids),
identical(
attr(future::plan("list")[[1L]], "call"),
- attr(previous_plan, "call")
+ attr(configured_plan, "call")
),
future::nbrOfWorkers() == previous_workers,
identical(file.exists(shared_outputs), shared_outputs_existed),
@@ -72,7 +74,7 @@ bad_database$inputs <- bad_database$inputs[
]
failed <- run_assessments(
c("dfo_cod_2j3kl_2025", "ices_cod_north_sea_2025"),
- database = bad_database, parallel = TRUE, workers = 2, fit = FALSE
+ database = bad_database, fit = FALSE
)
goa <- run_assessments("afsc_cod_goa_2026",
database = database,
@@ -85,4 +87,5 @@ stopifnot(
!is.null(goa$afsc_cod_goa_2026$obs)
)
+future::plan(previous_plan)
cat("Multi-assessment runner tests passed.\n")
diff --git a/analysis/comp_assessments/tests/test_saio_migration.R b/analysis/comp_assessments/tests/test_saio_migration.R
index f983a838..4db513a6 100644
--- a/analysis/comp_assessments/tests/test_saio_migration.R
+++ b/analysis/comp_assessments/tests/test_saio_migration.R
@@ -1,20 +1,14 @@
-source("analysis/comp_assessments/R/read_database.R")
source("analysis/comp_assessments/R/run_assessment.R")
-
+source("analysis/comp_assessments/R/update_database_cache.R")
registry <- assessment_translations()
stopifnot(all(file.exists(registry$script)),
!anyDuplicated(paste(registry$assessment_id, registry$tier)),
- nrow(assessment_translations("curated")) == 35L,
- !anyNA(.saio_read_table("stocks.csv")$stock_id),
- identical(.database_head(saio_path(), "refs/heads/main"), .saio_revision()),
- identical(.saio_revision("curated"), .saio_revision("preliminary")))
+ nrow(assessment_translations("curated")) == 35L)
local({
original_wd <- getwd()
- fixture <- tempfile("saio_migration_")
+ fixture <- tempfile("saio_snapshot_")
dir.create(fixture)
- stopifnot(startsWith(normalizePath(fixture, winslash = "/"),
- normalizePath(tempdir(), winslash = "/")))
on.exit({setwd(original_wd); unlink(fixture, recursive = TRUE)}, add = TRUE)
relative_root <- file.path("analysis", "comp_assessments")
dir.create(file.path(fixture, relative_root, "R"), recursive = TRUE)
@@ -31,86 +25,60 @@ local({
stocks <- data.frame(stock_id = "source_stock", scientific_name = "Source species")
assessments <- data.frame(assessment_id = c("shared", "extra"),
stock_id = "source_stock", is_current = c(FALSE, TRUE))
- assumptions <- data.frame(assessment_id = assessments$assessment_id,
- component = "population", setting = "age_range", value = "1-2",
- source_reference = "fixture", notes = "fixture")
tables <- list(stocks = stocks, assessments = assessments,
- assumptions = assumptions,
+ assumptions = data.frame(assessment_id = assessments$assessment_id,
+ component = "population", setting = "age_range", value = "1-2",
+ source_reference = "fixture", notes = "fixture"),
inputs = data.frame(assessment_id = assessments$assessment_id),
outputs = data.frame(assessment_id = assessments$assessment_id))
for (name in names(tables)) write.csv(tables[[name]],
file.path("data_repo/database", paste0(name, ".csv")), row.names = FALSE)
git("init", "-b", "main")
git("add", "database")
- git("-c", "user.name=MigrationTest", "-c", "user.email=migration@example.invalid",
- "-c", "commit.gpgsign=false", "commit", "-m", "fixture")
+ commit <- function(text) git("-c", "user.name=SnapshotTest", "-c", "user.email=snapshot@example.invalid",
+ "-c", "commit.gpgsign=false", "commit", "-m", text)
+ commit("fixture")
revision <- git("rev-parse", "HEAD")[[1]]
- stocks$scientific_name <- "Uncommitted replacement"
- write.csv(stocks, "data_repo/database/stocks.csv", row.names = FALSE)
- source(file.path(relative_root, "R", "read_database.R"))
- original_options <- options(tinyAM.saio_path = normalizePath("data_repo", winslash = "/"))
- on.exit(options(original_options), add = TRUE)
- pinned <- read_committed_database(revision, repo = "data_repo")
- latest <- read_database()
- latest_preliminary <- read_database(tier = "preliminary")
- working <- read_database("data_repo/database")
+ update_database_cache("data_repo")
+ pinned <- read_database()
stopifnot(pinned$stocks$scientific_name == "Source species",
- latest$stocks$scientific_name == "Source species",
- identical(latest$commit, latest_preliminary$commit),
- latest$database_selection == "latest_main",
- latest$database_ref == "refs/heads/main",
- pinned$database_selection == "explicit_revision",
- working$stocks$scientific_name == "Uncommitted replacement",
- grepl("^working-[[:xdigit:]]{32}$", working$database_key),
- identical(read_database("data_repo/database")$database_key, working$database_key),
- identical(pinned$commit, revision))
- stocks$scientific_name <- "Another uncommitted value"
- write.csv(stocks, "data_repo/database/stocks.csv", row.names = FALSE)
- stopifnot(read_database("data_repo/database")$database_key != working$database_key,
- read_database()$stocks$scientific_name == "Source species")
- stocks$scientific_name <- "Uncommitted replacement"
- write.csv(stocks, "data_repo/database/stocks.csv", row.names = FALSE)
- git("switch", "-c", "experiment")
- git("add", "database")
- git("-c", "user.name=MigrationTest", "-c", "user.email=migration@example.invalid",
- "-c", "commit.gpgsign=false", "commit", "-m", "experiment")
- experimental <- git("rev-parse", "HEAD")[[1]]
- stopifnot(read_database()$stocks$scientific_name == "Source species",
- read_database(revision = experimental)$stocks$scientific_name == "Uncommitted replacement")
- git("switch", "main")
- git("update-ref", "refs/remotes/origin/main", revision)
- stocks$scientific_name <- "Latest committed main"
+ pinned$commit == revision, pinned$database_selection == "cached_snapshot")
+ # Reading remains local and stable when upstream data advance or Git is unavailable.
+ stocks$scientific_name <- "New committed species"
write.csv(stocks, "data_repo/database/stocks.csv", row.names = FALSE)
- git("add", "database")
- git("-c", "user.name=MigrationTest", "-c", "user.email=migration@example.invalid",
- "-c", "commit.gpgsign=false", "commit", "-m", "advance main")
+ stopifnot(read_database()$stocks$scientific_name == "Source species")
+ git("add", "database"); commit("new data")
+ stopifnot(read_database()$commit == revision)
+ update_database_cache("data_repo")
current <- read_database()
- stopifnot(current$stocks$scientific_name == "Latest committed main",
- current$commit != revision, current$database_key == current$commit,
- read_database(revision = revision)$stocks$scientific_name == "Source species")
- git("update-ref", "refs/remotes/origin/main", experimental)
- divergent <- tryCatch(read_database(), error = identity)
- stopifnot(inherits(divergent, "error"), grepl("behind or diverged", conditionMessage(divergent), fixed = TRUE),
- read_database(revision = revision)$stocks$scientific_name == "Source species")
- git("update-ref", "refs/remotes/origin/main", revision)
- git("switch", "-c", "remote-ahead")
- stocks$scientific_name <- "Remote main ahead"
- write.csv(stocks, "data_repo/database/stocks.csv", row.names = FALSE)
- git("add", "database")
- git("-c", "user.name=MigrationTest", "-c", "user.email=migration@example.invalid",
- "-c", "commit.gpgsign=false", "commit", "-m", "remote main")
- remote_ahead <- git("rev-parse", "HEAD")[[1]]
- git("switch", "main")
- git("update-ref", "refs/remotes/origin/main", remote_ahead)
- behind <- tryCatch(read_database(), error = identity)
- stopifnot(inherits(behind, "error"), grepl("behind or diverged", conditionMessage(behind), fixed = TRUE))
- git("update-ref", "refs/remotes/origin/main", revision)
-
+ stopifnot(current$commit != revision, current$stocks$scientific_name == "New committed species",
+ dir.exists(file.path(relative_root, "cache", "saio", revision)))
+ # An incomplete source copy cannot change the selected manifest.
+ writeLines("next source revision", "data_repo/note.txt")
+ git("add", "note.txt"); commit("copy interruption fixture")
+ file.rename("data_repo/database/outputs.csv", "data_repo/database/outputs.saved")
+ selected <- readLines(file.path(relative_root, "saio_revisions.csv"))
+ copy_error <- suppressWarnings(tryCatch(update_database_cache("data_repo"), error = identity))
+ stopifnot(inherits(copy_error, "error"),
+ identical(readLines(file.path(relative_root, "saio_revisions.csv")), selected),
+ identical(read_database()$commit, current$commit))
+ file.rename("data_repo/database/outputs.saved", "data_repo/database/outputs.csv")
+ rename <- file.rename("data_repo", "hidden_source")
+ original_head <- .database_head
+ .database_head <- function(...) stop("Reader must not invoke Git")
+ stopifnot(rename, identical(read_database()$commit, current$commit))
+ .database_head <- original_head
+ # A missing copy gives the manual-refresh instruction.
+ path <- file.path(relative_root, "cache", "saio", current$commit, "database.rds")
+ file.rename(path, paste0(path, ".saved"))
+ bad <- tryCatch(read_database(), error = identity)
+ stopifnot(inherits(bad, "error"), grepl("unavailable or incomplete", conditionMessage(bad)))
+ file.rename(paste0(path, ".saved"), path)
entries <- data.frame(assessment_id = c("shared", "shared", "extra"),
tier = c("curated", "preliminary", "preliminary"), source_commit = revision)
write.csv(entries, file.path(relative_root, "translations.csv"), row.names = FALSE)
for (tier in c("curated", "preliminary")) {
- folder <- file.path(relative_root, "scripts", "translation", tier)
+ folder <- file.path(relative_root, "scripts", tier)
dir.create(folder, recursive = TRUE)
for (id in entries$assessment_id[entries$tier == tier]) writeLines(c(
paste0('obs <- list(marker = "', tier, '")'),
@@ -138,7 +106,7 @@ local({
stopifnot(is.null(.assessment_cached_fit("shared", "curated", current)),
.assessment_cached_fit("shared", "curated", pinned)$marker == "historical",
current_cache != historical_cache,
- .assessment_cache_key(working) != .assessment_cache_key(current))
+ .assessment_cache_key(pinned) != .assessment_cache_key(current))
dir.create(current_cache, recursive = TRUE)
saveRDS(old_fit, file.path(current_cache, "fit.rds"))
mismatched <- tryCatch(.assessment_cached_fit("shared", "curated", current), error = identity)
@@ -186,7 +154,7 @@ local({
dir.create(cache, recursive = TRUE, showWarnings = FALSE)
writeLines("Cached dashboard", file.path(cache, "dashboard.html"))
}
- index_assessment_dashboards(tier, current)
+ .index_assessment_dashboards(tier, current)
}
curated_index <- readLines(file.path(.assessment_results("curated"), "index.html"))
preliminary_index <- readLines(file.path(.assessment_results("preliminary"), "index.html"))
@@ -200,4 +168,4 @@ local({
!any(grepl("/experiments/", curated_index, fixed = TRUE)),
!any(grepl("/experiments/", preliminary_index, fixed = TRUE)))
})
-cat("Latest committed SAIO reads, explicit snapshots, source-keyed caches, tier dispatch and provenance regression checks passed.\n")
+cat("Manual SAIO snapshots, stable local reads, tier dispatch and source-keyed cache checks passed.\n")
diff --git a/analysis/comp_assessments/tests/test_sandeel_area_1r_source.R b/analysis/comp_assessments/tests/test_sandeel_area_1r_source.R
index a57a8699..d2bba1a8 100644
--- a/analysis/comp_assessments/tests/test_sandeel_area_1r_source.R
+++ b/analysis/comp_assessments/tests/test_sandeel_area_1r_source.R
@@ -1,7 +1,7 @@
source("analysis/comp_assessments/R/read_database.R")
pkgload::load_all(".", quiet = TRUE)
source("analysis/comp_assessments/R/run_assessment.R")
-db <- read_database(tier = "preliminary")
+db <- read_database()
source <- read_assessment("ices_sandeel_area_1r_2026", db)
stopifnot(source$stocks$stock_id == "ices_sandeel_area_1r",
is.na(source$stocks$charbonneau_id), source$stocks$authority_stock_id == "san.sa.1r",
@@ -69,20 +69,4 @@ stopifnot(abs(exp(mean(log(recruits$value))) - 111219266) < 1,
stopifnot(grepl("surveySD/catchSD/estSD", source$assumptions$notes[
source$assumptions$setting == "remaining_sources"], fixed = TRUE))
-validate_seasonal_fixture <- function(tables) {
- env <- new.env(parent = globalenv())
- env$read.csv <- function(file, ...) tables[[sub(".csv", "", basename(file), fixed = TRUE)]]
- tryCatch({
- invisible(capture.output(sys.source(
- file.path(saio_path(), "scripts", "database", "002_validate_database.R"), envir = env)))
- NULL
- }, error = identity)
-}
-at <- which(db$inputs$assessment_id == source$assessment$assessment_id & db$inputs$type == "M")[1L]
-for (field in c("type", "season")) {
- wrong <- db
- wrong$inputs[[field]][at] <- if (field == "type") "weight" else NA_character_
- error <- validate_seasonal_fixture(wrong)
- stopifnot(inherits(error, "error"), grepl("M-at-age and a source season", conditionMessage(error), fixed = TRUE))
-}
-cat("Seasonal mortality validation rejects other input types and missing source seasons.\n")
+# Canonical schema rejection tests run in SAIO, which owns its validator.
diff --git a/analysis/comp_assessments/tests/test_sandeel_area_1r_translation.R b/analysis/comp_assessments/tests/test_sandeel_area_1r_translation.R
index 57974d2c..fb8e88da 100644
--- a/analysis/comp_assessments/tests/test_sandeel_area_1r_translation.R
+++ b/analysis/comp_assessments/tests/test_sandeel_area_1r_translation.R
@@ -7,7 +7,7 @@ obs <- x$obs
dat <- x$dat
stopifnot(identical(source_record, before), is.null(x$fit),
identical(x$years, 1983:2025), identical(x$ages, 1:4),
- tinyAM::check_obs(obs), nrow(obs$catch) == 172L, nrow(obs$index) == 43L,
+ tinyAM::check_tam_data(obs), nrow(obs$catch) == 172L, nrow(obs$index) == 43L,
all(obs$catch$unit == "thousand fish"),
obs$catch$obs[obs$catch$year == 1983 & obs$catch$age == 2] == (28971 + 3085) * 1000,
all(obs$index$age >= 1), sum(obs$index$survey == "Dredge") == 22L,
@@ -58,7 +58,7 @@ stopifnot(all(ref$pop$recruitment$est == n$value[match(ref$pop$recruitment$year,
all(attr(ref, "native_pop")$recruitment$age == 0))
cat("Sandeel annual catch/M aggregation, January biology, original indices, settings and same-year recruitment passed.\n")
source("analysis/comp_assessments/R/plot_stock_order.R")
-metadata <- assessment_plot_stocks(read_database(tier = "preliminary")$assessments, read_database(tier = "preliminary")$stocks,
+metadata <- assessment_plot_stocks(read_database()$assessments, read_database()$stocks,
source_record$assessment$assessment_id)
stopifnot(nrow(metadata) == 1L, metadata$scientific_name == "Ammodytes spp.",
as.character(metadata$basin) == "Northeast Atlantic")
diff --git a/analysis/comp_assessments/tests/test_sandeel_area_2r_source.R b/analysis/comp_assessments/tests/test_sandeel_area_2r_source.R
index 134cf0c6..c6a4ea1c 100644
--- a/analysis/comp_assessments/tests/test_sandeel_area_2r_source.R
+++ b/analysis/comp_assessments/tests/test_sandeel_area_2r_source.R
@@ -1,5 +1,5 @@
source("analysis/comp_assessments/R/run_assessment.R")
-db <- read_database(tier = "preliminary")
+db <- read_database()
s <- read_assessment("ices_sandeel_area_2r_2026", db)
i <- s$inputs; o <- s$outputs
stopifnot(s$stocks$authority_stock_id == "san.sa.2r", is.na(s$stocks$charbonneau_id),
diff --git a/analysis/comp_assessments/tests/test_sandeel_area_2r_translation.R b/analysis/comp_assessments/tests/test_sandeel_area_2r_translation.R
index 29ec8ec5..8d11e9a0 100644
--- a/analysis/comp_assessments/tests/test_sandeel_area_2r_translation.R
+++ b/analysis/comp_assessments/tests/test_sandeel_area_2r_translation.R
@@ -7,7 +7,7 @@ obs <- x$obs
dat <- x$dat
stopifnot(identical(source_record, before), is.null(x$fit),
identical(x$years, 1983:2025), identical(x$ages, 1:4),
- tinyAM::check_obs(obs), nrow(obs$catch) == 172L, nrow(obs$index) == 22L,
+ tinyAM::check_tam_data(obs), nrow(obs$catch) == 172L, nrow(obs$index) == 22L,
all(obs$catch$unit == "thousand fish"),
obs$catch$obs[obs$catch$year == 1983 & obs$catch$age == 2] == (6191 + 877) * 1000,
all(obs$index$age >= 1), sum(obs$index$survey == "Dredge") == 22L,
@@ -22,7 +22,7 @@ stopifnot(identical(source_record, before), is.null(x$fit),
expected <- c("1983" = "early.1", "1998" = "early.1", "1999" = "middle1.1",
"2004" = "middle1.1", "2005" = "middle2.1", "2009" = "middle2.1",
"2010" = "recent.1", "2025" = "recent.1")
-young <- obs$pop_covariates[obs$pop_covariates$age == 1, ]
+young <- obs$catch[obs$catch$age == 1, ]
stopifnot(identical(as.character(young$F_mean_group[
match(as.integer(names(expected)), young$year)]), unname(expected)))
diff --git a/analysis/comp_assessments/tests/test_sandeel_area_3r_source.R b/analysis/comp_assessments/tests/test_sandeel_area_3r_source.R
index dd9051d9..49dd924e 100644
--- a/analysis/comp_assessments/tests/test_sandeel_area_3r_source.R
+++ b/analysis/comp_assessments/tests/test_sandeel_area_3r_source.R
@@ -1,5 +1,5 @@
source("analysis/comp_assessments/R/run_assessment.R")
-db <- read_database(tier = "preliminary")
+db <- read_database()
s <- read_assessment("ices_sandeel_area_3r_2026", db)
i <- s$inputs; o <- s$outputs
stopifnot(s$stocks$authority_stock_id == "san.sa.3r", is.na(s$stocks$charbonneau_id),
diff --git a/analysis/comp_assessments/tests/test_sandeel_area_3r_translation.R b/analysis/comp_assessments/tests/test_sandeel_area_3r_translation.R
index 1a797cae..49ac4caa 100644
--- a/analysis/comp_assessments/tests/test_sandeel_area_3r_translation.R
+++ b/analysis/comp_assessments/tests/test_sandeel_area_3r_translation.R
@@ -5,7 +5,7 @@ before <- source_record
x <- .test_stock(source_record)
obs <- x$obs; dat <- x$dat
stopifnot(identical(source_record, before), is.null(x$fit),
- identical(x$years, 1987:2025), identical(x$ages, 1:4), tinyAM::check_obs(obs),
+ identical(x$years, 1987:2025), identical(x$ages, 1:4), tinyAM::check_tam_data(obs),
nrow(obs$catch) == 156L, nrow(obs$index) == 88L,
all(obs$catch$unit == "thousand fish"),
obs$catch$obs[obs$catch$year == 1987 & obs$catch$age == 1] == (33760 + 65) * 1000,
diff --git a/analysis/comp_assessments/tests/test_sandeel_area_4_source.R b/analysis/comp_assessments/tests/test_sandeel_area_4_source.R
index eb9df0e5..1bfdc1e8 100644
--- a/analysis/comp_assessments/tests/test_sandeel_area_4_source.R
+++ b/analysis/comp_assessments/tests/test_sandeel_area_4_source.R
@@ -1,5 +1,5 @@
source("analysis/comp_assessments/R/run_assessment.R")
-db <- read_database(tier = "preliminary")
+db <- read_database()
s <- read_assessment("ices_sandeel_area_4_2025", db)
i <- s$inputs; o <- s$outputs
stopifnot(s$stocks$authority_stock_id == "san.sa.4", is.na(s$stocks$charbonneau_id),
diff --git a/analysis/comp_assessments/tests/test_sandeel_area_4_translation.R b/analysis/comp_assessments/tests/test_sandeel_area_4_translation.R
index 9a7d20dd..61d29f49 100644
--- a/analysis/comp_assessments/tests/test_sandeel_area_4_translation.R
+++ b/analysis/comp_assessments/tests/test_sandeel_area_4_translation.R
@@ -4,7 +4,7 @@ s <- read_assessment("ices_sandeel_area_4_2025")
before <- s
x <- .test_stock(s)
obs <- x$obs; dat <- x$dat
-stopifnot(identical(s, before), is.null(x$fit), tinyAM::check_obs(obs),
+stopifnot(identical(s, before), is.null(x$fit), tinyAM::check_tam_data(obs),
identical(x$years, 1993:2024), identical(x$ages, 1:4),
nrow(obs$catch) == 128L, nrow(obs$index) == 42L,
all(obs$catch$unit == "thousand fish"), all(obs$index$age == 1),
diff --git a/analysis/comp_assessments/tests/test_stock_scripts.R b/analysis/comp_assessments/tests/test_stock_scripts.R
index b46887a8..4f16d734 100644
--- a/analysis/comp_assessments/tests/test_stock_scripts.R
+++ b/analysis/comp_assessments/tests/test_stock_scripts.R
@@ -37,7 +37,7 @@ stopifnot(!exists("warm_fit", inherits = FALSE))
# Run the interactive setup with fitting disabled, in a separate workspace.
interactive_env <- new.env(parent = globalenv())
-for (statement in parse("analysis/comp_assessments/scripts/translation/run_stock.R")) {
+for (statement in parse("analysis/comp_assessments/scripts/run_stock.R")) {
if (is.call(statement) && identical(statement[[1]], quote(`<-`)) &&
identical(statement[[2]], quote(do_fit))) {
statement[[3]] <- FALSE
@@ -53,7 +53,7 @@ cat("Top-level stock scripts and fitting-disabled isolation passed.\n")
# Loading another stock must clear absent comparison options, including in the workspace.
interactive_env$comparison_age_groups <- list(N = list(stale = 999))
-for (statement in parse("analysis/comp_assessments/scripts/translation/run_stock.R")) {
+for (statement in parse("analysis/comp_assessments/scripts/run_stock.R")) {
if (is.call(statement) && identical(statement[[1]], quote(`<-`))) {
if (identical(statement[[2]], quote(do_fit))) statement[[3]] <- FALSE
if (identical(statement[[2]], quote(assessment_id))) statement[[3]] <- "afsc_cod_goa_2026"
diff --git a/analysis/comp_assessments/tests/test_translation.R b/analysis/comp_assessments/tests/test_translation.R
index 7d5f8bd9..2ae53ee9 100644
--- a/analysis/comp_assessments/tests/test_translation.R
+++ b/analysis/comp_assessments/tests/test_translation.R
@@ -2,7 +2,7 @@ source("analysis/comp_assessments/R/read_database.R")
pkgload::load_all(".", quiet = TRUE)
root <- file.path("analysis", "comp_assessments")
source(file.path(root, "R", "database_to_tam_obs.R"))
-check_obs <- tinyAM::check_obs
+check_tam_data <- tinyAM::check_tam_data
source(file.path(root, "R", "audit_assumptions.R"))
if (!requireNamespace("cli", quietly = TRUE)) {
.translation_abort <- function(message) stop(message, call. = FALSE)
@@ -469,7 +469,8 @@ expect_equal(
)
expect_equal(fixed_m_inputs, fixed_m_inputs_before)
-read_table <- function(name) .saio_read_table(name)
+database <- read_database()
+read_table <- function(name) database[[sub("[.]csv$", "", name)]]
assumptions_db <- read_table("assumptions.csv")
inputs_db <- read_table("inputs.csv")
audit <- audit_assumptions("dfo_cod_4t4vn_2019", assumptions_db)
@@ -621,11 +622,9 @@ expect_equal(herring_inputs, herring_before)
expect_equal(sum(is.na(herring_obs$catch$obs)), 18L)
source(file.path(root, "R", "read_database.R"))
-working_db <- read_database()
-db <- read_committed_database()
-expect_equal(working_db$database_source, "saio_committed")
-expect_equal(db$database_source, "saio_committed")
-expect_equal(grepl("^[[:xdigit:]]{40}$", working_db$commit), TRUE)
+db <- read_database()
+expect_equal(db$database_source, "saio_snapshot")
+expect_equal(grepl("^[[:xdigit:]]{40}$", db$commit), TRUE)
fixture_dir <- tempfile("assessment_database_")
dir.create(fixture_dir)
@@ -633,12 +632,14 @@ database_files <- c(
"stocks.csv", "assessments.csv", "assumptions.csv",
"inputs.csv", "outputs.csv"
)
-for (name in database_files) {
- write.csv(data.frame(source_file = name), file.path(fixture_dir, name),
- row.names = FALSE
- )
-}
-fixture_db <- read_database(fixture_dir)
+fixture_revision <- paste(rep("f", 40), collapse = "")
+dir.create(file.path(fixture_dir, fixture_revision))
+fixture_tables <- setNames(lapply(database_files, function(name)
+ data.frame(source_file = name)), sub("[.]csv$", "", database_files))
+saveRDS(fixture_tables, file.path(fixture_dir, fixture_revision, "database.rds"))
+fixture_manifest <- file.path(fixture_dir, "saio_revisions.csv")
+write.csv(data.frame(revision = fixture_revision), fixture_manifest, row.names = FALSE)
+fixture_db <- read_database(fixture_dir, fixture_manifest)
expect_equal(
unname(vapply(
fixture_db[c(
@@ -650,12 +651,10 @@ expect_equal(
database_files
)
-working_ebs <- read_assessment("afsc_pollock_ebs_2024", working_db)
-ebs <- read_committed_assessment("afsc_pollock_ebs_2024", database = db)
+ebs <- read_assessment("afsc_pollock_ebs_2024", database = db)
expect_equal(nrow(ebs$assessment), 1L)
expect_equal(all(ebs$inputs$assessment_id == "afsc_pollock_ebs_2024"), TRUE)
expect_equal(grepl("^[[:xdigit:]]{40}$", ebs$commit), TRUE)
-expect_equal(working_ebs$inputs, ebs$inputs)
expect_equal(read_assessment("afsc_pollock_ebs_2024", db)$inputs, ebs$inputs)
ebs_surveys <- c(
"NMFS bottom-trawl VAST", "NMFS acoustic-trawl",
@@ -743,12 +742,12 @@ expect_equal(all(is.na(ebs_reference$obs_pred$catch$pred)), TRUE)
expect_equal(all(is.na(ebs_reference$pop$ssb$se_scale)), TRUE)
ns_haddock_id <- "ices_haddock_north_sea_2026"
-ns_haddock <- read_committed_assessment(ns_haddock_id, database = db)
+ns_haddock <- read_assessment(ns_haddock_id, database = db)
ns_haddock_obs <- database_to_tam_obs(
ns_haddock_id, ns_haddock$inputs,
years = 1972:2026, ages = 0:8, assumptions = ns_haddock$assumptions
)
-expect_equal(check_obs(ns_haddock_obs), TRUE)
+expect_equal(check_tam_data(ns_haddock_obs), TRUE)
expect_equal(nrow(ns_haddock_obs$index), 667L)
expect_equal(all(is.finite(ns_haddock_obs$index$relative_sd)), TRUE)
expect_equal(all(abs(ns_haddock_obs$index$relative_sd^2 *
diff --git a/analysis/comp_assessments/translation_notes/ck_ices_afwg_mentella_deep_1_2.md b/analysis/comp_assessments/translation_notes/ck_ices_afwg_mentella_deep_1_2.md
index dd7c5725..df30f576 100644
--- a/analysis/comp_assessments/translation_notes/ck_ices_afwg_mentella_deep_1_2.md
+++ b/analysis/comp_assessments/translation_notes/ck_ices_afwg_mentella_deep_1_2.md
@@ -5,7 +5,7 @@ Transferred from the pre-cleanup SAIO review without refitting. Check the regist
## Limited tinyAM translation
The maintained recipe is
-[preliminary recipe](../scripts/translation/preliminary/jrn_redfish_beaked_2026.R). It fits 1992–2025
+[preliminary recipe](../scripts/preliminary/jrn_redfish_beaked_2026.R). It fits 1992–2025
at ages 6–19+, using the non-overlapping total catch-at-age series once,
fixed M = 0.05, published age-6+ weight and maturity, and the ecosystem and
winter number indices for ages 6–15. The million-fish indices are converted
diff --git a/analysis/comp_assessments/translation_notes/dfo_cod_3ps_2024.md b/analysis/comp_assessments/translation_notes/dfo_cod_3ps_2024.md
index 70545a0a..5c567521 100644
--- a/analysis/comp_assessments/translation_notes/dfo_cod_3ps_2024.md
+++ b/analysis/comp_assessments/translation_notes/dfo_cod_3ps_2024.md
@@ -4,7 +4,7 @@ Transferred from the pre-cleanup SAIO review without refitting. Check the regist
## tinyAM translation readiness
-The translation recipe is in [preliminary recipe](../scripts/translation/preliminary/dfo_cod_3ps_2024.R). It uses reported catch-at-age directly, fits the five index series with independent age-by-survey q values, approximates seasonal timing by within-season midpoints, uses an age-correlated F random walk without the source age-2 separation or 1993 break, and models log M with age-specific levels and separate condition slopes for ages 2–5 and 6+. The N process is now IID to represent independent cohort-survival deviations, with exponential initial abundance. These remain approximations: tinyAM does not reproduce censored zero-index observations, censored landings intervals, continuation-ratio catch composition, the exact condition-to-M relationship, or all HYBRID process structures. Survey index units remain unresolved, with survey-specific q absorbing the reported scale.
+The translation recipe is in [preliminary recipe](../scripts/preliminary/dfo_cod_3ps_2024.R). It uses reported catch-at-age directly, fits the five index series with independent age-by-survey q values, approximates seasonal timing by within-season midpoints, uses an age-correlated F random walk without the source age-2 separation or 1993 break, and models log M with age-specific levels and separate condition slopes for ages 2–5 and 6+. The N process is now IID to represent independent cohort-survival deviations, with exponential initial abundance. These remain approximations: tinyAM does not reproduce censored zero-index observations, censored landings intervals, continuation-ratio catch composition, the exact condition-to-M relationship, or all HYBRID process structures. Survey index units remain unresolved, with survey-specific q absorbing the reported scale.
The updated recipe first used package-default initialization. Numerical checks failed: `nlminb` returned code 1 (`singular convergence (7)`), with a maximum gradient of 0.000753. The Hessian was positive definite and `sdreport` returned, but the fixed-parameter covariance contained non-finite values. After that failure, one documented retry used the published age-2 recruitment and Fbar estimates for 2020-2024 as starting values only; recruitment was converted from million fish to tinyAM's thousand-fish scale and Fbar initialized ages 5-8 equally. All other parameters retained package defaults, and observations and model settings were unchanged. The retry returned optimizer code 0 (`relative convergence (4)`) and a lower objective of 2100.817, with a maximum gradient of 0.000606, but the fixed-parameter covariance still contained non-finite values. It therefore also failed numerical checks. Both attempts had 80 fixed effects and 1,703 random effects, a positive-definite Hessian, and an advisory that the F-increment age correlation was near its boundary. No converged-fit dashboard was produced; treat both fits and their uncertainty as unavailable. The recipe test verifies the year and age grid, timing, IID N setting, two condition-effect columns, comparison window, and alignment/scale of available source starts. Its observation converter is stubbed, so it does not validate source-to-observation conversion or establish a successful fit.
diff --git a/analysis/comp_assessments/translation_notes/dfo_petrale_bc.md b/analysis/comp_assessments/translation_notes/dfo_petrale_bc.md
index bc552adb..90d1025e 100644
--- a/analysis/comp_assessments/translation_notes/dfo_petrale_bc.md
+++ b/analysis/comp_assessments/translation_notes/dfo_petrale_bc.md
@@ -9,7 +9,7 @@ SSB now preserved in the canonical database. Fitting is **source-blocked**.
These source gaps must be resolved before preparing a model. There is no
catch-only substitute, invented `fit_tam()` model, optimizer result or dashboard.
-The stock entry point is now the [curated stock script](../scripts/translation/curated/dfo_petrale_bc_2024.R). It records the source
+The stock entry point is now the [curated stock script](../scripts/curated/dfo_petrale_bc_2024.R). It records the source
background and stops with the missing-input inventory before `prepare_tam()`
or `fit_tam()`, for both `fit = FALSE` and `fit = TRUE`. An explicit runner
attempt is recorded as `translation_failed`, with no optimizer or uncertainty
diff --git a/analysis/comp_assessments/translation_notes/ices_cod_irish_sea.md b/analysis/comp_assessments/translation_notes/ices_cod_irish_sea.md
index e2e6bec7..1806434f 100644
--- a/analysis/comp_assessments/translation_notes/ices_cod_irish_sea.md
+++ b/analysis/comp_assessments/translation_notes/ices_cod_irish_sea.md
@@ -12,6 +12,6 @@ Index scale multipliers and exact likelihood allocations are unresolved. The nat
The default-initialization fit converged (optimizer code 0, maximum gradient 0.000364, positive-definite Hessian). Over 1968–2018, approximate SSB agreement was mean absolute percent difference 36.5%, terminal difference -9.3%, and trend correlation 0.971. Age-0 recruitment agreement was weaker: mean absolute percent difference 61.0%, terminal difference +1568%, and trend correlation 0.731. The simpler recruitment process remains an important limitation; no starting values were supplied.
-The comparison period does not include the assessment's terminal 2019–2025 estimates. Official aggregate SSB timing is not stated, while tinyAM calculates beginning-of-year mature biomass, so SSB is approximate. Recruitment is age-aligned but its published point-estimate convention is unspecified, so it is also approximate. Official age-specific N and F are unavailable. Official Fbar is ages 2–4, while tinyAM Fbar is abundance-weighted and the required official N-at-age is unavailable; therefore the Fbar values are not compared. M is fixed at the published input values and shown as an input rather than a fitted output. The maintained translation and structural checks are in [preliminary recipe](../scripts/translation/preliminary/ices_cod_irish_sea_2026.R) and [translation check](../tests/test_irish_sea_cod_translation.R); source values are validated by [SAIO source check](https://github.com/PaulRegular/saio/blob/main/tests/test_irish_sea_cod_source.R).
+The comparison period does not include the assessment's terminal 2019–2025 estimates. Official aggregate SSB timing is not stated, while tinyAM calculates beginning-of-year mature biomass, so SSB is approximate. Recruitment is age-aligned but its published point-estimate convention is unspecified, so it is also approximate. Official age-specific N and F are unavailable. Official Fbar is ages 2–4, while tinyAM Fbar is abundance-weighted and the required official N-at-age is unavailable; therefore the Fbar values are not compared. M is fixed at the published input values and shown as an input rather than a fitted output. The maintained translation and structural checks are in [preliminary recipe](../scripts/preliminary/ices_cod_irish_sea_2026.R) and [translation check](../tests/test_irish_sea_cod_translation.R); source values are validated by [SAIO source check](https://github.com/PaulRegular/saio/blob/main/tests/test_irish_sea_cod_source.R).
[Official source review](https://github.com/PaulRegular/saio/blob/main/source_reviews/ices_cod_irish_sea.md).
diff --git a/analysis/comp_assessments/translation_notes/ices_herring_norwegian_spring_spawning.md b/analysis/comp_assessments/translation_notes/ices_herring_norwegian_spring_spawning.md
index d579402b..8f4de44b 100644
--- a/analysis/comp_assessments/translation_notes/ices_herring_norwegian_spring_spawning.md
+++ b/analysis/comp_assessments/translation_notes/ices_herring_norwegian_spring_spawning.md
@@ -9,7 +9,7 @@ Official assessment evidence is maintained in [SAIO](https://github.com/PaulRegu
### tinyAM translation
The current-run approximation is defined in
-[preliminary recipe](../scripts/translation/preliminary/ices_herring_norwegian_spring_2026.R). It fits
+[preliminary recipe](../scripts/preliminary/ices_herring_norwegian_spring_2026.R). It fits
1988–2025 at ages 2–12, using IID N with free initial abundance, an F random
walk, and the standard age-specific M pattern as fixed. Catch and the four
numerical survey series are fitted with tinyAM observation errors; SAM's
diff --git a/cheatsheet/background.md b/cheatsheet/background.md
index b1cd8700..b29c8047 100644
--- a/cheatsheet/background.md
+++ b/cheatsheet/background.md
@@ -15,10 +15,10 @@ The fits use `cod_obs` for 1983-2025 and model ages 2-14, with age 14 as the plu
### Navigating the dashboard
-Use **Fisheries** and **Surveys** to compare observations with fitted values and inspect residual plots. Fisheries lists one page per fleet, then **Aggregate fishery**. Without fleets, the aggregate page contains the single-fishery diagnostics. **Trends** compares population estimates such as spawning stock biomass. The **Parameters** pages show fixed parameter estimates, including coefficients and process or observation standard deviations, and random parameter estimates, including latent states and process deviations across years and ages.
+The opening **Model structure** page shows the diagram and function calls. Use **Fisheries** and **Surveys** to compare observations with fitted values and inspect residuals. Plots start with retained OSAs where available, otherwise standardized residuals; the dropdown selects either type. Fisheries lists each fleet, then **Aggregate fishery**. Without fleets, the aggregate page contains the single-fishery diagnostics. **Trends** compares population estimates. **Parameters** shows uncertainty, latent states and process deviations.
On **Random**, **Process deviations** shows fitted log-scale departures from mean or cohort predictions, or annual increments for random walks. **Joint-sample residuals** standardizes these innovations using the fitted process correlations and variation, then propagates one shared draw from Gaussian parameter uncertainty. This local Gaussian approximation provides reproducible diagnostic values (default seed `123456`). Missing or invalid joint uncertainty is identified explicitly; fitted deviations remain available. Each viewer separates recruitment line plots from N/F/M matrices or bubbles.
-The **Tables** pages contain numerical and statistical diagnostics, input data, model settings and calls, and fitted outputs such as population estimates and parameter tables. Use these checks to assess model fit before interpreting differences. The separate hindcast call in the cheatsheet assesses survey forecast skill conditional on observed catches.
+The **Tables** pages contain diagnostics, inputs and fitted outputs. Assess model fit before interpreting differences. The separate hindcast call in the cheatsheet assesses survey forecast skill conditional on observed catches.
Hover over Plotly figures to inspect values and units. Drag to zoom, hold Shift while dragging to pan, and double-click to reset the view. Use the legend to show or hide series and use any animation controls to move between models or years. Shaded bands show 95% confidence intervals where available.
diff --git a/cheatsheet/examples.R b/cheatsheet/examples.R
index d8dface6..f9b7fc44 100644
--- a/cheatsheet/examples.R
+++ b/cheatsheet/examples.R
@@ -92,8 +92,9 @@ dat <- make_tam_data(
)
# ---- end ----
# Reporting/layout changes must not invalidate numerical model fits.
- fit_sources <- sources[!names(sources) %in% c("plot.R", "plot-process.R", "palette.R",
- "tidy.R", "vis.R", "process-diagnostics.R", "NAMESPACE")]
+ fit_sources <- sources[!grepl("^plot[-.]", names(sources)) &
+ !names(sources) %in% c("palette.R", "tidy.R", "vis.R", "model-diagram.R",
+ "getters.R", "process-diagnostics.R", "NAMESPACE")]
fit_key <- list(sources = fit_sources, example = provenance$example_md5, data = fingerprint(dat), runtime = provenance$runtime)
records <- list()
cached <- function(name, key, code) {
@@ -167,7 +168,8 @@ ncam <- update(scaa,
provenance$process_diagnostics <- diagnostics
tidy_key <- list(fit = fit_key, sources = sources[c("tidy.R", "process-diagnostics.R")], diagnostics = diagnostics)
out <- cached("tidy", tidy_key, function() tidy_tam(scaa, sam, ncam, process_seed = process_seed))
- dashboard_key <- list(tidy = tidy_key, sources = sources[c("vis.R", "plot.R", "plot-process.R")],
+ dashboard_key <- list(tidy = tidy_key, sources = sources[c("vis.R", "model-diagram.R",
+ names(sources)[grepl("^plot[-.]", names(sources))])],
layout = "random-dropdowns-v2", templates = templates, background = file_hashes(background_file))
prior <- if (file.exists(dashboard_file)) readRDS(dashboard_file) else NULL
# ---- dashboard ----
diff --git a/cheatsheet/tinyAM_cheatsheet.pdf b/cheatsheet/tinyAM_cheatsheet.pdf
index 7c353a52..600c6fd0 100644
Binary files a/cheatsheet/tinyAM_cheatsheet.pdf and b/cheatsheet/tinyAM_cheatsheet.pdf differ
diff --git a/inst/js/model-structure.js b/inst/js/model-structure.js
new file mode 100644
index 00000000..995847be
--- /dev/null
+++ b/inst/js/model-structure.js
@@ -0,0 +1,169 @@
+(function() {
+ 'use strict';
+ var root = document.currentScript.closest('.tam-model-structure');
+ if (!root) return;
+ var scheduled = false;
+ var ns = 'http://www.w3.org/2000/svg';
+
+ function box(element, origin, padding) {
+ var r = element.getBoundingClientRect();
+ return {left: r.left - origin.left - padding, right: r.right - origin.left + padding,
+ top: r.top - origin.top - padding, bottom: r.bottom - origin.top + padding};
+ }
+ function port(rect, side, fraction) {
+ var p = side === 'left' || side === 'right' ?
+ [rect[side], rect.top + fraction * (rect.bottom - rect.top)] :
+ [rect.left + fraction * (rect.right - rect.left), rect[side]];
+ var direction = {left: [-1, 0], right: [1, 0], top: [0, -1], bottom: [0, 1]}[side];
+ return {tip: p, outside: [p[0] + 10 * direction[0], p[1] + 10 * direction[1]]};
+ }
+ function clear(a, b, obstacles) {
+ return !obstacles.some(function(r) {
+ return a[0] === b[0] ? a[0] > r.left && a[0] < r.right &&
+ Math.max(a[1], b[1]) > r.top && Math.min(a[1], b[1]) < r.bottom :
+ a[1] > r.top && a[1] < r.bottom && Math.max(a[0], b[0]) > r.left &&
+ Math.min(a[0], b[0]) < r.right;
+ });
+ }
+ // Orthogonal paths follow measured gaps, keeping arrows outside node text at
+ // every layout width. The small visibility grid needs no layout dependency.
+ function route(start, end, obstacles, width, height) {
+ function round(x) { return Math.round(x * 1000) / 1000; }
+ function coordinates(values, maximum) {
+ return Array.from(new Set(values.map(round))).filter(function(x) {
+ return x >= 2 && x <= maximum - 2;
+ }).sort(function(a, b) { return a - b; });
+ }
+ start = start.map(round); end = end.map(round);
+ var xs = coordinates([2, width - 2, start[0], end[0]].concat(obstacles.flatMap(function(r) {
+ return [r.left - 2, r.right + 2];
+ })), width);
+ var ys = coordinates([2, height - 2, start[1], end[1]].concat(obstacles.flatMap(function(r) {
+ return [r.top - 2, r.bottom + 2];
+ })), height);
+ var ny = ys.length;
+ function id(p) { return xs.indexOf(p[0]) * ny + ys.indexOf(p[1]); }
+ function point(i) { return [xs[Math.floor(i / ny)], ys[i % ny]]; }
+ function distance(a, b) { return Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]); }
+ if (!xs.includes(start[0]) || !xs.includes(end[0]) || !ys.includes(start[1]) || !ys.includes(end[1])) return [];
+ var first = id(start), last = id(end), costs = Array(xs.length * ny).fill(Infinity);
+ var previous = Array(costs.length).fill(-1), open = [{id: first, cost: 0, score: distance(start, end)}];
+ costs[first] = 0;
+ while (open.length) {
+ open.sort(function(a, b) { return b.score - a.score || a.cost - b.cost; });
+ var current = open.pop();
+ if (current.cost !== costs[current.id]) continue;
+ if (current.id === last) {
+ var result = [];
+ for (var k = last; k !== -1; k = previous[k]) result.push(point(k));
+ return result.reverse();
+ }
+ var ix = Math.floor(current.id / ny), iy = current.id % ny;
+ [[ix - 1, iy], [ix + 1, iy], [ix, iy - 1], [ix, iy + 1]].forEach(function(next) {
+ if (next[0] < 0 || next[0] >= xs.length || next[1] < 0 || next[1] >= ny) return;
+ var nextId = next[0] * ny + next[1], a = point(current.id), b = point(nextId);
+ if (!clear(a, b, obstacles)) return;
+ var cost = current.cost + distance(a, b);
+ if (cost >= costs[nextId]) return;
+ costs[nextId] = cost; previous[nextId] = current.id;
+ open.push({id: nextId, cost: cost, score: cost + distance(b, end)});
+ });
+ }
+ return [];
+ }
+ function arrow(svg, points, name, dashed) {
+ if (points.length < 2) return;
+ points = points.filter(function(p, i) {
+ if (i === 0 || i === points.length - 1) return true;
+ var a = points[i - 1], b = points[i + 1];
+ return !(a[0] === p[0] && p[0] === b[0]) && !(a[1] === p[1] && p[1] === b[1]);
+ });
+ var group = document.createElementNS(ns, 'g');
+ group.setAttribute('data-connection', name);
+ var line = document.createElementNS(ns, 'polyline');
+ line.setAttribute('points', points.map(function(p) { return p.join(','); }).join(' '));
+ line.setAttribute('fill', 'none'); line.setAttribute('stroke', dashed ? '#af8a27' : '#526a75');
+ line.setAttribute('stroke-width', '1.6');
+ if (dashed) line.setAttribute('stroke-dasharray', '4,3');
+ group.appendChild(line);
+ var end = points[points.length - 1], before = points[points.length - 2];
+ var length = Math.hypot(end[0] - before[0], end[1] - before[1]);
+ if (!length) return;
+ var dx = (end[0] - before[0]) / length, dy = (end[1] - before[1]) / length;
+ var head = document.createElementNS(ns, 'polygon');
+ head.setAttribute('points', [end, [end[0] - 7 * dx - 3 * dy, end[1] - 7 * dy + 3 * dx],
+ [end[0] - 7 * dx + 3 * dy, end[1] - 7 * dy - 3 * dx]].map(function(p) { return p.join(','); }).join(' '));
+ head.setAttribute('fill', dashed ? '#af8a27' : '#526a75');
+ group.appendChild(head); svg.appendChild(group);
+ }
+ function draw(graph) {
+ var bounds = graph.getBoundingClientRect();
+ if (!bounds.width || !bounds.height) return;
+ graph.setAttribute('data-layout', bounds.width < 460 ? 'stacked' : bounds.width < 760 ? 'compact' : 'wide');
+ bounds = graph.getBoundingClientRect();
+ var svg = graph.querySelector('.tam-model-connectors'), nodes = {}, obstacles = [];
+ graph.querySelectorAll('[data-component]').forEach(function(node) {
+ nodes[node.getAttribute('data-component')] = box(node, bounds, 0);
+ obstacles.push(box(node, bounds, 4));
+ });
+ graph.querySelectorAll('[data-input-component]').forEach(function(input) {
+ obstacles.push(box(input, bounds, 4));
+ });
+ svg.replaceChildren();
+ svg.setAttribute('viewBox', '0 0 ' + bounds.width + ' ' + bounds.height);
+ var edges = [
+ ['initial', 'population', 'right', 'left', .5, .2],
+ ['recruitment', 'population', 'right', 'left', .5, .5],
+ ['fishing', 'population', 'top', 'bottom', .5, .25],
+ ['natural', 'population', 'top', 'left', .5, .8],
+ ['population', 'survey', 'right', 'left', .3, .5],
+ ['population', 'catch', 'right', 'left', .7, .5],
+ ['fishing', 'catch', 'bottom', 'bottom', .5, .5],
+ ['population', 'ssb', 'bottom', 'top', .7, .5]
+ ];
+ if (graph.getAttribute('data-feedback') === 'true') edges.push(['ssb', 'recruitment', 'left', 'right', .5, .8]);
+ edges.forEach(function(edge) {
+ if (!nodes[edge[0]] || !nodes[edge[1]]) return;
+ var from = port(nodes[edge[0]], edge[2], edge[4]), to = port(nodes[edge[1]], edge[3], edge[5]);
+ var middle = route(from.outside, to.outside, obstacles, bounds.width, bounds.height);
+ if (middle.length) arrow(svg, [from.tip].concat(middle, [to.tip]), edge[0] + '-' + edge[1], false);
+ });
+ graph.querySelectorAll('[data-input-component]').forEach(function(input) {
+ var target = nodes[input.getAttribute('data-input-component')];
+ if (!target) return;
+ var r = box(input, bounds, 0), x = (target.left + target.right) / 2;
+ arrow(svg, [[x, r.bottom], [x, target.top]], 'input-' + input.getAttribute('data-input-component'), true);
+ });
+ }
+ function schedule() {
+ if (scheduled) return;
+ scheduled = true;
+ window.requestAnimationFrame(function() {
+ scheduled = false;
+ root.querySelectorAll('.tam-model-graph').forEach(draw);
+ });
+ }
+ var selector = root.querySelector('[data-model-selector]');
+ if (selector) selector.addEventListener('change', function() {
+ var value = selector.value;
+ root.querySelectorAll('[data-model-pane]').forEach(function(pane) {
+ pane.style.display = pane.getAttribute('data-model-pane') === value ? '' : 'none';
+ });
+ schedule();
+ });
+ if (window.ResizeObserver) {
+ var resize = new window.ResizeObserver(schedule);
+ resize.observe(root);
+ root.querySelectorAll('.tam-model-graph').forEach(function(graph) { resize.observe(graph); });
+ }
+ // Dashboard tabs change ancestor classes/styles without a window resize.
+ if (window.MutationObserver) {
+ var visibility = new window.MutationObserver(schedule);
+ for (var parent = root.parentElement; parent; parent = parent.parentElement) {
+ visibility.observe(parent, {attributes: true, attributeFilter: ['class', 'style', 'hidden']});
+ }
+ }
+ window.addEventListener('resize', schedule);
+ if (document.fonts && document.fonts.ready) document.fonts.ready.then(schedule);
+ schedule();
+})();
diff --git a/inst/js/residual-selection.js b/inst/js/residual-selection.js
index 8bed14f1..0727ae32 100644
--- a/inst/js/residual-selection.js
+++ b/inst/js/residual-selection.js
@@ -2,6 +2,7 @@ function(el, x, config) {
'use strict';
function array(value) { return Array.isArray(value) ? value : [value]; }
config.frames = array(config.frames);
+ config.defaultViews = array(config.defaultViews);
config.views.forEach(function(view) {
view.cells.forEach(function(cell) {
cell.data.forEach(function(trace) {
@@ -12,23 +13,28 @@ function(el, x, config) {
});
});
});
- var state = {view: config.view, frame: config.frame}, busy = false, pending = false;
+ var state = {view: config.view, frame: config.frame, automatic: config.automatic}, busy = false, pending = false;
+ function selectedView(frame) {
+ return state.automatic ? config.defaultViews[frame] : state.view;
+ }
function draw() {
if (busy) { pending = true; return; }
busy = true;
- var view = config.views[state.view], cell = view.cells[state.frame];
+ state.view = selectedView(state.frame);
+ var cell = config.views[state.view].cells[state.frame];
var frames = config.frames.map(function(name, i) {
- return {name: name, data: view.cells[i].data, layout: view.cells[i].layout,
- traces: view.cells[i].data.map(function(trace, j) { return j; })};
+ var frameCell = config.views[selectedView(i)].cells[i];
+ return {name: name, data: frameCell.data, layout: frameCell.layout,
+ traces: frameCell.data.map(function(trace, j) { return j; })};
});
var layout = Object.assign({}, x.layout, cell.layout, {
- autosize: true, margin: Object.assign({}, cell.layout.margin, {t: 100}),
+ autosize: true, margin: Object.assign({}, cell.layout.margin, {t: config.showSelector ? 100 : 60}),
// Leave the upper-right modebar clear and reserve a separate method row.
- updatemenus: [{name: 'tam-residual-type', type: 'dropdown',
+ updatemenus: config.showSelector ? [{name: 'tam-residual-type', type: 'dropdown',
x: 0, y: 1, xanchor: 'left', yanchor: 'bottom', pad: {b: 28}, active: state.view,
- buttons: ['Auto', 'Standardized', 'OSA'].map(function(label, i) {
+ buttons: ['Standardized', 'OSA'].map(function(label, i) {
return {label: label, method: 'skip', execute: false, args: [i]};
- })}],
+ })}] : [],
sliders: config.animated ? [{active: state.frame, pad: {t: 45},
currentvalue: {prefix: config.prefix}, steps: config.frames.map(function(name) {
return {label: name, method: 'animate', args: [[name],
@@ -50,12 +56,17 @@ function(el, x, config) {
el.on('plotly_buttonclicked', function(event) {
if (!event.menu || event.menu.name !== 'tam-residual-type') return;
state.view = event.button.args[0];
+ state.automatic = false;
draw();
});
el.on('plotly_animatingframe', function(event) {
var index = config.frames.indexOf(event.name);
if (index >= 0) {
state.frame = index;
+ if (state.automatic) {
+ state.view = config.defaultViews[index];
+ if (!busy && config.showSelector) Plotly.relayout(el, {'updatemenus[0].active': state.view});
+ }
if (busy) pending = true;
}
});
diff --git a/inst/rmd/model_structure.Rmd b/inst/rmd/model_structure.Rmd
index b46981ab..efeba86d 100644
--- a/inst/rmd/model_structure.Rmd
+++ b/inst/rmd/model_structure.Rmd
@@ -1,8 +1,28 @@
Model structure
================================================================================
-### Model structure
+Row {.tabset}
+--------------------------------------------------------------------------------
+
+### Diagram
```{r}
plot_model(model_list = fits)
```
+
+### Function calls
+
+```{r, results = "asis"}
+call_fits <- Filter(function(fit) inherits(fit, "tam_fit"), fits)
+calls <- lapply(call_fits, `[[`, "call")
+if (length(calls)) {
+ calls_parsed <- sapply(calls, deparse1, width.cutoff = 50L, collapse = "\n")
+ calls_html <- paste("\n",
+ htmltools::htmlEscape(calls_parsed),
+ "\n
")
+ calls_df <- data.frame(Model = htmltools::htmlEscape(names(calls)), Call = calls_html)
+ cat('\n\n')
+ print(knitr::kable(calls_df, format = "html", escape = FALSE, col.names = NULL))
+ cat('\n
\n\n')
+}
+```
diff --git a/inst/rmd/tables.Rmd b/inst/rmd/tables.Rmd
index 0be82381..ac1b5b8e 100644
--- a/inst/rmd/tables.Rmd
+++ b/inst/rmd/tables.Rmd
@@ -50,30 +50,6 @@ if (!is.null(obs_stack$pop_covariates)) {
```
-Settings {data-navmenu="Tables"}
-================================================================================
-
-Row {.tabset}
---------------------------------------------------------------------------------
-
-### Function calls
-
-```{r, results = "asis"}
-call_fits <- Filter(function(fit) inherits(fit, "tam_fit"), fits)
-calls <- lapply(call_fits, `[[`, "call")
-if (length(calls)) {
- calls_parsed <- sapply(calls, deparse1, width.cutoff = 50L, collapse = "\n")
- calls_html <- paste("\n",
- calls_parsed,
- "\n
")
- calls_df <- data.frame(Model = names(calls), Call = calls_html)
- cat('\n\n')
- print(knitr::kable(calls_df, format = "html", escape = FALSE, col.names = NULL))
- cat('\n
\n\n')
-}
-```
-
-
Output {data-navmenu="Tables"}
================================================================================
diff --git a/inst/rmd/vis_tam.Rmd b/inst/rmd/vis_tam.Rmd
index 20104eaa..67a6f36b 100644
--- a/inst/rmd/vis_tam.Rmd
+++ b/inst/rmd/vis_tam.Rmd
@@ -152,6 +152,9 @@ table_link <- function(x, table_name = NULL, download_text = "Download table") {
```
+```{r model-structure, child = "model_structure.Rmd"}
+```
+
```{r background, child = if (is.null(background)) NULL else "background.Rmd"}
```
@@ -179,12 +182,3 @@ table_link <- function(x, table_name = NULL, download_text = "Download table") {
```{r tables, child = "tables.Rmd"}
```
-```{r model-structure, child = "model_structure.Rmd"}
-```
-
-
-
-
-
-
-
diff --git a/man/check_obs.Rd b/man/check_obs.Rd
deleted file mode 100644
index b6b550ca..00000000
--- a/man/check_obs.Rd
+++ /dev/null
@@ -1,22 +0,0 @@
-% Generated by roxygen2: do not edit by hand
-% Please edit documentation in R/data.R
-\name{check_obs}
-\alias{check_obs}
-\title{Check the structure of a TAM obs list}
-\usage{
-check_obs(obs, quiet = FALSE)
-}
-\arguments{
-\item{obs}{A named list of tinyAM input tables.}
-
-\item{quiet}{Logical. Suppress the success summary? Defaults to \code{FALSE}.}
-}
-\value{
-\code{TRUE} (invisibly) if validation passes; otherwise aborts.
-}
-\description{
-A convenience wrapper for \code{\link[=check_tam_data]{check_tam_data()}}.
-}
-\examples{
-check_tam_data(cod_obs)
-}
diff --git a/man/check_tam.Rd b/man/check_tam.Rd
index 0f004856..f7917d3b 100644
--- a/man/check_tam.Rd
+++ b/man/check_tam.Rd
@@ -20,7 +20,7 @@ through named \code{...} arguments or \code{model_list}.}
unnamed models use their expression names. Unused by the print methods.}
\item{grad_tol}{Positive gradient tolerance. \code{NULL} uses the tolerance stored
-in the fit, or \code{1e-2} if none is stored.}
+in the fit.}
\item{detailed}{Examine covariance eigenvalues and weak parameter directions?}
diff --git a/man/fit_retro.Rd b/man/fit_retro.Rd
index 002181cf..b8360127 100644
--- a/man/fit_retro.Rd
+++ b/man/fit_retro.Rd
@@ -120,8 +120,17 @@ Missing baselines or zero persistence error make MASE unavailable. Failed
numerical fits remain in \code{fits}, but not in stacked results or scores.
Supplied fits and fitted objectives are unchanged.
}
+\section{Parallel execution}{
+
+Set the execution plan before calling either function, for example
+\code{future::plan(future::multisession, workers = 4)}. Without a parallel plan,
+folds run sequentially. Neither function changes your plan or worker count.
+Use \code{future::plan(future::sequential)} afterwards to shut down the workers.
+}
+
\examples{
\dontrun{
+future::plan(future::multisession, workers = 4)
retro <- fit_retro(fit, alternative = other_fit, folds = 5)
hind <- fit_hindcast(fit, folds = 10,
proj_settings = list(n_proj = 3, n_mean = 3, catch = "observed"))
@@ -129,6 +138,7 @@ print(hind)
hind$scores
vis_tam(hind)
vis_tam(retro, model = names(retro$fits)[1])
+future::plan(future::sequential)
}
}
\seealso{
diff --git a/man/fit_tam.Rd b/man/fit_tam.Rd
index 888778b9..33955efe 100644
--- a/man/fit_tam.Rd
+++ b/man/fit_tam.Rd
@@ -65,8 +65,9 @@ RW residuals require independent temporal mean contrasts; constant
directions cancel and are rejected. See \link{tinyAM-model}.
\item \code{sd_form}: ordinary formula for log SD of older-age survival residuals,
default \code{~ 1}. Use \code{~ factor(age)} or a supplied age-sharing column in
-\code{data$pop_covariates}. Its design must be constant across years. Recruitment and
-initial-abundance SDs remain separate. Random-effect SD terms are not enabled.
+\code{data$pop_covariates}. Its design must be constant across years. Recruitment
+SD is controlled separately by \code{rec_settings}; initial abundance has no
+separate SD. Random-effect SD terms are not enabled.
}}
\item{rec_settings}{A list controlling recruitment separately from cohort survival:
@@ -273,7 +274,9 @@ retain its results, use \code{\link[=add_osa_residuals]{add_osa_residuals()}}.}
\item{start_par}{Optional fitted \code{tam_fit} or list of starting parameter values, matching the
structure returned by \code{\link[=make_par]{make_par()}}. Useful for warm starts or retrospective
runs where parameter estimates from a previous fit are reused to improve
-convergence and speed. Non-matching or missing entries are ignored.}
+convergence and speed. Non-matching or missing entries are ignored.
+Fitted-object starts require the current stored parameter modes; regenerate
+older saved fits. Catchability coefficients are omitted if \code{q_link} changes.}
\item{grad_tol}{Numeric tolerance passed to \code{\link[=check_tam]{check_tam()}} when
evaluating the fitted object's gradients. Defaults to \code{1e-2}.}
@@ -316,36 +319,18 @@ RTMB integrates random effects using the Laplace approximation; \code{nlminb}
optimizes the remaining fixed effects. \code{\link[=mono]{mono()}} increments \code{dq} and \code{dmu_f} have a zero
lower bound, as do initial-abundance decrements \code{d_n0}. Their SEs remain on the increment scale, but symmetric Wald
intervals are only local approximations at a boundary.
-Set \code{index_settings$q_link = "logit"} to restrict the full catchability
-prediction to between zero and one; the default \code{"log"} allows q above one.
-Formula coefficients are then named \code{logit_q}, and \code{\link[=mono]{mono()}} increments
-act on logit-q. Starting q coefficients must use the selected link scale:
-old \code{log_q} starts are not reused as \code{logit_q}. See \code{\link[=prepare_tam]{prepare_tam()}} and \code{\link[=tidy_par]{tidy_par()}}.
-
-Random-effect blocks are chosen automatically from the model settings:
-\itemize{
-\item Includes \code{log_f} when the F process is active, and eligible \code{log_r} states when recruitment is stochastic. Early recruitment
-anchors are fixed; see \link{recruitment_formulas}.
-\item Includes \code{log_n0} only if \code{N_settings$init == "latent"}.
-\item Includes \code{log_n} if \code{N_settings$process != "off"}.
-\item Includes \code{log_m} if \code{M_settings$process != "off"}.
-\item Includes the unique \verb{eta_q_*} states of Gaussian catchability formula terms.
-\item Includes Gaussian F/M mean and catch/index log-SD formula states.
-Logistic midpoint/slope and process SD/correlation are fixed effects.
-}
+Random-effect blocks are chosen from the active processes and Gaussian
+formula effects. Initial abundance and recruitment boundaries are integrated
+only when their respective \code{init} setting is \code{"latent"}; see \link{tinyAM-model}
+and \link{recruitment_formulas}. \code{\link[=make_par]{make_par()}} describes the parameter layout.
A warning is issued if the number of random effects exceeds 1.5 times the
number of observed data points (rough identifiability check).
-For F random walks, \code{\link[=prepare_tam]{prepare_tam()}} removes time-invariant mean columns
-because they cancel from all increments. For M random walks, mean coefficients
-with no effect on either increments or mortality outside the fitted process
-states are held at their starting values.
-An RW SD is also held fixed if the process has only one row (no increments).
-These parameters cannot be estimated from the likelihood; no constraint is
-imposed on the latent states themselves.
-For AR1, the correlation for an axis containing a single row or column is
-fixed at zero; otherwise it would be confounded with the SD.
+Parameters without likelihood support are removed or fixed: examples include
+constant mean directions canceled by RW increments, RW SDs with no increments,
+and AR1 correlations on singleton axes. The latent states themselves retain
+the conventions in \link{tinyAM-model}.
\code{is_converged} summarizes optimizer, gradient and uncertainty checks. Also inspect
\code{opt$convergence} and \code{opt$message}; a passing numerical check does not
diff --git a/man/nll_fun.Rd b/man/nll_fun.Rd
index d7974ad9..cde53d91 100644
--- a/man/nll_fun.Rd
+++ b/man/nll_fun.Rd
@@ -4,8 +4,6 @@
\alias{nll_fun}
\title{Evaluate or simulate the tinyAM model}
\usage{
-nll_fun(par, dat, simulate = FALSE)
-
nll_fun(par, dat, simulate = FALSE)
}
\arguments{
@@ -77,11 +75,6 @@ F scales terminal historical F by a supplied multiplier or a multiplier solved
from projected catch biomass. Both paths use the same chronological population.
Active M states already extend into projected years under their process density.
}
-\section{Simulate from fitted parameters}{
-p_hat <- as.list(sdrep, "Estimate")
-sims <- nll_fun(p_hat, dat, simulate = TRUE)
-}
-
\examples{
dat <- prepare_tam(
cod_obs,
@@ -101,7 +94,4 @@ head(exp(simulated$log_r))
}
\seealso{
\link{tinyAM-model}, \code{\link[=prepare_tam]{prepare_tam()}}, \code{\link[=make_par]{make_par()}}, \code{\link[=fit_tam]{fit_tam()}}, \code{\link[=sim_tam]{sim_tam()}}
-
-\code{\link[=prepare_tam]{prepare_tam()}}, \code{\link[=make_par]{make_par()}}, \code{\link[=fit_tam]{fit_tam()}}, \code{\link[=sim_tam]{sim_tam()}},
-\code{\link[=dprocess_ar1]{dprocess_ar1()}}, \code{\link[=rprocess_ar1]{rprocess_ar1()}}
}
diff --git a/man/plot_age_year.Rd b/man/plot_age_year.Rd
index 63098153..22901da3 100644
--- a/man/plot_age_year.Rd
+++ b/man/plot_age_year.Rd
@@ -1,5 +1,5 @@
% Generated by roxygen2: do not edit by hand
-% Please edit documentation in R/plot.R
+% Please edit documentation in R/plot-trends.R
\name{plot_age_year}
\alias{plot_age_year}
\title{Switch between annual trends and age profiles}
diff --git a/man/plot_matrix.Rd b/man/plot_matrix.Rd
index 6a3ec36c..04a92f22 100644
--- a/man/plot_matrix.Rd
+++ b/man/plot_matrix.Rd
@@ -1,5 +1,5 @@
% Generated by roxygen2: do not edit by hand
-% Please edit documentation in R/plot.R
+% Please edit documentation in R/plot-matrix.R
\name{plot_matrix}
\alias{plot_matrix}
\title{Explore age-year patterns}
diff --git a/man/plot_model.Rd b/man/plot_model.Rd
index b745775f..34f72eca 100644
--- a/man/plot_model.Rd
+++ b/man/plot_model.Rd
@@ -13,7 +13,7 @@ or one named list of these objects.}
\item{model_list}{A named list of models, instead of \code{...}.}
}
\value{
-A browsable HTML container with a responsive SVG diagram and model
+A browsable HTML container with responsive HTML nodes, SVG connectors and model
settings. Display it directly, or save it with \code{\link[htmltools:save_html]{htmltools::save_html()}}.
}
\description{
diff --git a/man/plot_tam.Rd b/man/plot_tam.Rd
index 4da0b5e6..ab8d27b1 100644
--- a/man/plot_tam.Rd
+++ b/man/plot_tam.Rd
@@ -1,16 +1,39 @@
% Generated by roxygen2: do not edit by hand
-% Please edit documentation in R/plot.R
+% Please edit documentation in R/plot-matrix.R, R/plot-parameters.R,
+% R/plot-resid.R, R/plot-trends.R, R/plot-utils.R
\name{plot_tam}
\alias{plot_tam}
-\alias{plot_trend}
-\alias{add_plot_buttons}
\alias{plot_heatmap}
-\alias{plot_obs_pred}
+\alias{plot_par}
\alias{plot_bubbles}
\alias{plot_resid}
-\alias{plot_par}
+\alias{plot_trend}
+\alias{plot_obs_pred}
+\alias{add_plot_buttons}
\title{Plot utilities for \code{tidy_tam()} outputs}
\usage{
+plot_heatmap(data, zlab = "Estimate", title = "", ...)
+
+plot_par(data, ...)
+
+plot_bubbles(
+ data,
+ res = NULL,
+ y = ~age,
+ ylab = "Age",
+ title = NULL,
+ residual_type = c("auto", "standardized", "osa"),
+ ...
+)
+
+plot_resid(
+ data,
+ x = ~year,
+ xlab = "Year",
+ residual_type = c("auto", "standardized", "osa"),
+ ...
+)
+
plot_trend(
data,
x = ~year,
@@ -23,10 +46,6 @@ plot_trend(
...
)
-add_plot_buttons(p)
-
-plot_heatmap(data, zlab = "Estimate", title = "", ...)
-
plot_obs_pred(
data,
ylab = "",
@@ -37,60 +56,43 @@ plot_obs_pred(
...
)
-plot_bubbles(
- data,
- res = NULL,
- y = ~age,
- ylab = "Age",
- title = NULL,
- residual_type = c("auto", "standardized", "osa"),
- ...
-)
-
-plot_resid(
- data,
- x = ~year,
- xlab = "Year",
- residual_type = c("auto", "standardized", "osa"),
- ...
-)
-
-plot_par(data, ...)
+add_plot_buttons(p)
}
\arguments{
\item{data}{A data frame of observations or estimates.}
-\item{x}{Formula specifying x variable}
-
-\item{y}{Formula specifying y variable}
-
-\item{xlab}{Label for the x-axis.}
-
-\item{ylab}{Label for the y-axis.}
+\item{zlab}{Label for the colorbar in heatmaps.}
\item{title}{Plot title.}
-\item{add_intervals}{Logical; if \code{TRUE}, show confidence intervals when \code{lwr} and \code{upr} are present.}
-
-\item{add_buttons}{Logical; if \code{TRUE}, include buttons to toggle y-axis between Linear / Log and, if
-applicable, Show CI / Hide CI.}
-
\item{...}{Additional arguments to pass to \code{\link[plotly:plot_ly]{plotly::plot_ly()}} such as \code{color}, \code{colors}, etc.}
-\item{p}{A Plotly object, with all desired layers already added.}
-
-\item{zlab}{Label for the colorbar in heatmaps.}
-
\item{res}{Optional one-column formula for signed bubble values, e.g.
\code{~est} for process deviations. Omitted values use OSA or standardized
observation residuals. Use \code{y = ~age_block} for shared M state blocks.}
+\item{y}{Formula specifying y variable}
+
+\item{ylab}{Label for the y-axis.}
+
\item{residual_type}{Initial observation residual view: \code{"auto"} (default),
-\code{"standardized"} or \code{"osa"}. Auto uses available cached OSA residuals
-separately for each model in the supplied panel, otherwise standardized
-residuals. Partial OSA gaps remain missing. A dropdown switches views;
+\code{"standardized"} or \code{"osa"}. The default uses retained OSA residuals
+when available, otherwise standardized residuals. Model frames choose
+separately; overlays use OSA only when all models have them. A dropdown
+offers OSA and Standardized when OSAs are available. Partial gaps stay missing;
unavailable OSA values are identified without computing them. A supplied
\code{res} formula in \code{plot_bubbles()} bypasses these observation controls.}
+
+\item{x}{Formula specifying x variable}
+
+\item{xlab}{Label for the x-axis.}
+
+\item{add_intervals}{Logical; if \code{TRUE}, show confidence intervals when \code{lwr} and \code{upr} are present.}
+
+\item{add_buttons}{Logical; if \code{TRUE}, include buttons to toggle y-axis between Linear / Log and, if
+applicable, Show CI / Hide CI.}
+
+\item{p}{A Plotly object, with all desired layers already added.}
}
\value{
A Plotly object.
diff --git a/man/prepare_tam.Rd b/man/prepare_tam.Rd
index cc1197ee..a2b4a4d8 100644
--- a/man/prepare_tam.Rd
+++ b/man/prepare_tam.Rd
@@ -56,8 +56,9 @@ RW residuals require independent temporal mean contrasts; constant
directions cancel and are rejected. See \link{tinyAM-model}.
\item \code{sd_form}: ordinary formula for log SD of older-age survival residuals,
default \code{~ 1}. Use \code{~ factor(age)} or a supplied age-sharing column in
-\code{data$pop_covariates}. Its design must be constant across years. Recruitment and
-initial-abundance SDs remain separate. Random-effect SD terms are not enabled.
+\code{data$pop_covariates}. Its design must be constant across years. Recruitment
+SD is controlled separately by \code{rec_settings}; initial abundance has no
+separate SD. Random-effect SD terms are not enabled.
}}
\item{rec_settings}{A list controlling recruitment separately from cohort survival:
@@ -290,126 +291,40 @@ function automatically; call it directly to inspect a model specification.
Supply only the entries to change from the defaults shown in Usage. Formulas
and vectors replace an entire entry. Optional \code{NULL} entries clear a formula,
-offset or age selection; recruitment defaults to an anchored RW and a latent
-\code{sd_form = NULL} restores \code{~ 1}. Required entries cannot be \code{NULL}.
+offset or age selection; \code{sd_form = NULL} restores \code{~ 1}.
+Required entries cannot be \code{NULL}.
\code{\link[=update]{update()}} retains unspecified entries from the original fit instead.
-\strong{Spawning time}
+\strong{Inputs and preparation}
-An optional \code{data$maturity$spawn_time} column supplies spawning timing as a
-fraction of the year for each year and age. If absent, timing is zero
-(start-of-year SSB). If present, every value must be finite and between zero
-and one, including immature and retained older biological ages. Projections
-carry forward the terminal historical value at each biological age.
-SSB includes survival through this fraction of annual F and M. The same
-definition supplies recruitment parents and uncertainty. Age-specific timing
-sums each age's contribution at its own spawning time; it is not a snapshot
-on one common date. Hidden plus-age contributions are calculated separately
-before summing. Weight and maturity remain supplied quantities; seasonal
-growth and maturation are not modeled.
+See \code{\link[=as_tam_data]{as_tam_data()}} for table keys, units, fleet-specific catch weights and
+covariate routing, and \link{cod_obs} for example data. Catch and survey zeros and
+missing values are excluded from the lognormal observation likelihood;
+complete predictions are still calculated.
-\strong{Observation handling}
-\itemize{
-\item Inputs are expected as a list with components \code{catch}, \code{index}, \code{weight},
-and \code{maturity}. Each must include columns \code{year}, \code{age}, and a value column
-named \code{obs}. Rename any differently named input columns before calling. See
-\link{cod_obs} for an example of the required structure. The optional
-\code{pop_covariates} table supplies population formula inputs; see \code{\link[=as_tam_data]{as_tam_data()}}.
-\item Catch and maturity must already contain exactly one row per
-year-age combination across the input range. Survey tables may be sparse.
-Weight needs the same complete grid for every supplied \code{type}; see
-\code{\link[=as_tam_data]{as_tam_data()}} for stock and catch weights and fallback rules.
-\item A combined observation table is created for catch and index; \code{log(0)} is
-excluded from the observation likelihood.
-\item When biological inputs extend above the modeled plus age, the underlying
-stock/catch weight and maturity values are retained in \code{W_plus_input},
-\code{W_catch_plus_input} and \code{P_plus_input},
-indexed by \code{years} and \code{plus_ages}. Their projection rows use the same
-recent-year averages by biological age as other weight/maturity inputs.
-\code{dat$W}, \code{dat$W_catch}, \code{dat$P}, and \code{dat$obs} retain input values at modeled ages;
-their terminal biological values are placeholders. \code{\link[=nll_fun]{nll_fun()}} replaces
-terminal W by an abundance-weighted mean and terminal P by a biomass-weighted
-proportion using its reconstructed hidden age composition. See \link{tinyAM-model}.
-Formula covariates continue to use the input rows at modeled ages, not these
-effective biological values; this avoids circular mortality calculations.
-}
+Biological ages above the modeled plus age are retained in \code{W_plus_input},
+\code{W_catch_plus_input} and \code{P_plus_input}. The population calculation derives
+effective plus-group weights and maturity from their hidden abundances;
+prepared terminal biological values are placeholders. Formula covariates
+use the supplied modeled-age rows, avoiding circular mortality calculations.
+The equations and spawning-time definition are in \link{tinyAM-model}.
-\strong{Design matrices}
-\itemize{
-\item \code{catch_settings$sd_form} is evaluated on the catch table to produce
-\code{sd_catch_modmat} and associated \strong{log-scale} parameters \code{log_sd_catch}
-(intercept removed when \code{sd_supplied} is provided so supplied SDs act as
-offsets).
-\item \code{index_settings$sd_form} is evaluated on the index table to produce
-\code{sd_index_modmat} and \strong{log-scale} parameters \code{log_sd_index}.
-Both SD formulas support additive \code{\link[=iid]{iid()}}, \code{\link[=rw]{rw()}}, \code{\link[=ar1]{ar1()}} and random
-intercepts; their compact unique states are stored in \code{sd_catch_terms}
-and \code{sd_index_terms}. They modify log observation SD, with supplied SDs
-acting as offsets. See \link{formula_effects} for interpretation and support checks.
-\item \code{index_settings$q_form} is evaluated on the index table to produce
-\code{q_modmat} and parameters \code{log_q} (log link) or \code{logit_q} (logit link).
-Additive \code{\link[=mono]{mono()}} terms instead contribute cumulative indicators
-in \code{q_mono_modmat}, with directly fitted non-negative increments \code{dq}
-on the selected link scale.
-\code{q_mono_steps} records each transition and its optional group.
-Structured \code{\link[=iid]{iid()}}, \code{\link[=rw]{rw()}}, \code{\link[=ar1]{ar1()}}, random-intercept and \code{\link[=logistic]{logistic()}}
-terms are recorded separately in \code{q_terms}. Their unique states and process
-parameters are added by \code{\link[=make_par]{make_par()}}, rather than treated as ordinary columns.
-\item If \code{M_settings$mu_form} is provided, \code{M_modmat <- model.matrix(mu_form, data = data$pop_covariates)} and the resulting coefficients are parameters \code{mu_m}.
-These coefficients are applied on the log scale to build \eqn{M}, but
-retain the \code{mu_} prefix to emphasize they can be positive or negative.
-If \code{mu_supplied} is also provided, the intercept in \code{mu_form} is dropped
-and a warning is issued.
-\item If neither \code{M_settings$mu_form} nor \code{M_settings$mu_supplied} is supplied,
-the function stops, because \eqn{M} must be identified by either a supplied
-surface or a mean structure.
-}
+\strong{Formula designs}
-\strong{Process options and guards}
-\itemize{
-\item Initial abundance is controlled by \code{N_settings$init}, independently of
-the subsequent N process. First-year recruitment is the state
-\code{log_r0}, controlled separately by \code{rec_settings$init}. The default uses survivorship from recruitment under first-year
-total mortality Z, with no equilibrium plus-group correction.
-\item \code{"exp"} is the default: a parsimonious, comparatively stable nuisance-state
-initialization. More flexible choices are available when first-year data
-support them. \code{"free"} estimates unpenalized fixed \code{log_n0} states.
-\code{"latent"} integrates those states without an initial density, including
-when the subsequent N process is off. It resembles SAM's unpenalized
-older-age boundary; use \code{rec_settings$init = "latent"} to integrate first-year recruitment too.
-\item \code{"mono"} estimates non-increasing initial abundance from r0, including
-the plus group: \verb{log N0[a] = log r0 - cumsum(d_n0)[a-1]}. \code{"capped"}
-only requires each older abundance to be no greater than r0:
-\verb{log N0[a] = log r0 - d_n0[a-1]}. Both use nonnegative log-decrements,
-allow exact ties, and require no N process or separate initial SD.
-The pooled plus group can naturally exceed a single age; constraining it
-is a deliberate biological assumption. Boundary Wald intervals need care.
-\item Recruitment initialization is independent of these choices. Simulations condition on initial states;
-joint uncertainty draws can vary fitted latent initial states. No
-mortality-history distribution is imposed on initial abundances.
-\item \code{M_settings$age_breaks} (vector of break points on ages)
-defines \code{M_settings$age_blocks} via \code{\link[=cut_ages]{cut_ages()}}, used
-to share absolute latent \eqn{M} states across ages. All mean components
-must be constant across ages within each fitted block.
-\item The AR(1) correlation parameters are only initialized for
-processes whose \code{process == "ar1"}. Correlations are assumed to be 0
-when \code{process == "iid"}. A temporal random walk (\code{"rw"}) has independent
-year-to-year increments within each age or age block, with no age
-correlation or AR parameter. Its first process row is unpenalized.
-For N the walk acts on cohort residuals; for F/M it acts on deviations
-from their existing log mean surfaces. Initial abundance, the M process
-start year, and terminal-F projections retain their usual meanings.
-}
+Ordinary formula terms create design matrices. Structured \link{formula_effects},
+\code{\link[=mono]{mono()}} curves and \link{smooth_formulas} also store unique effect states and
+their grouping metadata. \code{\link[=make_par]{make_par()}} constructs the associated parameters.
+Supplied M and observation SDs act as offsets; their mean/SD formulas omit
+an intercept to avoid estimating another baseline. The settings below define
+available terms, restrictions and data sources.
-\strong{Projections (optional)}
-\itemize{
-\item If \code{proj_settings} is supplied, the function adds:
-\itemize{
-\item \code{proj_years}: the set of projection years;
-\item \code{is_proj}: logical vector identifying the projection years;
-\item \code{obs$...$is_proj} rows to each \code{obs} table identifying the projection years.
-}
-}
+\strong{Projections}
+
+\code{proj_settings} appends future years and marks them with \code{is_proj} in the
+prepared grid and tables. Stock/catch weights and maturity use recent-year
+averages by biological age; covariates and spawning timing retain terminal
+historical values. See \link{tinyAM-model} for forecasting and initial-state
+equations, and \code{\link[=fit_hindcast]{fit_hindcast()}} for held-out-data evaluation.
}
\examples{
dat <- prepare_tam(
diff --git a/man/process_ar1.Rd b/man/process_ar1.Rd
index f403e4b4..e0bf66fb 100644
--- a/man/process_ar1.Rd
+++ b/man/process_ar1.Rd
@@ -61,5 +61,5 @@ dprocess_ar1(X_ar1, sd = 0.3, phi = c(0.5, 0.9))
}
\seealso{
-\code{\link[RTMB:dseparable]{RTMB::dseparable()}}, \code{\link[RTMB:dautoreg]{RTMB::dautoreg()}}, \code{\link[MASS:mvrnorm]{MASS::mvrnorm()}}
+\code{\link[RTMB:MVgauss]{RTMB::dseparable()}}, \code{\link[RTMB:MVgauss]{RTMB::dautoreg()}}, \code{\link[MASS:mvrnorm]{MASS::mvrnorm()}}
}
diff --git a/man/stack_list.Rd b/man/stack_list.Rd
index 13600248..50544755 100644
--- a/man/stack_list.Rd
+++ b/man/stack_list.Rd
@@ -26,7 +26,7 @@ A single data frame produced by row-binding the list elements. If
output.
}
\description{
-Convenience wrapper around \code{\link[base:rbind]{base::rbind()}} for stacking a (named) list of
+Convenience wrapper around \code{\link[base:cbind]{base::rbind()}} for stacking a (named) list of
data frames while recording the source list element in a label column. When
the list is named, the names are used as labels; otherwise, integer indices
(\code{"1"}, \code{"2"}, …) are used. Columns absent in some elements are filled with
diff --git a/man/tam_cols.Rd b/man/tam_cols.Rd
index 2a1da935..19854c9c 100644
--- a/man/tam_cols.Rd
+++ b/man/tam_cols.Rd
@@ -29,5 +29,5 @@ tam_pal(3)
}
\seealso{
-\code{\link[grDevices:colorRampPalette]{grDevices::colorRampPalette()}}
+\code{\link[grDevices:colorRamp]{grDevices::colorRampPalette()}}
}
diff --git a/man/tidy_array.Rd b/man/tidy_array.Rd
index 9fee5058..ba6e5ad3 100644
--- a/man/tidy_array.Rd
+++ b/man/tidy_array.Rd
@@ -2,12 +2,9 @@
% Please edit documentation in R/tidy.R
\name{tidy_array}
\alias{tidy_array}
-\alias{tidy_mat}
\title{Tidy a (named) matrix/array into long format}
\usage{
tidy_array(x, value_name = "x", require_dimnames = TRUE)
-
-tidy_mat(x, value_name = "x", require_dimnames = TRUE)
}
\arguments{
\item{x}{A matrix or array. Works with 2D or higher dimensions.}
@@ -27,7 +24,7 @@ one column per dimension (using the dimension names if available), and a
value column.
}
\details{
-Internally uses \code{\link[base:as.data.frame.table]{base::as.data.frame.table()}} to reshape \code{x}. After reshaping,
+Internally uses \code{\link[base:table]{base::as.data.frame.table()}} to reshape \code{x}. After reshaping,
all \strong{dimension columns} are passed through \code{\link[utils:type.convert]{utils::type.convert()}} with
\code{as.is = TRUE}, so numeric-like labels (e.g., \code{"1"}, \code{"2.5"}) become numeric.
}
diff --git a/man/tidy_par.Rd b/man/tidy_par.Rd
index 474594fb..a6a2e268 100644
--- a/man/tidy_par.Rd
+++ b/man/tidy_par.Rd
@@ -94,7 +94,7 @@ initialization, derived \code{n0} estimates use joint RTMB uncertainty.
\item For matrices (e.g., \code{log_f}, \code{log_n}), \code{year} and/or \code{age} columns are added
from their dimensions; shared F/M states use \code{age_block} rather than
duplicating the parameter at each biological age.
-via \code{\link[=tidy_mat]{tidy_mat()}}.
+via \code{\link[=tidy_array]{tidy_array()}}.
}
}
\examples{
@@ -117,5 +117,5 @@ head(par_tab$random$log_f)
}
\seealso{
-\code{\link[=tidy_mat]{tidy_mat()}}, \code{\link[=fit_tam]{fit_tam()}}
+\code{\link[=tidy_array]{tidy_array()}}, \code{\link[=fit_tam]{fit_tam()}}
}
diff --git a/man/tidy_rep.Rd b/man/tidy_rep.Rd
index 02944fd1..9016027d 100644
--- a/man/tidy_rep.Rd
+++ b/man/tidy_rep.Rd
@@ -35,5 +35,5 @@ head(trends$N)
}
\seealso{
-\code{\link[=tidy_obs_pred]{tidy_obs_pred()}}, \code{\link[=tidy_sdrep]{tidy_sdrep()}}, \code{\link[=tidy_pop]{tidy_pop()}}, \code{\link[=tidy_mat]{tidy_mat()}}
+\code{\link[=tidy_obs_pred]{tidy_obs_pred()}}, \code{\link[=tidy_sdrep]{tidy_sdrep()}}, \code{\link[=tidy_pop]{tidy_pop()}}, \code{\link[=tidy_array]{tidy_array()}}
}
diff --git a/man/tinyAM-model.Rd b/man/tinyAM-model.Rd
index 4eb968e3..06e4a886 100644
--- a/man/tinyAM-model.Rd
+++ b/man/tinyAM-model.Rd
@@ -14,7 +14,7 @@ Choose process assumptions to express biological hypotheses. \code{"iid"} allows
independent departures, \code{"ar1"} allows departures to be similar in adjacent
years and ages, and \code{"rw"} allows departures to accumulate over time without
returning to a mean. These choices make different assumptions, even when
-they produce similar fitted curves. N and M can also have their process
+they produce similar fitted curves. N, F and M can also have their process
switched \code{"off"}. Recruitment defaults to a temporal random walk;
\code{rec_settings$mu_form} can describe stock-recruit relationships and covariates.
@@ -63,10 +63,6 @@ states. Full surfaces \code{log_N}, \code{log_F}, and \code{log_M} are construct
them by recursion, age-block expansion, and projection. An \verb{eta_*} quantity
is a deviation from an expected log state, never an additional absolute state.
All matrices have years in rows. Vectorization stacks columns, as in R.
-
-Numbers and weights must use consistent units. There is no automatic unit
-conversion. For \link{cod_obs}, numbers in thousands and weights in kg give
-biomass in tonnes. Survey catchability absorbs differences in survey units.
}
\subsection{Recruitment and initial abundance}{
@@ -97,6 +93,9 @@ For \code{"mono"},
\eqn{\log N_{1,a}=\log r_0-\sum_{j=2}^a d_j}; for \code{"capped"},
\eqn{\log N_{1,a}=\log r_0-d_a}, with \eqn{d_j\ge0}.
These fixed constraints include the terminal plus group and allow ties.
+A pooled plus group can exceed a single age's abundance, so imposing these
+constraints on it is a deliberate biological assumption. Wald intervals
+at a constraint boundary are only local approximations.
The baseline always uses the realized preceding initial-age state.
No equilibrium plus-group correction is applied in this first year.
This is an initial-state assumption, not a reconstruction of past cohorts.
@@ -411,9 +410,9 @@ including updated biology within a shortened plus group. Predictions and
observation draws then use each row's SD. Initial RW
states are retained: F/M walks start from the supplied first latent row;
the N walk retains its first \code{log_n} row and computes that row's residual
-against the newly generated cohort prediction. Random N0 is redrawn; free
-N0, first recruitment and additional early boundary recruitment states
-remain supplied fixed states.
+against the newly generated cohort prediction. Initial older-age abundance,
+first recruitment and additional early recruitment boundaries retain their
+supplied values, including when integrated with \code{init = "latent"}.
\code{\link[=sim_tam]{sim_tam()}} can hold parameters fixed, draw fixed effects with their estimated
covariance while retaining fitted random states, or draw all parameters with
diff --git a/man/vis_tam.Rd b/man/vis_tam.Rd
index 9eb4f72b..c99237f2 100644
--- a/man/vis_tam.Rd
+++ b/man/vis_tam.Rd
@@ -89,6 +89,7 @@ Pandoc, included with RStudio or available from
Precomputed assessment references may be mixed with tinyAM fits; they are
not refittable TAM objects. Supply \code{background} to add a page
describing assessment assumptions and translation choices.
+Model structure opens first, with Diagram and Function calls tabs.
Fisheries is a dropdown with one page per declared catch fleet,
followed by Aggregate fishery. Fleet pages show yield, catch and F
at age, and observation residuals. Unlabeled catch models appear
@@ -96,9 +97,10 @@ only on the aggregate page; they are not allocated among fleets.
Without fleets, Aggregate fishery retains all single-fishery
diagnostics. With fleets, it shows total yield; residuals are
assessed separately within each fleet.
-Observation residuals default to Auto: each model uses retained OSA
+Observation residuals initially use retained OSA
residuals when available in that panel, otherwise standardized
-residuals. The dropdown also selects either type explicitly. Partial
+residuals. The dropdown offers OSA and Standardized when OSAs are
+available. Model frames choose their initial type separately. Partial
OSA gaps remain missing, and labels identify the method. Retain OSA
calculations with \code{\link[=add_osa_residuals]{add_osa_residuals()}}; rendering never computes them.
The Parameters menu separates Fixed and Random pages. Fixed plots
diff --git a/tests/testthat/model-structure-controls.js b/tests/testthat/model-structure-controls.js
new file mode 100644
index 00000000..6cf950c9
--- /dev/null
+++ b/tests/testthat/model-structure-controls.js
@@ -0,0 +1,92 @@
+const fs = require('fs'), vm = require('vm'), assert = require('assert');
+const source = fs.readFileSync(process.argv[2], 'utf8');
+const order = ['initial', 'recruitment', 'fishing', 'natural', 'population', 'survey', 'catch', 'ssb'];
+function element() {
+ return {attributes: {}, children: [],
+ setAttribute(name, value) { this.attributes[name] = String(value); },
+ getAttribute(name) { return this.attributes[name]; },
+ appendChild(child) { this.children.push(child); },
+ replaceChildren() { this.children = []; }};
+}
+function widget(width, feedback) {
+ const queue = [], observations = [], ancestors = [{parentElement: null}];
+ const root = {parentElement: ancestors[0], visible: true};
+ const pane = {style: {display: ''}, getAttribute: () => '1'};
+ const reference = {style: {display: 'none'}, getAttribute: () => '2'};
+ const selector = {value: '1', addEventListener: (name, fun) => { selector[name] = fun; }};
+ const svg = element(), graph = element(); graph.width = width;
+ graph.attributes['data-feedback'] = String(feedback);
+ graph.layout = () => {
+ if (graph.width >= 760) return {
+ initial: [24, 60, 200, 120], recruitment: [24, 280, 200, 120],
+ fishing: [24, 500, 200, 120], natural: [274, 500, 200, 120],
+ population: [524, 280, 200, 120], survey: [774, 60, 200, 120],
+ catch: [774, 500, 200, 120], ssb: [524, 500, 200, 120]};
+ if (graph.width >= 460) return {
+ initial: [24, 60, 270, 120], recruitment: [346, 60, 270, 120],
+ fishing: [24, 280, 270, 120], natural: [346, 280, 270, 120],
+ population: [24, 500, 592, 120], survey: [24, 720, 270, 120],
+ catch: [346, 720, 270, 120], ssb: [24, 940, 592, 120]};
+ return Object.fromEntries(order.map((name, i) => [name, [24, 60 + i * 200, graph.width - 48, 120]]));
+ };
+ function bounds(name, input) {
+ const p = graph.layout()[name], top = p[1] - (input ? 42 : 0), height = input ? 26 : p[3];
+ return {left: p[0], right: p[0] + p[2], top, bottom: top + height, width: p[2], height};
+ }
+ const nodes = order.map(name => ({getAttribute: () => name, getBoundingClientRect: () => bounds(name, false)}));
+ const inputs = ['survey', 'fishing'].map(name => ({getAttribute: () => name, getBoundingClientRect: () => bounds(name, true)}));
+ graph.getBoundingClientRect = () => ({left: 0, top: 0,
+ width: root.visible && pane.style.display !== 'none' ? graph.width : 0,
+ height: graph.width >= 760 ? 660 : graph.width >= 460 ? 1100 : 1640});
+ graph.querySelector = () => svg;
+ graph.querySelectorAll = query => query === '[data-component]' ? nodes : inputs;
+ root.querySelector = () => selector;
+ root.querySelectorAll = query => query === '.tam-model-graph' ? [graph] : [pane, reference];
+ const observers = {};
+ const window = {
+ requestAnimationFrame: fun => queue.push(fun), addEventListener: (name, fun) => { window[name] = fun; },
+ ResizeObserver: class {constructor(fun) { observers.resize = fun; } observe(target) { observations.push(target); }},
+ MutationObserver: class {constructor(fun) { observers.visibility = fun; } observe() {}}
+ };
+ const document = {currentScript: {closest: () => root}, createElementNS: element};
+ vm.runInNewContext(source, {window, document});
+ function flush() { while (queue.length) queue.shift()(); }
+ function validate() {
+ assert.equal(svg.children.length, 10 + Number(feedback));
+ const obstacles = nodes.concat(inputs).map(node => node.getBoundingClientRect());
+ svg.children.filter(group => !group.attributes['data-connection'].startsWith('input-')).forEach(group => {
+ const points = group.children[0].attributes.points.split(' ').map(p => p.split(',').map(Number));
+ points.slice(1).forEach((p, i) => {
+ const a = points[i];
+ assert(a[0] === p[0] || a[1] === p[1], 'connector must be orthogonal');
+ obstacles.forEach(r => {
+ const crosses = a[0] === p[0] ? a[0] > r.left && a[0] < r.right &&
+ Math.max(a[1], p[1]) > r.top && Math.min(a[1], p[1]) < r.bottom :
+ a[1] > r.top && a[1] < r.bottom && Math.max(a[0], p[0]) > r.left &&
+ Math.min(a[0], p[0]) < r.right;
+ assert(!crosses, 'connector must avoid text boxes: ' + group.attributes['data-connection']);
+ });
+ });
+ });
+ assert(svg.children.some(group => group.children[0].attributes['stroke-dasharray'] === '4,3'));
+ }
+ flush(); validate();
+ assert(observations.includes(root) && observations.includes(graph));
+ return {root, graph, svg, selector, pane, reference, observers, flush, validate};
+}
+const a = widget(1000, true), b = widget(1000, false);
+a.graph.width = 640; a.observers.resize(); a.flush(); a.validate();
+assert.equal(a.graph.attributes['data-layout'], 'compact');
+assert.equal(b.graph.attributes['data-layout'], 'wide');
+a.graph.width = 400; a.observers.resize(); a.flush(); a.validate();
+assert.equal(a.graph.attributes['data-layout'], 'stacked');
+assert(a.svg.attributes.viewBox.startsWith('0 0 400 '));
+a.selector.value = '2'; a.selector.change(); a.flush();
+assert.equal(a.pane.style.display, 'none'); assert.equal(a.reference.style.display, '');
+assert.equal(b.pane.style.display, '');
+a.graph.width = 1000; a.selector.value = '1'; a.selector.change(); a.flush(); a.validate();
+assert.equal(a.graph.attributes['data-layout'], 'wide');
+a.root.visible = false; a.graph.width = 640; a.observers.visibility(); a.flush();
+a.root.visible = true; a.observers.visibility(); a.flush(); a.validate();
+assert.equal(a.graph.attributes['data-layout'], 'compact');
+console.log('Responsive model structure controls passed');
diff --git a/tests/testthat/residual-selection-controls.js b/tests/testthat/residual-selection-controls.js
index 300a2173..fc6cacff 100644
--- a/tests/testthat/residual-selection-controls.js
+++ b/tests/testthat/residual-selection-controls.js
@@ -6,7 +6,10 @@ global.Plotly = {
react: async (el, data, layout) => { el.data = data; el.layout = layout; },
addFrames: async (el, frames) => { banks++; el._transitionData = {_frames: frames}; },
deleteFrames: async el => { el._transitionData = {_frames: []}; },
- animate: async () => {}
+ animate: async () => {},
+ relayout: async (el, patch) => {
+ if (patch["updatemenus[0].active"] !== undefined) el.layout.updatemenus[0].active = patch["updatemenus[0].active"];
+ }
};
const flush = () => new Promise(resolve => setImmediate(resolve));
function widget() {
@@ -30,36 +33,38 @@ const points = el => el.data.reduce((n, trace) => n + trace.x.length, 0);
await flush();
assert.equal(points(a), 2);
assert.equal(points(b), 2);
- assert.deepEqual(a.layout.updatemenus[0].buttons.map(button => button.label), ['Auto', 'Standardized', 'OSA']);
+ assert.deepEqual(a.layout.updatemenus[0].buttons.map(button => button.label), ['Standardized', 'OSA']);
assert.equal(a.layout.updatemenus[0].x, 0);
assert.equal(a.layout.updatemenus[0].xanchor, 'left');
assert.equal(a.layout.updatemenus[0].yanchor, 'bottom');
assert.equal(a.layout.updatemenus[0].pad.b, 28);
assert.equal(a.layout.margin.t, 100); // Header rows stay clear of the upper-right modebar.
chooseFrame(a, 'B');
- assert.equal(points(a), 6); // Auto keeps a model without cached OSA.
+ assert.equal(points(a), 6); // Initial choice keeps models without cached OSA.
+ assert.equal(a.layout.updatemenus[0].active, 0);
const before = banks;
chooseFrame(a, 'A');
chooseFrame(a, 'B');
assert.equal(banks, before); // Native frame changes keep their frame bank.
- chooseType(a, 2); await flush();
+ chooseType(a, 1); await flush();
assert.equal(a.layout.sliders[0].active, 1);
assert.equal(points(a), 0);
assert(a.layout.annotations[0].text.includes('OSA residuals unavailable'));
assert.equal(points(b), 2); // Widgets have independent selections.
- chooseType(a, 1); await flush();
+ chooseType(a, 0); await flush();
assert.equal(points(a), 6);
assert.equal(a.layout.sliders[0].active, 1);
chooseFrame(a, 'A');
assert.equal(points(a), 6);
- chooseType(a, 0); await flush();
+ chooseType(a, 1); await flush();
assert.equal(points(a), 2); // Partial OSA never fills its gaps from standard residuals.
assert(a.layout.annotations[0].text.includes('oneStepGeneric'));
- for (let i = 0; i < 3; i++) chooseType(a, i);
+ for (let i = 0; i < 5; i++) chooseType(a, i % 2);
await flush(); await flush();
- assert.equal(a.layout.updatemenus[0].active, 2);
+ assert.equal(a.layout.updatemenus[0].active, 0);
const singleConfig = JSON.parse(JSON.stringify(source));
singleConfig.frames = 'Only'; singleConfig.animated = false;
+ singleConfig.defaultViews = 1;
singleConfig.views.forEach(view => {
view.cells = [view.cells[0]];
view.cells[0].data.forEach(trace => {
@@ -71,5 +76,9 @@ const points = el => el.data.reduce((n, trace) => n + trace.x.length, 0);
assert.deepEqual(single.data[0].x, [2000]);
assert.deepEqual(single.data[0].customdata, ['custom']);
assert.deepEqual(single.layout.sliders, []);
+ const noOsaConfig = JSON.parse(JSON.stringify(singleConfig));
+ noOsaConfig.showSelector = false; noOsaConfig.defaultViews = 0;
+ const noOsa = widget(); draw(noOsa, {layout: {}}, noOsaConfig); await flush();
+ assert.deepEqual(noOsa.layout.updatemenus, []);
console.log('Residual controls passed');
})().catch(error => { console.error(error); process.exit(1); });
diff --git a/tests/testthat/setup-fixtures.R b/tests/testthat/setup-fixtures.R
index eedeb842..4bba21cc 100644
--- a/tests/testthat/setup-fixtures.R
+++ b/tests/testthat/setup-fixtures.R
@@ -64,6 +64,7 @@ if (requireNamespace("RTMB", quietly = TRUE) && exists("cod_obs", inherits = TRU
age_breaks = c(3, 14)
)
)
+ fits <- list(N_dev = N_dev, M_dev = M_dev)
} else {
default_fit <- NULL
N_dev <- NULL
diff --git a/tests/testthat/test-check.R b/tests/testthat/test-check.R
index e921a3ef..5c51f9d9 100644
--- a/tests/testthat/test-check.R
+++ b/tests/testthat/test-check.R
@@ -120,8 +120,6 @@ test_that("numerical diagnostics include optimizer status and use one tolerance"
fit$sdrep$gradient.fixed <- .005
expect_true(check_tam(fit)$is_converged)
expect_false(check_tam(fit, grad_tol = .001)$is_converged)
- fit$grad_tol <- NULL
- expect_equal(check_tam(fit)$grad_tol, .01)
})
test_that("unavailable and non-finite numerical information cannot pass", {
diff --git a/tests/testthat/test-dat.R b/tests/testthat/test-dat.R
index 41e8784f..f4c3d05c 100644
--- a/tests/testthat/test-dat.R
+++ b/tests/testthat/test-dat.R
@@ -153,13 +153,6 @@ test_that("prepare_tam stops if neither M mu_supplied nor mu_form is provided",
)
})
-test_that("prepare_tam rejects the retired initialization setting", {
- expect_error(
- make_test_dat(N_settings = list(process = "off", init_N0 = FALSE)),
- "has been retired"
- )
-})
-
test_that("prepare_tam appends projection years and shapes obs correctly with proj_settings (F_mult API)", {
# n_proj = 2, n_mean = 3, status-quo F multiplier = 1
dat <- prepare_tam(
diff --git a/tests/testthat/test-data.R b/tests/testthat/test-data.R
index e2bf3d64..c6864e70 100644
--- a/tests/testthat/test-data.R
+++ b/tests/testthat/test-data.R
@@ -35,7 +35,6 @@ test_that("tam_data adds only a class and validates again after edits", {
expect_identical(unclass(x), unclass(data))
expect_identical(as_tam_data(x), x)
expect_invisible(check_tam_data(x))
- expect_identical(check_obs(x), check_tam_data(x))
a <- make_test_dat(data = data, years = 1983:1990, ages = 2:6)
b <- make_test_dat(data = x, years = 1983:1990, ages = 2:6)
expect_identical(make_par(a), make_par(b))
diff --git a/tests/testthat/test-fit.R b/tests/testthat/test-fit.R
index 6af8395c..478c3f66 100644
--- a/tests/testthat/test-fit.R
+++ b/tests/testthat/test-fit.R
@@ -72,16 +72,6 @@ test_that("fit_tam objective is unaffected by projections", {
expect_true(all(is.finite(fit$rep$log_pred[is_proj])))
})
-test_that("fit_tam does not estimate missing observation values", {
- fit <- update(
- default_fit,
- catch_settings = list(sd_form = ~1),
- index_settings = list(sd_form = ~1, q_form = ~ q_block),
- silent = TRUE
- )
- expect_false("missing" %in% fit$obj$env$.random)
-})
-
test_that("update can add arguments absent from the original call", {
cl <- update(
default_fit,
@@ -128,23 +118,6 @@ test_that("fit_retro inherits grad_tol stored on the fit when omitted", {
expect_false(any(out$fold_diagnostics$is_converged))
})
-test_that("fit_retro falls back to default grad_tol when fit has none", {
- fit <- default_fit
- fit$grad_tol <- NULL
- fit$sdrep$gradient.fixed[] <- 5e-4
-
- implicit <- fit_retro(fit, folds = 0, progress = FALSE)
- explicit <- fit_retro(fit, folds = 0, progress = FALSE, grad_tol = 1e-2)
-
- expect_identical(names(implicit$fits[[1]]), names(explicit$fits[[1]]))
- expect_identical(
- lapply(implicit$fits[[1]], `[[`, "is_converged"),
- lapply(explicit$fits[[1]], `[[`, "is_converged")
- )
- expect_identical(names(implicit$obs_pred), names(explicit$obs_pred))
- expect_identical(names(implicit$pop), names(explicit$pop))
-})
-
test_that("tam_fit summary and print methods provide structured output", {
fit <- default_fit
diff --git a/tests/testthat/test-math-audit.R b/tests/testthat/test-math-audit.R
index 3f4a9e36..ad9f46af 100644
--- a/tests/testthat/test-math-audit.R
+++ b/tests/testthat/test-math-audit.R
@@ -113,7 +113,7 @@ test_that("invalid grid boundaries cannot enter cohort and mortality matrices",
for (nm in c("catch", "weight", "maturity")) {
obs <- cod_obs
obs[[nm]] <- rbind(obs[[nm]], obs[[nm]][1, ])
- expect_error(check_obs(obs), "exactly one row per year and age")
+ expect_error(check_tam_data(obs), "exactly one row per year and age")
}
})
@@ -165,6 +165,6 @@ test_that("missing formula rows and invalid supplied scales cannot misalign like
for (nm in c("catch", "index", "weight", "maturity")) {
bad <- cod_obs
bad[[nm]]$obs[1] <- -1
- expect_error(check_obs(bad), "non-negative values")
+ expect_error(check_tam_data(bad), "non-negative values")
}
})
diff --git a/tests/testthat/test-model-diagram.R b/tests/testthat/test-model-diagram.R
index 26e8fe45..b04eba71 100644
--- a/tests/testthat/test-model-diagram.R
+++ b/tests/testthat/test-model-diagram.R
@@ -83,7 +83,7 @@ test_that("model selectors handle references and independent escaped containers"
expect_match(html, 'data-model-pane="1" style="display:none"', fixed = TRUE)
expect_match(html, "unavailable for a reporting reference", fixed = TRUE)
expect_match(html, "Fit <two> "quoted"", fixed = TRUE)
- expect_match(html, "closest('.tam-model-structure')", fixed = TRUE)
+ expect_match(html, "document.currentScript.closest('.tam-model-structure')", fixed = TRUE)
expect_false(grepl("document.querySelector", html, fixed = TRUE))
all_refs <- htmltools::renderTags(plot_model(reference))$html
expect_match(all_refs, "unavailable", fixed = TRUE)
@@ -91,11 +91,40 @@ test_that("model selectors handle references and independent escaped containers"
expect_error(plot_model(default_fit, model_list = fits), "either")
})
-test_that("the dedicated Model structure page preserves the dashboard initial page", {
+test_that("Model structure opens first and groups its diagram with function calls", {
template <- readLines(system.file("rmd", "vis_tam.Rmd", package = "tinyAM"))
structure <- readLines(system.file("rmd", "model_structure.Rmd", package = "tinyAM"))
expect_true(any(grepl('child = "model_structure.Rmd"', template, fixed = TRUE)))
expect_true(any(grepl("plot_model(model_list = fits)", structure, fixed = TRUE)))
- expect_gt(which(grepl('child = "model_structure.Rmd"', template, fixed = TRUE)),
+ expect_lt(which(grepl('child = "model_structure.Rmd"', template, fixed = TRUE)),
which(grepl('child = "fisheries.Rmd"', template, fixed = TRUE)))
+ expect_lt(which(grepl('child = "model_structure.Rmd"', template, fixed = TRUE)),
+ which(grepl('child = if (is.null(background))', template, fixed = TRUE)))
+ expect_true(any(grepl("Row {.tabset}", structure, fixed = TRUE)))
+ expect_true(all(c("### Diagram", "### Function calls") %in% structure))
+ tables <- readLines(system.file("rmd", "tables.Rmd", package = "tinyAM"))
+ expect_false(any(grepl("### Function calls", tables, fixed = TRUE)))
+ expect_false(any(grepl('Settings {data-navmenu="Tables"}', tables, fixed = TRUE)))
+})
+
+test_that("diagram nodes inherit typography and explain colours and arrows below", {
+ html <- htmltools::renderTags(plot_model(default_fit))$html
+ expect_match(html, ' should fail coverage check
obs <- cod_obs
drop_n <- min(3L, nrow(obs$weight))
obs$weight <- obs$weight[-seq_len(drop_n), , drop = FALSE]
- expect_error(check_obs(obs), "`weight` coverage is incomplete", fixed = TRUE)
+ expect_error(check_tam_data(obs), "`weight` coverage is incomplete", fixed = TRUE)
obs <- cod_obs
drop_n <- min(3L, nrow(obs$index))
obs$index <- obs$index[-seq_len(drop_n), , drop = FALSE]
- expect_invisible(expect_true(check_obs(obs)))
+ expect_invisible(expect_true(check_tam_data(obs)))
})
diff --git a/tests/testthat/test-plot.R b/tests/testthat/test-plot.R
index fb0c07b3..718f38a7 100644
--- a/tests/testthat/test-plot.R
+++ b/tests/testthat/test-plot.R
@@ -1,8 +1,5 @@
## Globals ----
-suppressWarnings(
- source(system.file("examples/example_fits.R", package = "tinyAM"))
-)
tabs <- tidy_tam(model_list = fits)
## Helpers ----
diff --git a/tests/testthat/test-public-api.R b/tests/testthat/test-public-api.R
index ec6b9099..5d237be8 100644
--- a/tests/testthat/test-public-api.R
+++ b/tests/testthat/test-public-api.R
@@ -7,22 +7,11 @@ test_that("prepare_tam and fit_tam expose the same data and model arguments", {
expect_true(all(settings %in% names(formals(fit_tam))))
expect_false(any(c("obs", "...") %in% names(formals(fit_tam))))
expect_identical(as.list(formals(fit_tam))[settings], as.list(formals(prepare_tam)))
- expect_false("make_dat" %in% getNamespaceExports("tinyAM"))
dat <- prepare_tam(data = cod_obs, years = 2000:2005, ages = 2:6)
expect_true("obs" %in% names(dat))
expect_false("data" %in% names(dat))
expect_true(all(vapply(dat$obs[c("catch", "index", "weight", "maturity")], function(x) "obs" %in% names(x), logical(1))))
- expect_error(prepare_tam(obs = cod_obs), "unused argument")
- expect_error(fit_tam(obs = cod_obs), "unused argument")
-})
-
-test_that("both input validators use the same checks", {
- expect_identical(check_tam_data(cod_obs), check_obs(cod_obs))
- expect_invisible(check_tam_data(cod_obs))
- invalid <- cod_obs
- invalid$index$samp_time[1] <- 2
- expect_error(check_tam_data(invalid), "samp_time")
})
test_that("update stores evaluated data under the public argument name", {
diff --git a/tests/testthat/test-recruitment-init.R b/tests/testthat/test-recruitment-init.R
index 08ab3660..1749486e 100644
--- a/tests/testthat/test-recruitment-init.R
+++ b/tests/testthat/test-recruitment-init.R
@@ -36,10 +36,6 @@ test_that("stock-recruit boundary years retain their values without extra penalt
})
test_that("fits classify initial recruitment correctly and preserve warm starts", {
- legacy <- default_fit
- legacy$refit_args$rec_settings$init <- NULL
- call <- update(legacy, rec_settings = list(process = "iid", mu_form = ~1), evaluate = FALSE)
- expect_identical(call$rec_settings$init, "free")
f <- update(default_fit, rec_settings = list(init = "latent"),
start_par = default_fit, silent = TRUE)
expect_true("log_r0" %in% f$obj$env$.random)
diff --git a/tests/testthat/test-recruitment.R b/tests/testthat/test-recruitment.R
index 56453667..85fd3107 100644
--- a/tests/testthat/test-recruitment.R
+++ b/tests/testthat/test-recruitment.R
@@ -53,15 +53,25 @@ test_that("recruitment covariates come only from youngest-age population covaria
test_that("Gaussian recruitment simulation keeps the anchor and residual convention", {
for (type in c("iid", "ar1", "rw")) {
spec <- list(process = type, mu_form = ~1, sd = .2, phi = if (type == "ar1") .5 else NULL)
- d <- make_test_dat(N_settings = list(process = "off"), rec_settings = spec)
+ d <- make_test_dat(years = 2000:2006, ages = 2:6,
+ N_settings = list(process = "off"), rec_settings = spec)
p <- make_par(d)
p$log_r0 <- 1
if (!is.null(p$rec_beta)) p$rec_beta[] <- 2
set.seed(912)
- draws <- replicate(500, tinyAM:::.simulate_rec(p, d))
- expected <- if (type == "rw") 1 else 2
- expect_lt(max(abs(rowMeans(draws) - expected)), if (type == "rw") .15 else .05)
- expect_equal(p$log_r0, 1)
+ actual <- .population_states(p, d, d$W * 0 + .3, simulate = TRUE)
+ set.seed(912)
+ noise <- stats::rnorm(length(d$years) - 1L)
+ expected <- c(1, rep(NA_real_, length(noise)))
+ for (i in seq_along(noise)) {
+ mean <- switch(type, iid = 2, rw = expected[i],
+ ar1 = if (i == 1L) 2 else 2 + .5 * (expected[i] - 2))
+ sd <- if (type == "ar1" && i == 1L) .2 / sqrt(1 - .5^2) else .2
+ expected[i + 1L] <- mean + sd * noise[i]
+ }
+ expect_equal(unname(actual$log_recruitment), expected, tolerance = 1e-12)
+ expect_equal(unname(actual$log_N[, 1L]), expected, tolerance = 1e-12)
+ expect_equal(actual$log_recruitment[1L], c(`2000` = p$log_r0))
}
})
diff --git a/tests/testthat/test-residual-selection.R b/tests/testthat/test-residual-selection.R
index 2fe0caaa..26d7a5f2 100644
--- a/tests/testthat/test-residual-selection.R
+++ b/tests/testthat/test-residual-selection.R
@@ -13,7 +13,7 @@ residual_plot_data <- function() {
residual_config <- function(p) p$jsHooks$render[[1L]]$data
residual_points <- function(cell) sum(vapply(cell$data, function(trace) length(trace$x), integer(1)))
-test_that("Auto selects retained residuals per model and preserves OSA gaps", {
+test_that("initial resolution selects retained residuals per model and preserves OSA gaps", {
d <- residual_plot_data(); before <- d
auto <- .observation_residual_data(d, "auto")
expect_identical(auto$status$residual_type, c("osa", "standardized"))
@@ -42,39 +42,41 @@ test_that("public residual plots retain models, labels and unavailable OSA frame
expect_equal(vapply(p$x$frames, residual_points, integer(1)), c(2L, 6L))
expect_true(all(vapply(p$x$frames, function(frame) length(frame$traces) > 0L, logical(1))))
expect_identical(plotly::plotly_build(p)$x$frames, p$x$frames)
- expect_identical(config$view, 0L)
- expect_identical(vapply(config$views, `[[`, character(1), "type"), c("auto", "standardized", "osa"))
- expect_equal(vapply(config$views[[1]]$cells, residual_points, integer(1)), c(2L, 6L))
- expect_equal(vapply(config$views[[2]]$cells, residual_points, integer(1)), c(6L, 6L))
- expect_equal(vapply(config$views[[3]]$cells, residual_points, integer(1)), c(2L, 0L))
- note <- config$views[[3]]$cells[[2]]$layout$annotations[[1]]$text
+ expect_identical(config$view, 1L)
+ expect_identical(config$defaultViews, c(1L, 0L))
+ expect_true(config$showSelector)
+ expect_identical(vapply(config$views, `[[`, character(1), "type"), c("standardized", "osa"))
+ expect_equal(vapply(config$views[[1]]$cells, residual_points, integer(1)), c(6L, 6L))
+ expect_equal(vapply(config$views[[2]]$cells, residual_points, integer(1)), c(2L, 0L))
+ note <- config$views[[2]]$cells[[2]]$layout$annotations[[1]]$text
expect_match(note, "OSA residuals unavailable", fixed = TRUE)
- expect_match(config$views[[1]]$cells[[1]]$layout$annotations[[1]]$text,
+ expect_match(config$views[[2]]$cells[[1]]$layout$annotations[[1]]$text,
"oneStepGeneric", fixed = TRUE)
- annotation <- config$views[[1]]$cells[[1]]$layout$annotations[[1]]
+ annotation <- config$views[[2]]$cells[[1]]$layout$annotations[[1]]
expect_equal(annotation$x, 0)
expect_identical(annotation$xanchor, "left")
expect_equal(annotation$yshift, 8)
slots <- unlist(lapply(config$views, function(view) vapply(view$cells, function(cell) length(cell$data), integer(1))))
expect_length(unique(slots), 1L)
- expect_identical(residual_config(fun(d, residual_type = "osa"))$view, 2L)
+ expect_identical(residual_config(fun(d, residual_type = "osa"))$view, 1L)
}
expect_identical(d, before)
expect_error(plot_resid(d, residual_type = "unknown"), "arg")
})
-test_that("Auto is chosen for the whole panel rather than separately for animation ages", {
+test_that("overlays choose one type for the whole panel before animation", {
d <- residual_plot_data()
p <- plot_resid(d, frame = ~age, color = ~model, colors = c("#001122", "#ee8800"))
config <- residual_config(p)
status <- config$views[[1]]$cells[[2]]$status
- expect_identical(status$residual_type, c("osa", "standardized"))
- expect_equal(status$n, c(0L, 3L))
- expect_equal(residual_points(config$views[[1]]$cells[[2]]), 3L)
+ expect_identical(status$residual_type, c("standardized", "standardized"))
+ expect_equal(status$n, c(3L, 3L))
+ expect_identical(config$defaultViews, c(0L, 0L))
+ expect_equal(residual_points(p$x$frames[[2]]), 6L)
p <- plot_resid(d, frame = ~model, color = ~model, colors = c("#001122", "#ee8800"))
config <- residual_config(p)
for (i in 1:2) {
- nonempty <- Filter(function(trace) length(trace$x), config$views[[2]]$cells[[i]]$data)
+ nonempty <- Filter(function(trace) length(trace$x), config$views[[1]]$cells[[i]]$data)
expect_identical(nonempty[[1]]$marker$color, c("#001122", "#ee8800")[i])
}
})
@@ -85,14 +87,15 @@ test_that("residual views preserve custom hover, scalar inputs and process bubbl
p <- plot_resid(d, text = ~paste("custom", year), hovertemplate = "Custom: %{text}",
residual_type = "standardized")
expect_false(residual_config(p)$animated)
- expect_identical(residual_config(p)$view, 1L)
- traces <- Filter(function(trace) length(trace$x), residual_config(p)$views[[2]]$cells[[1]]$data)
+ expect_identical(residual_config(p)$view, 0L)
+ traces <- Filter(function(trace) length(trace$x), residual_config(p)$views[[1]]$cells[[1]]$data)
expect_identical(traces[[1]]$hovertemplate, "Custom: %{text}")
expect_match(traces[[1]]$text, "custom", fixed = TRUE)
- expect_match(residual_config(plot_bubbles(d))$views[[1]]$cells[[1]]$layout$annotations[[1]]$text,
+ expect_match(residual_config(plot_bubbles(d))$views[[2]]$cells[[1]]$layout$annotations[[1]]$text,
"method unspecified", fixed = TRUE)
d$osa_res <- NA_real_
expect_identical(residual_config(plot_resid(d))$views[[1]]$cells[[1]]$status$residual_type, "standardized")
+ expect_false(residual_config(plot_resid(d))$showSelector)
d$std_res <- NA_real_
expect_equal(residual_points(residual_config(plot_resid(d))$views[[1]]$cells[[1]]), 0L)
process <- data.frame(year = 2000:2001, age_block = c("2-4", "5-8"), est = c(-1, 2))
@@ -100,6 +103,7 @@ test_that("residual views preserve custom hover, scalar inputs and process bubbl
expect_s3_class(p, "plotly")
expect_null(p$jsHooks$render)
expect_equal(sum(vapply(plotly::plotly_build(p)$x$data, function(trace) length(trace$x), integer(1))), 2L)
+ expect_true(all(vapply(plotly::plotly_build(p)$x$data, function(trace) identical(trace$marker$line$width, 0), logical(1))))
})
test_that("native residual dropdown and frames coordinate in independent widgets", {
@@ -113,3 +117,35 @@ test_that("native residual dropdown and frames coordinate in independent widgets
expect_null(attr(result, "status"))
expect_match(paste(result, collapse = "\n"), "Residual controls passed", fixed = TRUE)
})
+
+
+test_that("overlay defaults require eligible OSA in every displayed model", {
+ d <- residual_plot_data()
+ p <- plot_resid(d)
+ config <- residual_config(p)
+ expect_identical(config$view, 0L)
+ expect_equal(residual_points(list(data=p$x$data)), 12L)
+ d$osa_res[7L] <- -.5
+ p <- plot_resid(d, frame=~age)
+ expect_identical(residual_config(p)$defaultViews, c(1L, 1L))
+ expect_equal(residual_points(p$x$frames[[2]]), 0L)
+ traces <- do.call(c, lapply(residual_config(plot_bubbles(d))$views, function(view)
+ do.call(c, lapply(view$cells, `[[`, "data"))))
+ expect_true(all(vapply(traces, function(trace) identical(trace$marker$line$width, 0), logical(1))))
+})
+
+test_that("explicit hover vectors follow model frames and partial OSA rows", {
+ d <- residual_plot_data()
+ labels <- paste0("row-", seq_len(nrow(d)))
+ p <- plot_resid(d, frame = ~model, text = labels,
+ customdata = seq_len(nrow(d)), hovertemplate = "%{text}: %{customdata}")
+ osa <- residual_config(p)$views[[2]]$cells[[1]]
+ traces <- Filter(function(trace) length(trace$x), osa$data)
+ expect_identical(as.character(traces[[1]]$text), labels[1:2])
+ expect_equal(as.numeric(traces[[1]]$customdata), 1:2)
+ expect_true(all(as.character(traces[[1]]$hovertemplate) == "%{text}: %{customdata}"))
+ standard <- residual_config(p)$views[[1]]$cells[[2]]
+ traces <- Filter(function(trace) length(trace$x), standard$data)
+ expect_identical(as.character(traces[[1]]$text), labels[7:12])
+ expect_equal(as.numeric(traces[[1]]$customdata), 7:12)
+})
diff --git a/tests/testthat/test-retro-vis.R b/tests/testthat/test-retro-vis.R
index 66c6e31b..0edc0bca 100644
--- a/tests/testthat/test-retro-vis.R
+++ b/tests/testthat/test-retro-vis.R
@@ -1,4 +1,4 @@
-test_that("S3 dispatch preserves legacy fit, reference and list labels", {
+test_that("S3 dispatch preserves fit, reference and list labels", {
testthat::local_mocked_bindings(.render_vis_tam = function(fits, ...) names(fits),
.package = "tinyAM")
first <- second <- default_fit
diff --git a/tests/testthat/test-settings.R b/tests/testthat/test-settings.R
index e7868a67..a6e6e1db 100644
--- a/tests/testthat/test-settings.R
+++ b/tests/testthat/test-settings.R
@@ -107,7 +107,6 @@ test_that("Usage shows every supported settings entry and optional projections",
})
test_that("recruitment settings validate and merge independently of survival", {
- expect_error(make_test_dat(N_settings = list(rec_form = ~ rw(year))), "has moved")
expect_error(make_test_dat(rec_settings = list(process = "iid")), "mu_form is required")
expect_error(make_test_dat(rec_settings = list(process = "ar1")), "mu_form is required")
expect_error(make_test_dat(rec_settings = list(process = "off")), "mu_form is required")
diff --git a/tests/testthat/test-sim.R b/tests/testthat/test-sim.R
index b35422e5..4ace4411 100644
--- a/tests/testthat/test-sim.R
+++ b/tests/testthat/test-sim.R
@@ -1,6 +1,3 @@
-suppressWarnings(
- source(system.file("examples/example_fits.R", package = "tinyAM"))
-)
fit <- N_dev
## sim_tam ----
diff --git a/tests/testthat/test-smooth-formulas.R b/tests/testthat/test-smooth-formulas.R
index 79a95ca0..f0ccc911 100644
--- a/tests/testthat/test-smooth-formulas.R
+++ b/tests/testthat/test-smooth-formulas.R
@@ -103,7 +103,7 @@ test_that("temporal smooth forecasts hold training-year values without future ba
historical <- .parse_q_formula(form,d)
actual <- .parse_q_formula(form,full)
for (j in seq_along(actual$q_terms)) {
- expect_true(.smooth_basis_compatible(historical$q_terms[[j]],actual$q_terms[[j]]))
+ expect_false(is.null(.smooth_basis_transform(historical$q_terms[[j]],actual$q_terms[[j]])))
expect_equal(actual$q_terms[[j]]$design[!full$is_proj,,drop=FALSE],historical$q_terms[[j]]$design)
expect_equal(unname(actual$q_terms[[j]]$design[full$is_proj,,drop=FALSE]),
unname(actual$q_terms[[j]]$design[full$year == 2007,,drop=FALSE]))
@@ -287,10 +287,10 @@ test_that("smooth warm starts preserve functions and penalties under explicit ro
expect_false(any(colnames(d$q_terms[[1]]$smooth_fixed) %in% names(rejected$log_q)))
changed <- d$q_terms[[1]]
changed$basis_signature$grid$age <- changed$basis_signature$grid$age+.01
- expect_false(.smooth_basis_compatible(d$q_terms[[1]],changed))
+ expect_null(.smooth_basis_transform(d$q_terms[[1]],changed))
changed <- d$q_terms[[1]]
changed$scope <- list(type="survey",name="different",id="survey:different")
- expect_false(.smooth_basis_compatible(d$q_terms[[1]],changed))
+ expect_null(.smooth_basis_transform(d$q_terms[[1]],changed))
fake$dat <- old
fake$dat$q_terms <- fake$dat$q_terms[-1]
rejected <- suppressMessages(.start_parameters(fake,d))
diff --git a/tests/testthat/test-ssb-timing.R b/tests/testthat/test-ssb-timing.R
index 5c64a015..4b8db4a5 100644
--- a/tests/testthat/test-ssb-timing.R
+++ b/tests/testthat/test-ssb-timing.R
@@ -127,7 +127,7 @@ test_that("timing is validated even at immature and hidden biological ages", {
for (value in list(NA_real_, NaN, Inf, -.1, 1.1, "fall", factor("fall"))) {
data <- timed_obs()
data$maturity$spawn_time <- value
- expect_error(check_obs(data), "spawn_time")
+ expect_error(check_tam_data(data), "spawn_time")
expect_error(make_test_dat(data = data, ages = 2:8), "spawn_time")
}
data <- timed_obs()
diff --git a/tests/testthat/test-tidy.R b/tests/testthat/test-tidy.R
index a8ac0a50..8c841505 100644
--- a/tests/testthat/test-tidy.R
+++ b/tests/testthat/test-tidy.R
@@ -45,19 +45,6 @@ test_that("tidy_array respects value_name and converts numeric-like dimnames", {
})
-## tidy_mat ----
-
-test_that("tidy_mat is an alias of tidy_array", {
- m <- matrix(
- 1:6,
- nrow = 2,
- dimnames = list(age = c("2", "3"), year = c("2001", "2002", "2003"))
- )
- a <- tidy_array(m)
- b <- tidy_mat(m)
- expect_identical(a, b)
-})
-
test_that("tidy_array handles 3D arrays, converting only numeric-like dims", {
a <- array(
1:8,
diff --git a/tests/testthat/test-validation-feedback.R b/tests/testthat/test-validation-feedback.R
index 6b237362..15b310f2 100644
--- a/tests/testthat/test-validation-feedback.R
+++ b/tests/testthat/test-validation-feedback.R
@@ -3,7 +3,6 @@ test_that("input validation reports success while preserving its logical return"
expect_message(check_tam_data(cod_obs), "fleets: 1; surveys: 1")
expect_silent(result <- check_tam_data(cod_obs, quiet = TRUE))
expect_identical(result, TRUE)
- expect_silent(expect_identical(check_obs(cod_obs, quiet = TRUE), TRUE))
for (quiet in list(NA, 1, "yes", logical())) {
expect_error(check_tam_data(cod_obs, quiet = quiet), "quiet.*TRUE or FALSE")
}
diff --git a/tests/testthat/test-vis.R b/tests/testthat/test-vis.R
index 418e8396..ba203900 100644
--- a/tests/testthat/test-vis.R
+++ b/tests/testthat/test-vis.R
@@ -1,9 +1,5 @@
## vis_tam ----
-suppressWarnings(
- source(system.file("examples/example_fits.R", package = "tinyAM"))
-)
-
dashboard_plots <- function(file) {
# Embedded Plotly code contains Ctrl-Z, which ends Windows text-mode reads.
con <- file(file, "rb")
@@ -211,32 +207,6 @@ test_that("render_args must be a named list", {
)
})
-test_that("vis_tam accepts models provided via dots", {
- tmpfile <- tempfile(fileext = ".html")
-
- expect_no_error({
- vis_tam(N_dev, M_dev,
- output_file = tmpfile, open_file = FALSE,
- render_args = list(quiet = TRUE)
- )
- })
-
- expect_true(file.exists(tmpfile))
-})
-
-test_that("vis_tam accepts list input supplied through dots", {
- tmpfile <- tempfile(fileext = ".html")
-
- expect_no_error({
- vis_tam(list(N_dev = N_dev, M_dev = M_dev),
- output_file = tmpfile, open_file = FALSE,
- render_args = list(quiet = TRUE)
- )
- })
-
- expect_true(file.exists(tmpfile))
-})
-
test_that("vis_tam rejects unnamed or duplicated model lists", {
unnamed <- list(N_dev, M_dev)
duped <- structure(list(N_dev, M_dev), names = c("N_dev", "N_dev"))
@@ -515,7 +485,7 @@ test_that("tam_ref objects accept a Background page", {
call_start <- regexpr('',
visible_html,
perl = TRUE
))