OMMP16 model fit review

1 Overview

NoteHistorical checkpoint

This page records the model-fit review undertaken around OMMP16. It is not the final ESC31 assessment report: later scientific decisions, accepted MCMCs, the nine-cell grid, and the 2,000-draw projections are documented in the ESC31 workflow. In particular, the selected ESC31 base combines the 20% baseline CPUE CV with the raw year-specific GAM22 estimation CV; it does not use the scaled-CV sensitivity described below.

This report presents the OMMP16 operating model fit prepared from the 2026 input files. It includes the maximum-likelihood model setup, estimated parameters, selectivity hyper-parameters, model-fit plots, residual diagnostics, and population-dynamics plots.

The CCSBT data page describes the Commission data holdings used to support assessment inputs, including catch, catch-and-effort, catch-at-size, and related monitoring data (Commission for the Conservation of Southern Bluefin Tuna 2026). The latest CCSBT ESC data-input papers found at the time of drafting were the 2025 ESC30 updates, including CSIRO close-kin mark-recapture and gene-tagging program updates (Farley et al. 2025; Preece et al. 2025), the GAM22 CPUE input index (Itoh and Takahasi 2025), and fisheries indicators (Patterson 2025).

2 Input data

Code
data_loc <- file.path("..", "ESC31", "csv_2026")

length_mean <- read_csv(file.path(data_loc, "mean_length.csv"), show_col_types = FALSE)
length_sd <- read_csv(file.path(data_loc, "sd_length.csv"), show_col_types = FALSE)
catch <- read_csv(file.path(data_loc, "catch.csv"), show_col_types = FALSE)
catch_UA <- read_csv(file.path(data_loc, "catch_UA.csv"), show_col_types = FALSE)
scenarios_surface <- read_csv(file.path(data_loc, "scenarios_surface.csv"), show_col_types = FALSE)
scenarios_LL1 <- read_csv(file.path(data_loc, "scenarios_LL1.csv"), show_col_types = FALSE)
POPs <- read_csv(file.path(data_loc, "POPs.csv"), show_col_types = FALSE)
HSPs <- read_csv(file.path(data_loc, "HSPs.csv"), show_col_types = FALSE)
GTs <- read_csv(file.path(data_loc, "GTs.csv"), show_col_types = FALSE)
troll <- read_csv(file.path(data_loc, "trolling_index.csv"), show_col_types = FALSE)
cpue <- read_csv(file.path(data_loc, "cpue.csv"), show_col_types = FALSE)
cpue_cv <- read_csv(file.path(data_loc, "CV_out_10.csv"), show_col_types = FALSE)
age_freq <- read_csv(file.path(data_loc, "age_freq.csv"), show_col_types = FALSE)
length_freq <- read_csv(file.path(data_loc, "lf_assessment.csv"), show_col_types = FALSE)
Code
data_in <- list(
  last_yr = 2025,
  age_increase_M = 25,
  length_m50 = 150,
  length_m95 = 180,
  catch_surf_case = 1,
  catch_LL1_case = 1,
  length_mean = length_mean,
  length_sd = length_sd,
  catch = catch,
  catch_UA = catch_UA,
  scenarios_surf = scenarios_surface,
  scenarios_LL1 = scenarios_LL1,
  POPs = POPs,
  HSPs = HSPs,
  GTs = GTs,
  troll = troll,
  cpue = cpue,
  age_freq = age_freq,
  length_freq = length_freq,
  removal_switch_f = c(0, 0, 0, 1, 0, 0),
  sel_min_age_f = c(2, 2, 2, 8, 6, 0, 4),
  sel_max_age_f = c(17, 9, 17, 21, 25, 7, 17),
  sel_end_f = c(1, 0, 1, 1, 1, 0, 1),
  sel_LL1_yrs = c(1952, 1957, 1961, 1965, 1969, 1973, 1977, 1981, 1985, 1989, 1993, 1997, 2001, 2006, 2007, 2008, 2011, 2014, 2017, 2020, 2023),
  sel_LL2_yrs = c(1969, 2001, 2005, 2008, 2011, 2014, 2017, 2020, 2023),
  sel_LL3_yrs = c(1954, 1961, 1965, 1969, 1970, 1971, 2005, 2006, 2007),
  sel_LL4_yrs = c(1953),
  sel_Ind_yrs = c(1976, 1997, 1999, 2002, 2004, 2006, 2008, 2010, 2012:2021),
  sel_Aus_yrs = c(1952, 1969, 1973, 1977, 1981, 1985, 1989, 1993, 1997:2025),
  sel_CPUE_yrs = c(1969, 1973, 1977, 1981, 1985, 1989, 1993, 1997, 2001, 2006, 2007, 2008, 2011, 2014, 2017, 2020, 2023),
  af_switch = 1,
  lf_switch = 1,
  lf_minbin = c(1, 1, 1, 11, 6),
  cpue_switch = 1,
  cpue_a1 = 5,
  cpue_a2 = 17,
  aerial_switch = 4,
  aerial_tau = 0.3,
  troll_switch = 0,
  pop_switch = 1,
  hsp_switch = 1,
  hsp_false_negative = 0.6840729,
  gt_switch = 1,
  tag_switch = 1,
  tag_var_factor = 1.82
)

data <- get_data(data_in = data_in)
Code
input_summary <- tibble(
  `Input` = c("Assessment years", "Fisheries", "CPUE observations", "CPUE length-frequency rows", "Length-frequency rows", "Age-frequency rows", "Gene-tagging rows", "HSP rows", "POP rows", "Tag cohorts"),
  `Value` = c(
    paste(data$first_yr, data$last_yr, sep = "-"),
    data$n_fishery,
    length(data$cpue_obs),
    ifelse(is.null(data$cpue_lfs), 0, nrow(data$cpue_lfs)),
    nrow(data$lf_obs),
    nrow(data$af_obs),
    nrow(data$gt_obs),
    nrow(data$hsp_obs),
    nrow(data$pop_obs),
    data$n_K
  )
)

kable(
  replace_missing(input_summary),
  caption = "Summary of model-ready OMMP16 input data."
)
Table 1: Summary of model-ready OMMP16 input data.
Input Value
Assessment years 1931-2025
Fisheries 6
CPUE observations 57
CPUE length-frequency rows 57
Length-frequency rows 213
Age-frequency rows 88
Gene-tagging rows 8
HSP rows 116
POP rows 9005
Tag cohorts 6

The plots below show the input data before fitting. For time-varying biological inputs and composition data, the figures focus on the most recent available year and two earlier recent years to keep the review compact.

Code
plot_length_at_age(data = data, years = growth_plot_years)
Figure 1: Mean length at age by season for the first, middle, and final input years.
Code
plot_weight_at_age(data = data, years = growth_plot_years)
Figure 2: Weight at age by fishery for the first, middle, and final input years.
Code
ggplot(paly_plot_data, aes(x = .data$Age, y = .data$LengthIndex, fill = .data$Probability)) +
  geom_tile(width = 1, height = 1, color = "grey82", linewidth = 0.18, alpha = 0.86) +
  facet_wrap(~Year, nrow = 1) +
  labs(x = "Age", y = "Length (cm)", fill = "Probability") +
  scale_x_continuous(limits = bin_limits(paly_plot_data$Age, step = 1), breaks = paly_age_breaks, expand = expansion(mult = 0)) +
  scale_y_continuous(
    limits = bin_limits(paly_plot_data$LengthIndex, step = 1),
    breaks = paly_y_breaks,
    labels = function(x) round(paly_length_axis$Length[match(x, paly_length_axis$LengthIndex)]),
    expand = expansion(mult = 0)
  ) +
  scale_fill_viridis_c(option = "magma", direction = -1, limits = paly_probability_limits, oob = squish) +
  coord_cartesian(clip = "off") +
  theme(panel.grid = element_blank())
Figure 3: Conditional probability of age given length and year (paly) for the first and final non-zero PALY years. Length is shown in centimeters using PALY probabilities and model length-at-age inputs.
Code
ggplot(catch_plot_data, aes(x = .data$Year, y = .data$Catch)) +
  geom_line(color = fit_expected_color, linewidth = 0.45) +
  geom_point(color = fit_expected_color, size = 0.9) +
  facet_wrap(~Fishery, scales = "free_y", ncol = 2) +
  labs(x = "Year", y = "Catch") +
  scale_x_continuous(limits = year_limits(catch_plot_data$Year), breaks = pretty_breaks()) +
  scale_y_continuous(limits = c(0, NA), expand = expansion(mult = c(0, 0.05)))
Figure 4: Observed catch inputs by fishery.
Code
ggplot(index_plot_data, aes(x = .data$Year, y = .data$Value)) +
  geom_line(color = fit_expected_color, linewidth = 0.45) +
  geom_point(color = fit_expected_color, size = 1.2) +
  facet_wrap(~Series, scales = "free_y", ncol = 2) +
  labs(x = "Year", y = "Input value") +
  scale_x_continuous(limits = year_limits(index_plot_data$Year), breaks = pretty_breaks()) +
  scale_y_continuous(limits = c(0, NA), expand = expansion(mult = c(0, 0.05)))
Figure 5: Input index and monitoring series.
Code
ggplot(length_snapshot, aes(x = .data$Length, y = .data$Proportion, color = .data$Year)) +
  geom_line(linewidth = 0.5) +
  geom_point(size = 0.8) +
  facet_wrap(~Fishery, scales = "free_y", ncol = 2) +
  labs(x = "Length bin", y = "Proportion", color = "Year") +
  scale_x_continuous(breaks = pretty_breaks(n = 5)) +
  scale_y_continuous(limits = c(0, NA), expand = expansion(mult = c(0, 0.05)))
Figure 6: Observed length-frequency proportions for the three most recent years with length-frequency inputs.
Code
ggplot(age_snapshot, aes(x = .data$Age, y = .data$Proportion, color = .data$Year)) +
  geom_line(linewidth = 0.6) +
  geom_point(size = 1.4) +
  facet_wrap(~Fishery, scales = "free_y", ncol = 1) +
  labs(x = "Age", y = "Proportion", color = "Year") +
  scale_x_continuous(breaks = pretty_breaks(n = 6)) +
  scale_y_continuous(limits = c(0, NA), expand = expansion(mult = c(0, 0.05)))
Figure 7: Observed age-frequency proportions for the three most recent years with age-frequency inputs in each fishery.

3 Model setup

Code
parameters <- get_parameters(data = data)

parameters$par_log_h <- log(0.72)
parameters$par_log_psi <- log(1.75)
b0_start_multiplier <- 1.05
parameters$par_log_B0 <- parameters$par_log_B0 + log(b0_start_multiplier)

map <- get_map(parameters = parameters)
data$priors <- get_priors(parameters = parameters)

obj <- MakeADFun(func = cmb(sbt_model, data), parameters = parameters, map = map)
bounds <- get_bounds(obj = obj, parameters = parameters)

The initial B₀ value is increased by 5% relative to the default get_parameters() value before the objective is created. This small upward nudge gives the optimizer a less boundary-like starting biomass without changing the model structure.

Code
starting_values <- tibble(
  Parameter = parameter_labels(c("Steepness", "Psi", "B0", "M0", "M4", "M10", "M30")),
  `Starting value` = c(
    exp(parameters$par_log_h),
    exp(parameters$par_log_psi),
    exp(parameters$par_log_B0),
    exp(parameters$par_log_m0),
    exp(parameters$par_log_m4),
    exp(parameters$par_log_m10),
    exp(parameters$par_log_m30)
  )
)

kable(
  starting_values |>
    mutate(`Starting value` = format_decimal(.data$`Starting value`, digits = 3)) |>
    replace_missing(),
  align = numeric_table_align(starting_values),
  caption = "Key starting values used to initialize the OMMP16 fit."
)
Table 2: Key starting values used to initialize the OMMP16 fit.
Parameter Starting value
Steepness 0.720
Psi 1.750
B₀ 6,063,778.230
M₀ 0.422
M₄ 0.142
M₁₀ 0.115
M₃₀ 0.421
Code
estimated_parameter_names <- tibble(`Estimated parameter` = names(obj$par)) |>
  count(.data$`Estimated parameter`, name = "Number of active parameters") |>
  arrange(.data$`Estimated parameter`)

estimated_parameter_names <- bind_rows(
  estimated_parameter_names,
  tibble(
    `Estimated parameter` = "Total",
    `Number of active parameters` = length(obj$par)
  )
)

kable(
  replace_missing(estimated_parameter_names),
  align = c("l", "r"),
  caption = "Active parameter blocks in the maximum-likelihood optimization, including the number of estimated parameters in each block."
)
Table 3: Active parameter blocks in the maximum-likelihood optimization, including the number of estimated parameters in each block.
Estimated parameter Number of active parameters
par_log_B0 1
par_log_cpue_q 1
par_log_m0 1
par_log_m10 1
par_log_m30 1
par_log_m4 1
par_log_sel_1 336
par_log_sel_2 72
par_log_sel_3 144
par_log_sel_4 14
par_log_sel_5 360
par_log_sel_6 296
par_log_sel_7 238
par_rdev_y 95
Total 1561

4 Maximum-likelihood fit

Code
control <- list(eval.max = 10000, iter.max = 10000)

initial_nll <- obj$fn(obj$par)
opt <- nlminb(
  start = obj$par,
  objective = obj$fn,
  gradient = obj$gr,
  hessian = obj$he,
  lower = bounds$lower,
  upper = bounds$upper,
  control = control
)
opt <- nlminb(
  start = opt$par,
  objective = obj$fn,
  gradient = obj$gr,
  hessian = obj$he,
  lower = bounds$lower,
  upper = bounds$upper,
  control = control
)
obj$par <- opt$par
obj$env$last.par.best <- opt$par
final_nll <- obj$fn(opt$par)
max_gradient <- max(abs(obj$gr(opt$par)))
Code
optimisation_summary <- tibble(
  Section = "Optimization",
  Statistic = c("Initial negative log-likelihood", "Final negative log-likelihood", "Convergence code", "Convergence message", "Maximum absolute gradient"),
  Value = c(initial_nll, final_nll, opt$convergence, opt$message, max_gradient)
)

estimability_table <- estimability_summary |>
  pivot_longer(everything(), names_to = "Statistic", values_to = "Value") |>
  mutate(Section = "Estimability", .before = 1)

optimisation_estimability_summary <- bind_rows(
  optimisation_summary,
  estimability_table
)

kable(
  format_table(optimisation_estimability_summary) |>
    replace_missing(),
  caption = "Maximum-likelihood optimization and estimability summary. Estimability was checked using the AD Hessian at the maximum-likelihood estimate."
)
Table 4: Maximum-likelihood optimization and estimability summary. Estimability was checked using the AD Hessian at the maximum-likelihood estimate.
Section Statistic Value
Optimization Initial negative log-likelihood 42123802814153.2
Optimization Final negative log-likelihood 7298.62279830427
Optimization Convergence code 1
Optimization Convergence message false convergence (8)
Optimization Maximum absolute gradient 8.61373111177728
Estimability Estimability result Failed
Estimability Detail Some gradients are high, please improve optimization and only then use Check_Identifiable

In Table 4, the initial negative log-likelihood is the objective value at the starting parameter vector and the final negative log-likelihood is the value after the two nlminb() passes. A convergence code of 0 from nlminb() means the optimizer reported successful convergence; the convergence message is the corresponding optimizer stopping message. The maximum absolute gradient is a local first-order check on the optimized parameter vector, with smaller values indicating a flatter objective at the solution. The estimability rows report the check_estimability() result and the associated detail from the AD Hessian at the maximum-likelihood estimate.

WarningHistorical fit is not an accepted ESC31 result

This reconstruction stopped with convergence code 1 and maximum absolute gradient 8.61e+00. It therefore fails the current ESC31 numerical acceptance gates and is retained only as an OMMP16 diagnostic checkpoint. The accepted base fit and posterior are reported in the ESC31 base-model page.

5 Parameter tables

Code
estimated_parameters <- make_parameter_table(
  obj = obj,
  data = data,
  exclude_recruitment_devs = TRUE,
  include_selectivity_devs = FALSE,
  include_details = TRUE
) |>
  filter(.data$Estimated) |>
  select(any_of(c("Parameter", "Model scale", "Value", "Lower", "Upper", "Prior", "Prior par1", "Prior par2"))) |>
  rename(
    Distribution = Prior,
    par1 = `Prior par1`,
    par2 = `Prior par2`
  ) |>
  mutate(Parameter = parameter_labels(.data$Parameter))

kable(
  format_decimal_table(estimated_parameters, digits = 3) |>
    replace_missing(),
  align = numeric_table_align(estimated_parameters),
  caption = "Estimated non-recruitment, non-annual-selectivity model parameters. Log-scale parameters are shown on natural scale in the Value column."
) |>
  add_header_above(c(" " = 5, "Prior" = 3))
Table 5: Estimated non-recruitment, non-annual-selectivity model parameters. Log-scale parameters are shown on natural scale in the Value column.
Prior
Parameter Model scale Value Lower Upper Distribution par1 par2
B₀ 15.470 5,231,176.336 100,000.000 1,000,000,000.000
CPUE q -0.035 0.966 0.000 10,000.000
M₀ -0.857 0.424 0.250 0.700 normal 0.400 1.500
M₁₀ -2.048 0.129 0.029 0.210 normal 0.100 0.600
M₃₀ -0.907 0.404 0.200 0.700 normal 0.457 1.500
M₄ -1.988 0.137 0.120 0.240 normal 0.167 1.500
Code
par_list <- obj$env$parList(obj$env$last.par.best)
fishery_names <- c("LL1", "LL2", "LL3", "LL4", "Indonesia", "Australia", "CPUE")

selectivity_hyperparameters <- tibble(
  Fishery = fishery_names,
  `ρ year` = sel_rho_from_par(par_list$par_sel_rho_y),
  `ρ age` = sel_rho_from_par(par_list$par_sel_rho_a),
  `σ` = exp(par_list$par_log_sel_sigma)
)

kable(
  format_table(selectivity_hyperparameters) |>
    replace_missing(),
  caption = "Selectivity hyper-parameters by fishery on natural scale. Status columns are omitted because these hyper-parameters are fixed by the active parameter map for this fit."
)
Table 6: Selectivity hyper-parameters by fishery on natural scale. Status columns are omitted because these hyper-parameters are fixed by the active parameter map for this fit.
Fishery ρ year ρ age σ
LL1 0.7 0.9 0.31
LL2 0.7 0.5 0.5
LL3 0.5 0.5 0.75
LL4 0.5 0.5 0.75
Indonesia 0.98 0.98 0.12
Australia 0.5 0.5 1.13
CPUE 0.95 0.9 0.23

6 Model fit plots

6.1 Fishery and index fits

Code
fit_plots$catch$plot
Figure 8: Input and output catch by fishery and season. Orange points are input catch; blue lines are model output catch. Catch is an input, not a fitted observation.
Code
fit_plots$cpue_fit$plot
Figure 9: Observed and predicted CPUE index values with 95% intervals from the input CPUE standard deviations plus the fitted model observation error. Orange points are observed values and blue lines are expected values.
Code
fit_plots$aerial_fit$plot
Figure 10: Observed and predicted aerial survey index values with 95% intervals from the aerial covariance matrix plus the fitted model observation error. Orange points are observed values and blue lines are expected values.
Code
fit_plots$troll_fit$plot
Figure 11: Observed and predicted troll index values with 95% intervals from the input troll standard deviations plus the fixed troll observation error. Orange points are observed values and blue lines are expected values. The troll index is switched off in this fit, so this plot is diagnostic only and does not contribute to the objective function.
Code
fit_plots$gt_fit$plot
Figure 12: Observed and expected gene-tagging (GT) matches by release year with 95% binomial intervals. Orange points are observed matches, the solid blue line is expected matches, and dashed blue lines are the lower and upper 95% binomial bounds.
Code
fit_plots$hsp_pop_cohort$plot
Figure 13: Observed and predicted parent-offspring-pair matches (POP) by juvenile cohort. Orange points are observed matches and blue lines and intervals are expected matches.
Code
fit_plots$hsp_pop_age$plot
Figure 14: Observed and predicted parent-offspring-pair matches (POP) by adult capture age. Orange points are observed matches and blue lines and intervals are expected matches.
Code
fit_plots$hsp_pop_year$plot
Figure 15: Observed and predicted parent-offspring-pair matches (POP) by adult capture year. Orange points are observed matches and blue lines and intervals are expected matches.
Code
fit_plots$hsp_pair$plot
Figure 16: Observed and predicted half-sibling-pair matches (HSP) by cohort pair. Orange points are observed matches and blue lines and intervals are expected matches.
Code
fit_plots$hsp_initial$plot
Figure 17: Observed and predicted half-sibling-pair matches (HSP) by initial cohort. Orange points are observed matches and blue lines and intervals are expected matches.
Code
fit_plots$hsp_total$plot
Figure 18: Observed and predicted total parent-offspring-pair (POP), half-sibling-pair (HSP), and gene-tagging (GT) matches. Orange points are observed matches and blue points and intervals are expected matches.

6.2 Composition fits

Code
fit_plots$lf_ll1$plot
Figure 19: Length-composition fit for LL1. Orange points are observed proportions and blue dashed lines are expected proportions.
Code
fit_plots$lf_ll2$plot
Figure 20: Length-composition fit for LL2. Orange points are observed proportions and blue dashed lines are expected proportions.
Code
fit_plots$lf_ll3$plot
Figure 21: Length-composition fit for LL3. Orange points are observed proportions and blue dashed lines are expected proportions.
Code
fit_plots$lf_ll4$plot
Figure 22: Length-composition fit for LL4. Orange points are observed proportions and blue dashed lines are expected proportions.
Code
fit_plots$lf_cpue$plot
Figure 23: Length-composition fit for the CPUE length-frequency data. Orange points are observed proportions and blue dashed lines are expected proportions.
Code
fit_plots$af_indonesia$plot
Figure 24: Age-composition fit for the Indonesian fishery. Orange points are observed proportions and blue dashed lines are expected proportions.
Code
fit_plots$af_australia$plot
Figure 25: Age-composition fit for the Australian fishery. Orange points are observed proportions and blue dashed lines are expected proportions.

6.3 Selectivity fits

Selectivity panels are restricted to the configured sel_*_yrs values for each fishery. The default plot spans every model year, which creates large gaps because selectivity is only configured for selected years.

Code
fit_plots$selectivity_ll1$plot
Figure 26: Estimated selectivity-at-age for LL1. Dashed vertical lines mark the minimum and maximum ages over which selectivity is estimated; crosses mark years with composition observations shown in Figure 19.
Code
fit_plots$selectivity_ll2$plot
Figure 27: Estimated selectivity-at-age for LL2. Dashed vertical lines mark the minimum and maximum ages over which selectivity is estimated; crosses mark years with composition observations shown in Figure 20.
Code
fit_plots$selectivity_ll3$plot
Figure 28: Estimated selectivity-at-age for LL3. Dashed vertical lines mark the minimum and maximum ages over which selectivity is estimated; crosses mark years with composition observations shown in Figure 21.
Code
fit_plots$selectivity_indonesian$plot
Figure 29: Estimated selectivity-at-age for the Indonesian fishery. Dashed vertical lines mark the minimum and maximum ages over which selectivity is estimated; crosses mark years with composition observations shown in Figure 24.
Code
fit_plots$selectivity_australian$plot
Figure 30: Estimated selectivity-at-age for the Australian fishery. Dashed vertical lines mark the minimum and maximum ages over which selectivity is estimated; crosses mark years with composition observations shown in Figure 25.
Code
fit_plots$selectivity_cpue$plot
Figure 31: Estimated selectivity-at-age for the CPUE fleet. Dashed vertical lines mark the minimum and maximum ages over which selectivity is estimated; crosses mark years with composition observations shown in Figure 23. The high value at the first displayed age is an edge effect from the lower bound of the CPUE selectivity window rather than evidence for an additional younger-age mode.

7 Residual diagnostics

One-step-ahead (OSA) residuals provide randomized quantile-style diagnostics for non-Gaussian observations and are useful for assessing stock-assessment fit across observation types (Dunn and Smyth 1996; Stewart and Monnahan 2025). For composition data, the Francis and McAllister-Ianelli diagnostics summarize whether the input sample sizes are broadly consistent with the dispersion in the composition residuals (Francis 2011; McAllister and Ianelli 1997).

The scalar likelihood components use oneStepPredict() because each observation can be evaluated directly as a univariate OSA residual. The composition and tag recapture likelihoods are constrained multivariate observations, so compResidual is used when the residuals need to respect the simplex and Dirichlet-multinomial structure or when oneStepPredict() returns non-finite composition residuals. The residual source used for each data type is also reported in Table 7.

For normalized residuals, SDNR values near 1 indicate that the spread of the residuals is broadly consistent with the assumed observation error. High SDNR values indicate residuals that are more variable than expected, which can point to lack of fit or observation errors that are too small. Low SDNR values indicate residuals that are less variable than expected, which can point to observation errors that are too large or an overly down-weighted data set. MAR is the median absolute residual; values near 0.67 are expected for standard normal residuals, with larger values indicating larger typical residuals and smaller values indicating smaller typical residuals.

Code
kable(
  residual_statistics |>
    rename(
      Estimate = SDNR,
      `Lower 95%` = `SDNR lower 95%`,
      `Upper 95%` = `SDNR upper 95%`,
      `SDNR` = `Francis SDNR`,
      `N multiplier` = `Francis N multiplier`,
      `Harmonic N` = `McAllister-Ianelli harmonic N`,
      `Median N` = `McAllister-Ianelli median N`,
      `Effective/input N` = `McAllister-Ianelli/Input N`
    ) |>
    mutate(Source = format_residual_source(.data$Source)) |>
    format_residual_table(digits = 3) |>
    replace_missing(),
  align = c("l", rep("r", 5), "l", rep("r", 6)),
  caption = "Residual diagnostic statistics by data type. SDNR is the standard deviation of normalized residuals; MAR is the median absolute residual. Francis and McAllister-Ianelli diagnostics are reported for age- and length-composition data where available."
) |>
  add_header_above(c(" " = 2, "SDNR" = 3, " " = 2, "Francis" = 2, "McAllister-Ianelli" = 4))
Table 7: Residual diagnostic statistics by data type. SDNR is the standard deviation of normalized residuals; MAR is the median absolute residual. Francis and McAllister-Ianelli diagnostics are reported for age- and length-composition data where available.
SDNR
Francis
McAllister-Ianelli
Data type N Estimate Lower 95% Upper 95% MAR Source SDNR N multiplier Harmonic N Median N Mean input N Effective/input N
CPUE index 57 0.825 0.697 1.013 0.615 oneStepPredict
Aerial survey 20 0.991 0.754 1.448 0.432 oneStepPredict
Troll survey 29 1.467 1.164 1.984 0.849 likelihood-scale standardized
Gene tagging 8 0.837 0.554 1.704 0.600 oneStepPredict
Half-sibling pairs 116 1.073 0.951 1.232 0.704 oneStepPredict
Close-kin POPs 9005 0.993 0.979 1.008 0.658 oneStepPredict
Conventional tags 232 1.100 1.008 1.211 0.656 compResidual
Length composition: LL1 1776 0.544 0.526 0.562 0.350 compResidual 1.074 0.867 220.899 440.026 68.982 3.202
Length composition: LL2 648 0.634 0.601 0.671 0.418 compResidual 0.783 1.630 81.739 166.578 8.840 9.246
Length composition: LL3 480 0.714 0.672 0.763 0.477 compResidual 0.876 1.302 82.217 162.139 34.271 2.399
Length composition: CPUE 1083 0.620 0.595 0.647 0.367 compResidual 0.771 1.684 254.165 294.893 80.103 3.173
Age composition: Indonesian 624 0.620 0.587 0.656 0.406 compResidual 1.036 0.932 222.975 225.563 86.600 2.575
Age composition: Australian 434 0.774 0.725 0.829 0.417 compResidual 0.672 2.211 64.433 167.350 27.606 2.334
Code
residual_plots$cpue$plot
Figure 32: One-step-ahead (OSA) residuals for the CPUE index, derived with oneStepPredict on the lognormal CPUE observation.
Code
residual_plots$aerial$plot
Figure 33: One-step-ahead (OSA) residuals for the aerial survey index, derived with oneStepPredict on the lognormal aerial-survey observation.
Code
residual_plots$troll$plot
Figure 34: Residual diagnostics for the troll index, derived as likelihood-scale standardized lognormal residuals. The troll index is switched off in this fit, so this panel records that status rather than a fitted residual pattern.
Code
residual_plots$gt$plot
Figure 35: One-step-ahead (OSA) residuals for gene-tagging recaptures, derived with oneStepPredict on the binomial GT recapture observation.
Code
residual_plots$hsp$plot
Figure 36: One-step-ahead (OSA) residuals for half-sibling-pair observations, derived with oneStepPredict on the binomial HSP observation.
Code
residual_plots$pop$plot
Figure 37: One-step-ahead (OSA) residual map for close-kin POP observations, derived with oneStepPredict on the binomial POP observation and averaged within release-cohort and adult-capture-year cells.
Code
residual_plots$tags$plot
Figure 38: Residual diagnostics for conventional tag recaptures, derived with compResidual for the Dirichlet-multinomial recapture categories.
Code
residual_plots$lf_ll1$plot
Figure 39: Length-composition residual diagnostics for LL1, derived with oneStepPredict where finite and the compResidual fallback otherwise.
Code
residual_plots$lf_ll2$plot
Figure 40: Length-composition residual diagnostics for LL2, derived with oneStepPredict where finite and the compResidual fallback otherwise.
Code
residual_plots$lf_ll3$plot
Figure 41: Length-composition residual diagnostics for LL3, derived with oneStepPredict where finite and the compResidual fallback otherwise.
Code
residual_plots$lf_cpue$plot
Figure 42: Length-composition residual diagnostics for CPUE length-frequency data, derived with oneStepPredict where finite and the compResidual fallback otherwise.
Code
residual_plots$af_indonesia$plot
Figure 43: Age-composition residual diagnostics for the Indonesian fishery, derived with oneStepPredict where finite and the compResidual fallback otherwise.
Code
residual_plots$af_australia$plot
Figure 44: Age-composition residual diagnostics for the Australian fishery, derived with oneStepPredict where finite and the compResidual fallback otherwise.

7.1 Population dynamics

Code
fit_plots$initial_numbers$plot
Figure 45: Initial numbers at age from the maximum-likelihood fit.
Code
fit_plots$natural_mortality$plot
Figure 46: Estimated natural mortality at age from the maximum-likelihood fit.
Code
fit_plots$recruitment_deviates$plot
Figure 47: Recruitment deviates from the maximum-likelihood fit. The black dashed horizontal line marks zero; orange points identify the final three recruitment deviations penalized with the AR1 prior, and blue points identify deviations penalized with independent normal priors.
Code
fit_plots$recruitment$plot
Figure 48: Recruitment trajectory from the maximum-likelihood fit. The black dashed horizontal line is unfished recruitment; orange points identify the final three recruitment values associated with the AR1 recruitment-deviation prior, and blue points identify earlier recruitment values.

8 CPUE sensitivity fits

The historical sensitivity fits were configured in script_OMMP16.R and saved to sensitivity/ommp16_sensitivities.rds when run. That result file is not retained in the current checkout. The figures below therefore display labelled unavailable panels so the historical report remains reproducible; they are not unfinished ESC31 outputs and should not be regenerated for current sign-off. The accepted current sensitivity set is reported in the ESC31 sensitivity page.

Code
sensitivity_status <- tibble(
  `Output file` = sensitivity_results_path,
  Status = ifelse(
    file.exists(sensitivity_results_path),
    "Historical output file found",
    "Historical output not retained; see ESC31/3_sens.html"
  )
)

kable(
  replace_missing(sensitivity_status),
  caption = "Sensitivity-output status for the report."
)
Table 8: Sensitivity-output status for the report.
Output file Status
sensitivity/ommp16_sensitivities.rds Historical output not retained; see ESC31/3_sens.html
Code
kable(
  sensitivity_summary |>
    mutate(across(c(B0, M0, M4, M10, M30, h, psi), ~ format_decimal(.x, digits = 3))) |>
    format_decimal_table(digits = 3) |>
    replace_missing(),
  align = c("l", rep("r", ncol(sensitivity_summary) - 1)),
  caption = "Maximum-likelihood comparison for the base model and available CPUE sensitivity fits. Convergence code 0 indicates normal nlminb convergence."
)
Table 9: Maximum-likelihood comparison for the base model and available CPUE sensitivity fits. Convergence code 0 indicates normal nlminb convergence.
Model Convergence code NLL Max gradient B0 M0 M4 M10 M30 h psi
Base OMMP16 1.000 7,298.623 8.614 5,231,176.336 0.424 0.137 0.129 0.404 0.720 1.750

8.1 Sensitivity 1: CPUE q split from 2008

Sensitivity 1 adds a CPUE catchability split in 2008. This checks whether a post-2008 CPUE scale shift changes the core population trajectory or key estimated parameters.

Code
sensitivity_q2008_cpue_plot <- make_plot("Sensitivity 1 CPUE fit", function() {
  if (!has_sensitivity_fit(sens_q2008)) {
    stop("Run the Sensitivity 1 section in script_OMMP16.R to create ", sensitivity_results_path, call. = FALSE)
  }
  plot_cpue(data = sens_q2008$data, object = sens_q2008$obj)
})

sensitivity_q2008_cpue_plot$plot
Figure 49: CPUE index fit for Sensitivity 1, with a catchability split from 2008. Orange points are observed values and blue lines are expected values.

8.2 Sensitivity 2: time-varying CPUE CV

Sensitivity 2 uses the year-specific CPUE CVs in CV_out_10.csv, taking the cv_scaled column as the model CV while leaving the CPUE index values from cpue.csv unchanged. The mean scaled CV across years is 0.200, matching the intended 0.20 target. The scaling preserves the relative annual pattern in cv_raw, but forces the average uncertainty level to 20% using cv_scaled = cv_raw * 0.20 / mean(cv_raw).

Code
cpue_tv_cv_input |>
  ggplot(aes(x = .data$Year, y = .data$CV)) +
  geom_line(color = fit_expected_color, linewidth = 0.6) +
  geom_point(color = fit_expected_color, size = 1.8) +
  labs(x = "Year", y = "CPUE CV") +
  scale_x_continuous(limits = year_limits(cpue_tv_cv_input$Year), breaks = pretty_breaks()) +
  scale_y_continuous(limits = c(0, NA), expand = expansion(mult = c(0, 0.05)))
Figure 50: Time-varying CPUE CV used in Sensitivity 2. Values are read from the cv_scaled column of CV_out_10.csv and aligned to cpue.csv by year.
Code
sensitivity_cpue_tv_cv_plot <- make_plot("Sensitivity 2 CPUE fit", function() {
  if (!has_sensitivity_fit(sens_cpue_tv_cv)) {
    stop("Run the Sensitivity 2 section in script_OMMP16.R to create ", sensitivity_results_path, call. = FALSE)
  }
  plot_cpue(data = sens_cpue_tv_cv$data, object = sens_cpue_tv_cv$obj)
})

sensitivity_cpue_tv_cv_plot$plot
Figure 51: CPUE index fit for Sensitivity 2, with time-varying CPUE CVs from CV_out_10.csv. Orange points are observed values and blue lines are expected values.

8.3 Sensitivity comparison

Code
sensitivity_biomass_plot <- make_plot("Sensitivity biomass comparison", function() {
  if (!has_sensitivity_fit(sens_q2008) || !has_sensitivity_fit(sens_cpue_tv_cv)) {
    stop("Run both CPUE sensitivity sections in script_OMMP16.R to create ", sensitivity_results_path, call. = FALSE)
  }
  plot_biomass_spawning(
    data_list = list(data, sens_q2008$data, sens_cpue_tv_cv$data),
    object_list = list(obj, sens_q2008$obj, sens_cpue_tv_cv$obj),
    labels = c("Base OMMP16", "CPUE q split from 2008", "Time-varying CPUE CV"),
    relative = TRUE
  )
})

sensitivity_biomass_plot$plot
Figure 52: Relative spawning-biomass comparison for the base OMMP16 fit and the two CPUE sensitivity fits.

9 Grid MCMC results

This section records the prototype grid configured in script_OMMP16.R. Its historical output directory is not retained in this checkout, so the table shows the expected legacy file layout rather than current results. It must not be confused with the later accepted ESC31 nine-cell grid, diagnostics, balanced 2,000-draw posterior, and direct-(M) grid reported in the ESC31 grid page.

Code
grid_values_report <- expand.grid(h = c(0.6, 0.7, 0.8), psi = c(1.5, 1.75, 2))
grid_mcmc_dirs <- list.dirs("grid_mcmc", recursive = FALSE, full.names = TRUE)
grid_mcmc_dir <- if (length(grid_mcmc_dirs)) {
  grid_mcmc_dirs[which.max(file.info(grid_mcmc_dirs)$mtime)]
} else {
  NA_character_
}

grid_mcmc_status <- grid_values_report |>
  mutate(
    Cell = row_number(),
    `h start` = .data$h,
    `psi start` = .data$psi,
    `Expected output` = if (is.na(grid_mcmc_dir)) {
      file.path("grid_mcmc", "RUN_ID", paste0("grid", .data$Cell, ".rda"))
    } else {
      file.path(grid_mcmc_dir, paste0("grid", .data$Cell, ".rda"))
    },
    Status = ifelse(
      !is.na(grid_mcmc_dir) & file.exists(.data$`Expected output`),
      "Historical output file found",
      "Historical output not retained"
    )
  ) |>
  select(Cell, `h start`, `psi start`, `Expected output`, Status)

kable(
  grid_mcmc_status |>
    format_decimal_table(digits = 3) |>
    replace_missing(),
  align = c("r", "r", "r", "l", "l"),
  caption = "Historical OMMP16 prototype grid cells and expected legacy output files. These are not the accepted ESC31 grid artifacts."
)
Table 10: Historical OMMP16 prototype grid cells and expected legacy output files. These are not the accepted ESC31 grid artifacts.
Cell h start psi start Expected output Status
1.000 0.600 1.500 grid_mcmc/RUN_ID/grid1.rda Historical output not retained
2.000 0.700 1.500 grid_mcmc/RUN_ID/grid2.rda Historical output not retained
3.000 0.800 1.500 grid_mcmc/RUN_ID/grid3.rda Historical output not retained
4.000 0.600 1.750 grid_mcmc/RUN_ID/grid4.rda Historical output not retained
5.000 0.700 1.750 grid_mcmc/RUN_ID/grid5.rda Historical output not retained
6.000 0.800 1.750 grid_mcmc/RUN_ID/grid6.rda Historical output not retained
7.000 0.600 2.000 grid_mcmc/RUN_ID/grid7.rda Historical output not retained
8.000 0.700 2.000 grid_mcmc/RUN_ID/grid8.rda Historical output not retained
9.000 0.800 2.000 grid_mcmc/RUN_ID/grid9.rda Historical output not retained

10 M10 profile and selectivity sensitivity

The follow-up fixed-M10 profile was run to identify which data and selectivity components resist moving natural mortality at age 10 below 0.1. The profile fixes M10, re-optimizes the remaining active maximum-likelihood parameters, and then decomposes the objective into likelihood and prior components. The profile minimum was near M10 = 0.125; values below 0.1 were meaningfully worse even after removing the direct M10 prior contribution. The fixed-profile results are summarized in Table 11, with objective-component deltas in Table 12 and fishery-level composition/selectivity deltas in Table 13.

Code
m10_profile_table <- m10_profile_summary |>
  filter(.data$status == "ok") |>
  transmute(
    `Fixed M10` = .data$M10_fixed,
    `NLL` = .data$final_nll,
    `Delta NLL` = .data$delta_nll,
    `Delta NLL without M10 prior` = .data$delta_nll_without_m10_prior,
    `Convergence code` = .data$convergence,
    `Max gradient` = .data$max_gradient
  )

kable(
  m10_profile_table |>
    format_decimal_table(digits = 3) |>
    replace_missing(),
  align = c("r", "r", "r", "r", "r", "r"),
  caption = "Fixed-M10 profile summary. Delta NLL is measured relative to the best fixed-M10 profile point. Convergence code 0 indicates normal nlminb convergence; code 1 indicates the evaluation limit was reached."
)
Table 11: Fixed-M10 profile summary. Delta NLL is measured relative to the best fixed-M10 profile point. Convergence code 0 indicates normal nlminb convergence; code 1 indicates the evaluation limit was reached.
Fixed M10 NLL Delta NLL Delta NLL without M10 prior Convergence code Max gradient
0.065 7,638.700 9.972 9.784 1.000 2.218
0.075 7,635.331 6.603 6.558 1.000 0.840
0.085 7,633.078 4.350 4.382 1.000 0.042
0.095 7,631.278 2.550 2.615 1.000 0.008
0.100 7,630.547 1.819 1.889 1.000 0.007
0.105 7,629.946 1.218 1.284 1.000 0.443
0.110 7,629.441 0.713 0.769 1.000 0.008
0.115 7,629.072 0.344 0.386 1.000 0.004
0.120 7,628.835 0.107 0.130 1.000 0.324
0.125 7,628.728 0.000 0.000 0.000 0.040
0.130 7,628.764 0.036 0.010 1.000 0.005
0.135 7,628.948 0.220 0.164 1.000 0.006
0.140 7,629.285 0.558 0.469 1.000 0.001

The profile results in Table 11 show that M10 is not being held high by the direct M10 prior. At M10 = 0.100, the total delta NLL was about 1.82 and the delta NLL after removing the M10 prior was about 1.89. At M10 = 0.085, those deltas were about 4.35 and 4.38, respectively. The resistance to lower M10 therefore comes from the likelihood and selectivity penalty rather than the M10 prior.

Code
m10_component_delta_table <- m10_component_deltas |>
  filter(abs(.data$M10_fixed - 0.100) < 1e-8) |>
  transmute(
    Component = .data$component,
    `Delta versus M10 = 0.125` = .data$delta_vs_min
  ) |>
  arrange(desc(.data$`Delta versus M10 = 0.125`))

kable(
  m10_component_delta_table |>
    format_decimal_table(digits = 3) |>
    replace_missing(),
  align = c("l", "r"),
  caption = "Objective-component deltas at fixed M10 = 0.100 relative to the profile minimum near M10 = 0.125. Positive values penalize lower M10; negative values favor lower M10."
)
Table 12: Objective-component deltas at fixed M10 = 0.100 relative to the profile minimum near M10 = 0.125. Positive values penalize lower M10; negative values favor lower M10.
Component Delta versus M10 = 0.125
selectivity 4.031
length_composition 0.931
age_composition 0.879
HSP 0.239
gene_tagging 0.035
aerial_survey 0.018
aerial_tau 0.000
troll_index 0.000
penalty 0.000
prior_without_m10 -0.007
prior_m10 -0.069
prior_total -0.076
cpue_index -0.146
cpue_length_composition -0.193
recruitment -0.433
POP -0.491
unreported_or_constant -0.673
tagging -2.301

The component decomposition in Table 12 points to the selectivity penalty and composition fits as the main resistance to M10 = 0.100. Tagging, POP, recruitment, CPUE index, and CPUE length frequencies move slightly in the opposite direction and therefore do not explain the high M10.

Code
m10_fishery_delta_table <- m10_fishery_deltas |>
  transmute(
    Component = .data$component,
    Fishery = .data$fishery,
    `Delta versus M10 = 0.125` = .data$delta_vs_0125
  ) |>
  arrange(desc(.data$`Delta versus M10 = 0.125`))

kable(
  m10_fishery_delta_table |>
    format_decimal_table(digits = 3) |>
    replace_missing(),
  align = c("l", "l", "r"),
  caption = "Fishery-level selectivity and composition deltas at M10 = 0.100 relative to M10 = 0.125. Positive values penalize lower M10."
)
Table 13: Fishery-level selectivity and composition deltas at M10 = 0.100 relative to M10 = 0.125. Positive values penalize lower M10.
Component Fishery Delta versus M10 = 0.125
selectivity LL1 3.365
age_composition Indonesia 0.759
length_composition LL1 0.604
selectivity LL3 0.576
selectivity CPUE 0.339
length_composition LL3 0.186
length_composition LL2 0.140
selectivity LL2 0.136
age_composition Australia 0.119
selectivity LL4 0.000
length_composition LL4 0.000
selectivity Indonesia -0.189
cpue_length_composition CPUE -0.194
selectivity Australia -0.196

The fishery-level decomposition in Table 13 does not support Indonesia selectivity as the main driver. The largest penalty is LL1 selectivity, followed by Indonesia age composition and LL1 length composition. Indonesia selectivity itself slightly favors lower M10, and CPUE length frequencies also slightly favor lower M10. CPUE selectivity is a secondary positive penalty, but it is much smaller than LL1 selectivity.

Code
m10_tuning_table <- m10_tuning_summary |>
  arrange(.data$final_nll) |>
  transmute(
    Variant = .data$label,
    `M10` = .data$M10_parameter,
    `NLL` = .data$final_nll,
    `Delta from baseline` = .data$delta_from_best_baseline,
    `Max gradient` = .data$max_gradient,
    `Below 0.1` = .data$below_0_1,
    `LL1 rho year` = .data$LL1_rho_y,
    `LL1 rho age` = .data$LL1_rho_a,
    `LL1 sigma` = .data$LL1_sigma,
    `CPUE sigma` = .data$CPUE_sigma,
    `Indonesia sigma` = .data$Indonesia_sigma
  )

kable(
  m10_tuning_table |>
    format_decimal_table(digits = 3) |>
    replace_missing(),
  align = c("l", rep("r", 10)),
  caption = "Fixed selectivity-hyperparameter tuning runs used to test whether M10 can be reduced below 0.1. Delta from baseline is measured relative to the best baseline continuation in this tuning set."
)
Table 14: Fixed selectivity-hyperparameter tuning runs used to test whether M10 can be reduced below 0.1. Delta from baseline is measured relative to the best baseline continuation in this tuning set.
Variant M10 NLL Delta from baseline Max gradient Below 0.1 LL1 rho year LL1 rho age LL1 sigma CPUE sigma Indonesia sigma
baseline 0.109 7,667.920 0.000 3.881 FALSE 0.700 0.900 0.310 0.230 0.120
LL1_sigma_0.500 0.111 7,731.590 63.670 1.656 FALSE 0.700 0.900 0.500 0.230 0.120
LL1_rho_0.980_sigma_0.500 0.102 7,748.317 80.397 3.523 FALSE 0.980 0.980 0.500 0.230 0.120
LL1_sigma_0.750 0.102 7,834.221 166.300 0.528 FALSE 0.700 0.900 0.750 0.230 0.120
LL1_rho_0.980_sigma_0.750 0.089 7,852.640 184.719 0.521 TRUE 0.980 0.980 0.750 0.230 0.120
LL1_rho_0.990_sigma_0.750 0.091 7,867.621 199.700 4.276 TRUE 0.990 0.990 0.750 0.230 0.120
LL1_rho_0.995_sigma_0.750 0.090 7,882.406 214.485 5.203 TRUE 0.995 0.995 0.750 0.230 0.120
LL1_CPUE_sigma_0.500 0.109 7,892.240 224.319 8.450 FALSE 0.700 0.900 0.500 0.500 0.120
LL1_sigma_1.000 0.100 7,916.131 248.210 7.363 TRUE 0.700 0.900 1.000 0.230 0.120
LL1_CPUE_rho_0.980_sigma_0.500 0.100 7,929.174 261.254 3.235 TRUE 0.980 0.980 0.500 0.500 0.120
LL1_rho_0.980_sigma_1.000 0.097 7,946.885 278.965 6.990 TRUE 0.980 0.980 1.000 0.230 0.120
LL1_rho_0.995_sigma_1.000 0.094 7,971.407 303.486 5.461 TRUE 0.995 0.995 1.000 0.230 0.120
LL1_CPUE_rho_0.980_sigma_0.750 0.088 8,120.339 452.418 1.897 TRUE 0.980 0.980 0.750 0.750 0.120
LL1_Indonesia_sigma_0.500 0.111 8,187.019 519.099 0.731 FALSE 0.700 0.900 0.500 0.230 0.500
LL1_Indonesia_CPUE_sigma_0.500 0.107 8,347.243 679.323 0.771 FALSE 0.700 0.900 0.500 0.500 0.500

The tuning results in Table 14 show that the best low-M10 fixed-hyperparameter run used LL1 rho_y = rho_a = 0.98 and LL1 sigma = 0.75. That run produced M10 = 0.089 after a longer continuation, so low M10 is numerically attainable. The cost is large: the objective was about 185 NLL units above the baseline continuation and the final maximum gradient was still about 0.52. Adding CPUE or Indonesia to the hyperparameter tuning did not improve the tradeoff. The specific longer LL1 run used for the selectivity comparison is listed in Table 15.

Code
kable(
  ll1_tuned_summary |>
    format_decimal_table(digits = 3) |>
    replace_missing(),
  align = c("l", rep("r", 6)),
  caption = "Best low-M10 LL1 fixed-hyperparameter tuning case used for the selectivity comparison."
)
Table 15: Best low-M10 LL1 fixed-hyperparameter tuning case used for the selectivity comparison.
Variant M10 NLL Max gradient LL1 rho year LL1 rho age LL1 sigma
LL1_rho_0.980_sigma_0.750 0.089 7,852.640 0.521 0.980 0.980 0.750
Code
baseline_ll1_selectivity <- plot_selectivity(
  data = data,
  object = obj,
  fisheries = "LL1",
  years = data$sel_LL1_yrs
) +
  ggtitle("Maximum-likelihood fit")

tuned_ll1_selectivity <- plot_selectivity(
  data = data,
  object = ll1_tuned_obj,
  fisheries = "LL1",
  years = data$sel_LL1_yrs
) +
  ggtitle("LL1 rho = 0.98, sigma = 0.75")

baseline_ll1_selectivity | tuned_ll1_selectivity
Figure 53: LL1 selectivity-at-age in the maximum-likelihood fit and the best low-M10 fixed-hyperparameter test. The low-M10 test fixes LL1 rho_y and rho_a at 0.98 and LL1 sigma at 0.75, producing M10 = 0.089 but with a large objective penalty.

Visually, the low-M10 LL1 test allows LL1 selectivity to move much more freely across change years and ages. The lower M10 is therefore not achieved by a small, localized adjustment to Indonesia selectivity; it is achieved by relaxing the LL1 selectivity penalty enough for the LL1 selectivity surface to absorb tension that is otherwise expressed as higher age-10 natural mortality.

References

Commission for the Conservation of Southern Bluefin Tuna. 2026. SBT Data. https://www.ccsbt.org/en/content/sbt-data.
Dunn, Peter K., and Gordon K. Smyth. 1996. “Randomized Quantile Residuals.” Journal of Computational and Graphical Statistics 5 (3): 236–44. https://doi.org/10.1080/10618600.1996.10474708.
Farley, Jessica, Paige Eveson, and Rasanthi Gunasekera. 2025. Update on the SBT Close-Kin Tissue Sampling, Processing and Kin-Finding 2025. CCSBT-ESC/2508/08. CSIRO. https://www.ccsbt.org/system/files/2025-07/ESC30_08_CCSBT_CKMR.pdf.
Francis, R. I. C. Chris. 2011. “Data Weighting in Statistical Fisheries Stock Assessment Models.” Canadian Journal of Fisheries and Aquatic Sciences 68 (6): 1124–38. https://doi.org/10.1139/f2011-025.
Itoh, T., and N. Takahasi. 2025. Update of CPUE Abundance Index Using GAM for Southern Bluefin Tuna in CCSBT (GAM22) up to the 2024 Data. CCSBT-ESC/2508/BGD 02. CCSBT.
McAllister, Murdoch K., and James N. Ianelli. 1997. “Bayesian Stock Assessment Using Catch-Age Data and the Sampling-Importance Resampling Algorithm.” Canadian Journal of Fisheries and Aquatic Sciences 54 (2): 284–300. https://doi.org/10.1139/f96-285.
Patterson, H. 2025. Fisheries Indicators for the Southern Bluefin Tuna Stock 2024–25. ABARES Technical Report 25.09; CCSBT-ESC/2508/16. Australian Bureau of Agricultural; Resource Economics; Sciences. https://doi.org/10.25814/05e4-er58.
Preece, A. L., J. P. Eveson, J. R. Hartog, et al. 2025. Update on the SBT Gene-Tagging Recruitment Monitoring Program 2025. CCSBT-ESC/2508/09. CSIRO. https://www.ccsbt.org/system/files/2025-07/ESC30_09_GT2025.pdf.
Stewart, Ian J., and Cole C. Monnahan. 2025. “Diagnosing Common Sources of Lack of Fit to Composition Data in Fisheries Stock Assessment Models Using One-Step-Ahead (OSA) Residuals.” Canadian Journal of Fisheries and Aquatic Sciences 82: 1–13. https://doi.org/10.1139/cjfas-2025-0158.