
Save & Compare Refactoring Baseline
baseline.RmdIntroduction
This vignette saves a complete numerical baseline of the
opal BET model before a refactoring change, then compares a
post-refactoring build against it. The workflow has two modes controlled
by the compare flag:
-
compare = FALSE: Build the model, evaluatefn()andgr(), save all outputs todata/opal_baseline.rda, and update the dataset that is exposed viadata(opal_baseline). -
compare = TRUE(default): Load theopal_baselinedataset viadata(opal_baseline)(or the localdata/opal_baseline.rdafile), rebuild the same inputs, and check numerical equivalence and speedup.
Set the flag here before knitting:
compare <- TRUE
# Use here::here() to resolve paths from the project root reliably
baseline_file <- here::here("data", "opal_baseline.rda")
baseline_data_file <- here::here("data", "opal_baseline_data.rda")
baseline_parameters_file <- here::here("data", "opal_baseline_parameters.rda")
baseline_map_file <- here::here("data", "opal_baseline_map.rda")
bench_iters <- 20Load inputs
data(wcpo_bet_data)
data <- wcpo_bet_dataLength composition data
data(wcpo_bet_lf)
lf_wide <- wcpo_bet_lf %>%
tidyr::pivot_wider(
id_cols = c(fishery, year, month, ts),
names_from = bin,
values_from = value,
values_fill = 0
) %>%
dplyr::arrange(fishery, ts)
var_adjust_scalars <- 1 / rep(1, data$n_fishery)
data <- prep_lf_data(data, lf_wide,
lf_keep_fisheries = c(8, 9),
lf_var_adjust = var_adjust_scalars)
data$lf_switch <- 1LWeight composition data
data(wcpo_bet_wf)
data$wt_bin_start <- 1
data$wt_bin_width <- 1
data$n_wt <- 200L
wf_wide <- wcpo_bet_wf |>
tidyr::pivot_wider(
id_cols = c(fishery, year, month, ts),
names_from = bin,
values_from = value,
values_fill = 0
) |>
dplyr::arrange(fishery, ts)
data <- prep_wf_data(data, wf_wide,
wf_keep_fisheries = c(2),
wf_switch = 1L)Model setup
Parameters
data(wcpo_bet_parameters)
parameters <- list(
log_B0 = 20,
log_h = as.numeric(wcpo_bet_parameters$log_h),
log_sigma_r = as.numeric(wcpo_bet_parameters$log_sigma_r),
log_cpue_q = as.numeric(wcpo_bet_parameters$log_cpue_q),
cpue_creep = as.numeric(wcpo_bet_parameters$cpue_creep),
log_cpue_tau = as.numeric(wcpo_bet_parameters$log_cpue_tau),
log_cpue_omega = as.numeric(wcpo_bet_parameters$log_cpue_omega),
log_lf_tau = log(rep(0.1, data$n_fishery)),
log_wf_tau = rep(0, data$n_fishery),
log_L1 = as.numeric(wcpo_bet_parameters$log_L1),
log_L2 = as.numeric(wcpo_bet_parameters$log_L2),
log_k = as.numeric(wcpo_bet_parameters$log_k),
log_CV1 = as.numeric(wcpo_bet_parameters$log_CV1),
log_CV2 = as.numeric(wcpo_bet_parameters$log_CV2),
par_sel = as.matrix(wcpo_bet_parameters$par_sel),
rdev_y = as.numeric(wcpo_bet_parameters$rdev_y)
)Priors
data$priors <- get_priors(parameters = parameters, data = data)Parameter map
map_sel <- matrix(NA, nrow(parameters$par_sel), ncol(parameters$par_sel))
map <- list(
log_h = factor(NA),
log_sigma_r = factor(NA),
cpue_creep = factor(NA),
log_cpue_tau = factor(NA),
log_cpue_omega = factor(NA),
log_lf_tau = factor(rep(NA, length(parameters$log_lf_tau))),
log_wf_tau = factor(rep(NA, length(parameters$log_wf_tau))),
log_L1 = factor(NA),
log_L2 = factor(NA),
log_k = factor(NA),
log_CV1 = factor(NA),
log_CV2 = factor(NA),
par_sel = factor(map_sel)
)Build the AD object
t_build <- system.time({
obj <- MakeADFun(func = cmb(opal_model, data),
parameters = parameters, map = map)
})
cat("MakeADFun build time:", round(t_build["elapsed"], 2), "sec\n")## MakeADFun build time: 17.14 sec
## Estimated parameters: 270
Evaluate and benchmark
set.seed(123)
t_fn <- system.time(nll <- obj$fn(obj$par))
t_gr <- system.time(gr <- obj$gr(obj$par))## outer mgc: 13629.29
## obj$fn(): 0.007 sec (NLL = 852582 )
## obj$gr(): 0.026 sec (max|gr| = 13629.29 )
## gr/fn ratio: 3.7 x
Stable timing via bench::mark:
par1 <- obj$par
# need to perturb the parameters slightly to avoid caching effects
bm <- bench::mark(
fn = obj$fn(par1 + runif(length(par1), -1e-10, 1e-10)),
gr = obj$gr(par1 + runif(length(par1), -1e-10, 1e-10)),
iterations = 20,
check = FALSE
)## outer mgc: 13629.29
## outer mgc: 13629.29
## outer mgc: 13629.29
## outer mgc: 13629.29
## outer mgc: 13629.29
## outer mgc: 13629.29
## outer mgc: 13629.29
## outer mgc: 13629.29
## outer mgc: 13629.29
## outer mgc: 13629.29
## outer mgc: 13629.29
## outer mgc: 13629.29
## outer mgc: 13629.29
## outer mgc: 13629.29
## outer mgc: 13629.29
## outer mgc: 13629.29
## outer mgc: 13629.29
## outer mgc: 13629.29
## outer mgc: 13629.29
## outer mgc: 13629.29
## outer mgc: 13629.29
bm[, c("expression", "min", "median", "itr/sec")]## # A tibble: 2 × 4
## expression min median `itr/sec`
## <bch:expr> <bch:tm> <bch:tm> <dbl>
## 1 fn 6.8ms 6.96ms 143.
## 2 gr 31.6ms 32.22ms 31.0
Likelihood component breakdown
## lp_prior: 0
## lp_penalty: 0
## lp_rec: 205.8579
## lp_cpue: 490.8378 (sum of 268 obs)
## lp_lf: 133665.5 (sum of 210 obs)
## lp_wf: 718157.9 (sum of 112 obs)
## Total NLL: 852582
Save or compare
if (!compare) {
# ---- Save baseline ----
baseline <- list(
timestamp = Sys.time(),
description = "Pre-refactoring baseline",
platform = .Platform$OS.type,
opal_version = as.character(packageVersion("opal")),
dimensions = list(
n_year = data$n_year,
n_age = data$n_age,
n_fishery = data$n_fishery,
n_season = data$n_season,
n_len = data$n_len,
n_lf = data$n_lf,
n_wt = data$n_wt,
n_wf = data$n_wf
),
nll = nll,
gradient = gr,
max_gr = max(abs(gr)),
timing = list(
fn_elapsed = t_fn["elapsed"],
gr_elapsed = t_gr["elapsed"],
fn_median = as.numeric(bm$median[1]),
gr_median = as.numeric(bm$median[2])
),
report = list(
number_ysa = rep$number_ysa,
spawning_biomass_y = rep$spawning_biomass_y,
catch_pred_fya = rep$catch_pred_fya,
catch_pred_ysf = rep$catch_pred_ysf,
hrate_ysa = rep$hrate_ysa,
hrate_ysfa = rep$hrate_ysfa,
sel_fya = rep$sel_fya,
lp_prior = rep$lp_prior,
lp_penalty = rep$lp_penalty,
lp_rec = rep$lp_rec,
lp_cpue = rep$lp_cpue,
lp_lf = rep$lp_lf,
lp_wf = rep$lp_wf
),
par_values = obj$par
)
opal_baseline <- baseline
save(opal_baseline, file = baseline_file)
opal_baseline_data <- data
save(opal_baseline_data, file = baseline_data_file)
opal_baseline_parameters <- parameters
save(opal_baseline_parameters, file = baseline_parameters_file)
opal_baseline_map <- map
save(opal_baseline_map, file = baseline_map_file)
cat("Baseline saved to:", baseline_file, "\n")
cat(" Platform: ", .Platform$OS.type, "\n")
cat(" NLL: ", round(nll, 6), "\n")
cat(" max|gr|: ", round(max(abs(gr)), 6), "\n")
cat(" fn median: ", round(as.numeric(bm$median[1]) * 1000, 1), "ms\n")
cat(" gr median: ", round(as.numeric(bm$median[2]) * 1000, 1), "ms\n")
} else {
# ---- Compare against baseline ----
stopifnot(file.exists(baseline_file))
data(opal_baseline)
old <- opal_baseline
cat("Baseline from:", format(old$timestamp), "\n")
cat(" Description:", old$description, "\n")
if (!is.null(old$opal_version)) {
cat(" opal version:", old$opal_version, "\n")
}
# --- Platform-aware tolerance ---
baseline_platform <- old$platform %||% "windows"
current_platform <- .Platform$OS.type
cross_platform <- baseline_platform != current_platform
if (cross_platform) {
tol <- 1e-6
cat(" Cross-platform comparison (baseline:", baseline_platform,
"-> current:", current_platform, ")\n")
cat(" Using relaxed tolerance:", format(tol, scientific = TRUE), "\n")
} else {
tol <- 1e-10
cat(" Same-platform comparison (", current_platform, ")\n")
cat(" Using strict tolerance:", format(tol, scientific = TRUE), "\n")
}
cat("\n")
# --- Numerical equivalence ---
checks <- list(
nll = abs(nll - old$nll),
number_ysa = max(abs(rep$number_ysa - old$report$number_ysa)),
spawning_biomass_y = max(abs(rep$spawning_biomass_y - old$report$spawning_biomass_y)),
catch_pred_fya = max(abs(rep$catch_pred_fya - old$report$catch_pred_fya)),
catch_pred_ysf = max(abs(rep$catch_pred_ysf - old$report$catch_pred_ysf)),
hrate_ysa = max(abs(rep$hrate_ysa - old$report$hrate_ysa)),
hrate_ysfa = max(abs(rep$hrate_ysfa - old$report$hrate_ysfa)),
lp_penalty = abs(rep$lp_penalty - old$report$lp_penalty),
lp_rec = abs(rep$lp_rec - old$report$lp_rec),
lp_cpue = max(abs(rep$lp_cpue - old$report$lp_cpue)),
lp_lf = max(abs(rep$lp_lf - old$report$lp_lf)),
lp_wf = max(abs(rep$lp_wf - old$report$lp_wf)),
max_gradient = abs(max(abs(gr)) - old$max_gr)
)
cat("--- Numerical equivalence ---\n")
all_pass <- TRUE
for (nm in names(checks)) {
val <- checks[[nm]]
pass <- val < tol
flag <- ifelse(pass, "OK", "FAIL")
cat(sprintf(" %-22s %12.2e [%s]\n", nm, val, flag))
if (!pass) all_pass <- FALSE
}
cat("\nOverall:", ifelse(all_pass, "PASS", "FAIL"), "\n\n")
# --- Timing comparison ---
cat("--- Timing comparison ---\n")
cat(sprintf(" fn: %.1f ms -> %.1f ms (%.1fx)\n",
old$timing$fn_median * 1000,
as.numeric(bm$median[1]) * 1000,
old$timing$fn_median / as.numeric(bm$median[1])))
cat(sprintf(" gr: %.1f ms -> %.1f ms (%.1fx)\n",
old$timing$gr_median * 1000,
as.numeric(bm$median[2]) * 1000,
old$timing$gr_median / as.numeric(bm$median[2])))
# --- Likelihood component comparison ---
cat("\n--- Likelihood components ---\n")
cat(sprintf(" %-12s %12s %12s %12s\n", "Component", "Baseline", "Current", "Diff"))
components <- c("lp_prior", "lp_penalty", "lp_rec")
for (nm in components) {
old_val <- old$report[[nm]]
new_val <- rep[[nm]]
cat(sprintf(" %-12s %12.4f %12.4f %12.2e\n", nm, old_val, new_val, abs(new_val - old_val)))
}
sum_components <- c("lp_cpue", "lp_lf", "lp_wf")
for (nm in sum_components) {
old_val <- sum(old$report[[nm]])
new_val <- sum(rep[[nm]])
cat(sprintf(" %-12s %12.4f %12.4f %12.2e\n", nm, old_val, new_val, abs(new_val - old_val)))
}
cat(sprintf(" %-12s %12.4f %12.4f %12.2e\n", "Total NLL", old$nll, nll, abs(nll - old$nll)))
}## Baseline from: 2026-03-03 21:20:32
## Description: Pre-refactoring baseline
## opal version: 0.0.3
## Cross-platform comparison (baseline: windows -> current: unix )
## Using relaxed tolerance: 1e-06
##
## --- Numerical equivalence ---
## nll 0.00e+00 [OK]
## number_ysa 3.73e-09 [OK]
## spawning_biomass_y 1.19e-07 [OK]
## catch_pred_fya 2.27e-12 [OK]
## catch_pred_ysf 1.46e-11 [OK]
## hrate_ysa 5.42e-19 [OK]
## hrate_ysfa 5.42e-19 [OK]
## lp_penalty 4.93e-32 [OK]
## lp_rec 0.00e+00 [OK]
## lp_cpue 6.93e-14 [OK]
## lp_lf 1.46e-11 [OK]
## lp_wf 2.91e-11 [OK]
## max_gradient 0.00e+00 [OK]
##
## Overall: PASS
##
## --- Timing comparison ---
## fn: 0.0 ms -> 7.0 ms (0.0x)
## gr: 16.1 ms -> 32.2 ms (0.5x)
##
## --- Likelihood components ---
## Component Baseline Current Diff
## lp_prior 0.0000 0.0000 0.00e+00
## lp_penalty -0.0000 -0.0000 4.93e-32
## lp_rec 205.8579 205.8579 0.00e+00
## lp_cpue 490.8378 490.8378 2.27e-13
## lp_lf 133665.5125 133665.5125 2.91e-11
## lp_wf 718157.9275 718157.9275 1.16e-10
## Total NLL 852582.0320 852582.0320 0.00e+00