NEWS


Rceattle 5.66.1

Bug fixes

Documentation

Rceattle 5.66.0

Breaking changes

Rceattle 5.65.5

Internal

Rceattle 5.65.4

Internal

Rceattle 5.65.3

Internal

Rceattle 5.65.2

Internal

Rceattle 5.65.1

Internal

Rceattle 5.65.0

Breaking changes

Rceattle 5.64.8

Bug fixes

Rceattle 5.64.7

Internal

Rceattle 5.64.6

Internal

Rceattle 5.64.5

Bug fixes

Rceattle 5.64.4

Internal

Rceattle 5.64.3

Bug fixes

Rceattle 5.64.2

Bug fixes

Rceattle 5.64.1

Bug fixes

Rceattle 5.64.0

Breaking changes

Bug fixes

Documentation

Internal

Rceattle 5.63.2

Documentation

Rceattle 5.63.1

Documentation

Rceattle 5.63.0

Bug fixes

Internal

Rceattle 5.62.0

Deprecations

Behavior changes

Rceattle 5.61.0

Breaking changes

Rceattle 5.60.2

Documentation

Rceattle 5.60.1

Documentation

Rceattle 5.60.0

Breaking changes

Rceattle 5.59.3

Documentation

Rceattle 5.59.2

Bug fixes

Whose fits move: none. The rebuild never re-estimates anything, so no point estimate, no objective and no Mohn's rho changes, and the regression test asserts both against a getsd = FALSE run. What changes is retrospective()$Rceattle_list[[i]]$sdrep on a peel whose rebuild threw or came back unusable: previously the forecast refit's sdreport in the first case and the unusable rebuilt one in the second, now absent in both. vcov() on such a peel returns NULL, where before it returned a covariance over the peeled-year log_F alone (throw) or the rebuilt cov.fixed (unusable). The per-peel warnings also become one warning per call, so a handler matching "^Peel 3:" no longer fires; the phrase "could not report hindcast standard errors" is unchanged.

Figures change where a rebuild fails. A band that was drawn at zero width is now declined with a warning(), so plot_biomass() / plot_ssb() / plot_recruitment() with add_ci = TRUE over a peel list leave that peel's ribbon rows NA instead of collapsing the ribbon onto the line; ggplot2 then reports the removed rows at draw, so expect two warnings rather than one. Measured with a before/after ggplot_build() diff of plot_ssb(add_ci = TRUE) on a one-peel BS2017SS retrospective with the report build forced to fail -- it succeeds on an unmodified run, so the "before" state cannot be reproduced without forcing it. The ribbon layer holds 231 rows either way (3 species x 39 unpeeled years + 3 x 38 peel years), of which 114 were finite and exactly zero width before and 0 after, leaving the unpeeled model's 117 real rows. Those 114 were the fabricated band.

No bundled configuration is known to hit the failure -- measured on BS2017SS, one peel, single-species, random_rec = FALSE. Across the sibling repos 30 live retrospective() calls pass no getsd, so each inherits its input fit's setting, which is TRUE wherever that fit holds an sdrep (fit_control()'s default); the random-effect peels there are where a failure is most likely.

Rceattle 5.59.1

Documentation

Rceattle 5.59.0

Behavior changes

Bug fixes

Whose MSEs move: estDynamics is a control-sheet column, so a sweep of .R files cannot clear this. Rceattle-models/Pacific hake/04-mse.R is in the affected configuration: its workbook sets estDynamics = c(0, 1, 1), the script projects to 2025, and NByageFixed covers species 2 only to 2019 and species 3 only to 2024. Those two species now contribute no biomass and no predation in the uncovered years -- a missing row reads as zero, which is the intended meaning, not a number carried forward -- and both prey on hake, so that MSE's projected predation mortality on hake moves. (Neither has a fleet in that workbook, so no catch is lost.) MSE_yr2024.R is unaffected: its coverage runs to 2066, which also means the Pacific hake MSE gate does not exercise this change. An MSE that set estDynamics was previously running on invented numbers for every species but the first, and on flat-held numbers for that one.

Rceattle 5.58.0

Bug fixes

Whose fits move: any fit that sets an intercept init or bounds through a sel or q linkage on a fleet that shares its block. Nothing in the bundled datasets or the sibling assessment scripts does -- the GOA cod bridge's three intercept inits are all unstratified growth rows, and its selectivity specs are intercept-free -- so no shipped configuration changes. A fit that does set one was starting somewhere nobody asked for and will now start where it was told.

Rceattle 5.57.3

Bug fixes

Nothing shipping moves. Every Growth_Age_for_L1 in the 188 SS3 control files reachable here is 0.5 or an integer (1 x113, 0 x44, 0.5 x30, 3 x1), and for all of them the new gate reroutes no slot at any minage 0-4 or nages 2-20; the max(0.5, minage) default is likewise bit-identical for minage 0-4. Two mechanisms make that so: at an integer anchor the ramp ends exactly at l1, so the recursion already inherited a curve value, and at a sub-year anchor the first slot above it is Jan-1 age 1, which max(minage, 1) names when minage <= 1.

Both arms are pinned by new blocks in test-growth-vonbert-year-invariance.R; the non-integer block fails 10 assertions on the parent commit, which is also testthat's cap. /golden-check is not evidence here -- BS2017SS and GOA2018SS carry no growth_model, so estimate_growth() is never entered; it passed as a no-regression check on everything else.

Rceattle 5.57.2

Bug fixes

This is NOT a no-op, and the reason matters more than the claim. minage == 1 is bit-identical, unconditionally -- but minage = 0 is live, since it is the SS3 converter's own default and both the GOA and AI cod bridges pass it with parametric growth. What keeps those two unchanged is configuration, not the minage == 1 premise: GOA cod's anchor is 0.5 and its largest fleet month is 6, so fracyr equals the anchor exactly and the first branch's <= wins -- a margin of one month, and an August survey would move its age-0 length, ALK and weight-at-age. AI cod is safe for a different reason, an integral anchor of 1.0, and only a fractional anchor can diverge at minage = 0. Nothing live combines minage >= 2 with parametric growth.

The arm fires exactly when age_L1 is non-integer, fracyr > age_L1 - floor(age_L1), and floor(age_L1) lies in [minage, minage + nages - 1]; at most one age slot ever matches.

/golden-check cannot see this change at all. Every bundled dataset resolves to empirical weight-at-age, and the routine is gated behind growth_model(sp) > 0, so none of the four reference models enters it. A green golden run is not evidence here; test-growth-*.R is the cover.

Internal

Rceattle 5.57.1

Documentation

Rceattle 5.57.0

Behavior changes

Breaking changes

Four configurations a mirrored block cannot honour are now refused. One coefficient equalises a group's offsets only where every member has one. The base parameter is shared, but the offset it scales accumulates per fleet (inf_offset(param, flt, sex, yr)), so a row naming one member moves that member alone -- fitted, naming the lead alone leaves the pair 0.265 apart and the follower alone 0.248. "Put it on the lead fleet" is therefore right only for a prior, which is re-targeted onto the shared base; every param code and the q offset write a per-fleet slot, the apical height included. .stop_if_mirrored_block_linkage() refuses:

An intercept row is exempt from coverage and agreement: it re-targets the shared base and is pinned to NA, so it cannot diverge, and .stop_if_shared_block() already governs it.

Nothing shipping is refused. Of 104 linkage_spec() call sites in the sibling repositories, 13 declare a random-effect structure and the GOA pollock 2025 scripts do pass spec lists, so both shapes are live code -- but that workbook has no index group with two or more members whose block is estimated (its one shared group pairs a survey with an Off fleet). The SS3-bridged GOA Pacific cod bridge, which does share both indices across its *_ae1 fleets, was run against this branch by the session that owns it: clean in three configurations, forward-pass objective 1912.2067 in all three, and the baseline console output byte-identical to the same run before these rules. That is a build and forward pass at estimateMode = 3, not a fit, and that model happens not to exercise the Off-member case; GOA2018SS Selectivity_index 1 is the fixture that does.

Bug fixes

Internal

Rceattle 5.56.0

Behavior changes

Internal

Rceattle 5.55.0

Breaking changes

Internal

Rceattle 5.54.3

Bug fixes

Internal

Rceattle 5.54.2

Internal

Bug fixes

Rceattle 5.54.1

Bug fixes

Rceattle 5.54.0

New features

Rceattle 5.53.2

Bug fixes

Rceattle 5.53.1

Bug fixes

Internal

Rceattle 5.53.0

Breaking changes

Bug fixes

Rceattle 5.52.0

Bug fixes

Rceattle 5.51.0

Bug fixes

Who was exposed. The defect needs a multispecies model that mixes one-sex and two-sex species, since a uniform nsex leaves no padding at all. Of the bundled datasets only GOA2018SS qualifies (nsex c(1, 2, 1)), and single-species models are immune whatever their nsex. The live GOA multispecies assessment is in the affected configuration: its 2025 workbook has nsex c(1, 2, 1) and R/02_fit_models.R sets M1_model = c("sex_age_invariant", "sex_specific", "sex_age_invariant"), so species 1 and 3 are affected and species 2 is not. Of its three fits, the fixed-M single-species one runs M1_model = 0 and is immune, the estimated-M single-species one is phase = TRUE, and the headline multispecies fit is phase = FALSE -- the setting under which GOA2018SS moved 10.5 nats and 8.75% of species-1 SSB. Its own fit was not refitted; that was a deliberate decision, taken when this entry still said phasing made the point moot, which for the multispecies fit it does not. Whether its numbers move is unmeasured, and nothing above licenses an expectation either way: on the same data M1_model = 2 moved by 5.7e-06 while M1_model = 1 moved by 10.5 nats, so it turns entirely on whether that surface has a nearby worse optimum.

The same bare-comma pattern remains in the log_M1_dev, M1_beta, M1_rho and M1_dev_log_sd writes and is deliberately left alone: every one of those starts at 0, so averaging a padding cell into the level is exactly inert, and changing them would alter random-effect map structure for no numerical gain.

Rceattle 5.50.0

Bug fixes

Three adjacent M1 defects found during review are filed in inst/dev/CLEANUP_BACKLOG.md rather than fixed here, because each needs its own verification: the starting M1 is diluted toward 1.0 per year under M1_model >= 1 on a ragged-nsex set (build_map_m1() shares padding cells into a real map level and TMB:::updateMap() averages the level -- +57% on GOA2018SS pollock, and identical on 5.49.6, so pre-existing); fit_mod(updateM1 = TRUE) bypasses the bounds check entirely; and the linkage intercept-prior re-target reads log_M1 without a clamp.

Rceattle 5.49.8

Internal

This is the third instance of one pattern in as many days -- a guard measuring less than it claims -- and the second and third inside the machinery built to catch it. The first was the sixteen guards of 5.49.5; inst/dev/TRAPS.md has the class.

Rceattle 5.49.7

Internal

Rceattle 5.49.6

Documentation

Kept deliberately, because the rule's exceptions apply: comments explaining why an older input path still executes are behaviour, not history -- a fit saved before 5.32.0 can still store a retired srr_fun code, log_pop_scalar from before 5.35.0 was shaped by age, and log_catch_sd / log_index_sd are still written as deprecated aliases by rename_output(). The ceattle.cpp section 16 change log is a change log. Literature citations are the specification. Comments on the accepted-and-ignored base-graphics plotting arguments must say they are ignored. Regression-test provenance stays in its test header block, which is where two of the deleted facts already live.

Three items went to inst/dev/CLEANUP_BACKLOG.md rather than being swept, because none is safely history. A stranded Fixme: denominator is zero somewhere. Log of negative number. Check suitability. inside the ceattle.cpp change log, which change-log item 23 may have resolved but nothing says so, and which would be a silently-wrong M2 rather than a crash if it is live -- filed with the note that the marker is lowercase, so this file's own case-sensitive re-derive command does not count it. And two separate LEGACY (scheduled removal: v4.5.0) markers, still live: one on M1_mult.sum(), and one on the dynamic reference-point recruitment split, which feeds SB0(sp, nyrs-1) and so wants a deliberate decision rather than a sweep. These are two different paths, not one -- an earlier draft of the entry counted the grep hits and conflated them.

Rceattle 5.49.5

Internal

Note that adding the workflow makes the check appear on a pull request; making it required is a branch-protection setting, not something this repo holds as code.

Rceattle 5.49.4

Internal

Rceattle 5.49.3

Documentation

Bug fixes

Internal

Rceattle 5.49.2

Internal

No change to any fit. This release only deletes code that was already dead, so every objective, parameter and reported quantity is unchanged.

Rceattle 5.49.1

Bug fixes

Comments

Four statements in src/TMB/ceattle.cpp that were wrong about the code, so a reader could not use them: sel_init is filled in section 5.12.1 rather than 6.5 and is read to build Z_init and hence SPRFinit (the section number given for that was also wrong, so it now names the quantity instead); Time_varying_q 1 and 2 are IID and AR1 per tv_q_map, not "penalized deviate" and "random effect" -- whether the deviations are random effects is fit_mod(random_q = TRUE), and 2 was retired in 5.16.0 and is refused by data_check(); and the initial equilibrium catch is in weight or numbers per the fleet's Observation_units, as the loop's own flt_units == 2 branch and quantity_dictionary() both say, not kilograms.

Rceattle 5.49.0

Review findings on the 5.48.0 release (PR #184), fixed before the tag. Nothing here moves a fitted number on a configuration that was already valid: the four golden references reproduce their pinned objectives, the Pacific hake 04-mse.R chain reproduces all four of its stages to every digit before and after these changes, and reading every .xlsx in the four consumer repos through read_data() + data_check() gives the same verdict on all 375 as 5.48.0 does -- which matters here because two of the changes below are new refusals and two loosen existing ones.

Breaking changes

Two configurations that could not fit the model they named are now refused. Both are unreachable on a model that was converging, so a stored fit only stops refitting if it was already producing a non-finite objective or a prior on the wrong scale.

Configurations that were refused and should not have been

Bug fixes

Tests and documentation

Rceattle 5.48.0

A free initial recruitment level, build_srr(linkages = list(R_init = ...))

A stock whose series begins after a regime shift starts at a recruitment other than \eqn{R_0}, and Rceattle had nowhere to say so. The level had to go into init_dev, which is penalised \eqn{N(-\sigma^2/2, \sigma_R)} per age, so the model paid a recruitment-deviate penalty for sitting where the data say it sits.

That penalty biases the reference points. init_dev is \eqn{\log N_{target} - \log R_{init} + \sum M}, so the optimiser can retire the penalty by lowering \eqn{R_0} -- and does. On GOA Pacific cod it lowers it 0.37 log units, leaving \eqn{R_0} 31% below the value the same data support, while terminal SSB moves only -1.35%. \eqn{SB_0}, \eqn{B_{40%}} and depletion all scale with \eqn{R_0}, so a penalty on the initial state was moving the quantities that set the catch limit.

R_init is a fourth recruitment linkage parameter, alongside R0, alpha and beta. It is a log-scale multiplier on the initial age-structure, read at year 0 because the initial state is one year, and it carries no deviate penalty. The spec has to travel through build_srr(): fit_mod() overwrites data_list$srr_linkages from recFun, so assigning one onto the data_list drops it in silence.

d$env_data <- data.frame(Year = d$styr:d$projyr, lvl = 1)
fit <- fit_mod(d, recFun = build_srr(linkages = list(
  R_init = linkage_spec(formula = ~ 1, init = list(`(Intercept)` = 0.25))
)), initMode = "NonEquilibrium")

R_init is the only linkage parameter with no base parameter in rec_pars, so its (Intercept) is the one that stays estimable rather than being pinned at NA. Its init and bounds on that intercept are therefore the natural-scale multiplier on \eqn{R_0} — init = 0.25 is a quarter, as every other intercept's init is natural-scale — and are logged onto the coefficient in place of a base parameter. Only year 0 is read, so one design column per species is all the initial state identifies.

Measured on GOA Pacific cod 2024, where Stock Synthesis carries this as an SR_regime block on the year before the hindcast (\eqn{-1.3879}, a quarter of \eqn{R_0}, from the bridging variant that sets that block's likelihood weight to 0; the production run shrinks it and sits at \eqn{-0.678}, half of \eqn{R_0}): moving the level out of init_dev drops the objective 49.91 nats, all of it in the Initial abundance deviates row (54.975 to 5.062), with the index, catch, length-composition and CAAL components unchanged to 1.9e-04 and the recruitment deviates unchanged exactly. The stock-recruit curve is untouched -- \eqn{R_0} is the unfished level and does not move with the initial state.

Refused rather than silently inert:

A prior on the level is read on its own coefficient, i.e. on the multiplier, so lognormal(0, 0.5) gives the multiplier a prior MEAN of 1 -- no shift off \eqn{R_0} -- the same contract as the per-sex apical selectivity multiplier. Under the default bias_adjust_proc = TRUE that puts its median at \eqn{e^{-\sigma^2/2}}; pass bias_adjust_proc = FALSE to centre the median on 1 instead.

Under initMode = "FishedNonEquilibriumSelected" (6) the level scales every age of the equilibrium age-structure, age 0 included, so the predicted initial equilibrium catch is proportional to it.

A level fixed with est_phase = 0 survives a warm start: fit_mod() re-applies it over any inits, with or without an explicit init (the table's default of 0 is a well-defined multiplier of 1).

Shrink or bound the level on a real assessment. Stock Synthesis penalises its equivalent — 0.5 * (log(R1/R1_exp) / (sigma_R / ave_age))^2, at a default likelihood weight of 1 (SS_objfunc.tpl) — and no reference package surveyed (SS3, SAM, SPoRC, OPAL, WHAM) leaves an initial-level scalar free. Rceattle leaves it free by design, which reproduces the zero-weight variant rather than SS3's default; a lognormal(0, sigma_R / ave_age) prior on the (Intercept) expresses SS3's convention under bias_adjust_proc = FALSE, which is the setting that centres the prior's median on a multiplier of exactly 1, as SS3's penalty is.

This matters because the level is informed only by the first year's observations and is weakly identified at the low end: once the initial cohorts are effectively annihilated, the data cannot separate one very small level from another. tools/verify/verify-sim-recovery-r-init.R measures, on its own fixture at a true multiplier of 0.5, a few replicates in 60 running away to 5e-05 or below with a clean gradient — no convergence filter finds them — plus a low bias of about 0.12 log units over the rest. Four starting levels return the same estimate to four decimals at the same objective (inst/dev/TRAPS.md), so these are not a local optimum a multi-start or jitter() escapes: the data really do prefer the annihilated state. The counts are machine-dependent — the harness has returned both 3 and 6 of 60 on identical arguments — so re-measure rather than quoting them. A natural-scale lower bound above 0 keeps a fit out of that tail; a lower bound of exactly 0 logs to -Inf and does not.

A model with no R_init linkage is unchanged, including all four golden references.

Length compositions no longer carry ageing error

A length composition built from an age composition used age_obs_hat, the composition already smeared by the ageing-error matrix. The age-length key is P(length | true age) and a measured length carries no otolith reading, so the smear has no part in it; Stock Synthesis likewise applies its age_age matrix only to age and CAAL data, WHAM only to its age and CAAL predictions, and SPoRC passes AgeingError = NA for a length composition. Now built from the true-age composition, in both of the loops that predict one: a joint-sex composition is assembled as females over the first nlengths bins and males over the next, then normalized across both halves. Every length-comp row in the affected datasets is joint-sex, so the second loop is the one they exercise.

This moves any fit with a non-identity age_error and length comps on the same species, which is four bundled datasets and the external SS3 cod-bridge workbook (bundled GOAcod is not among them — its matrix is the identity). The two GOA golden references were re-pinned; BS2017SS/BS2017MS have an identity matrix throughout and are unchanged, as is GOApollock, whose matrix is non-identity but which fits no length comps. Refitting lowers every affected objective and moves the scale by a fraction of a percent: GOAatf -0.9416 nats and terminal SSB +1953 mt (+0.223%), NorthernRockfish2022 -2.9022 nats and +14 mt (+0.019%), GOA2018SS -1.1445 nats and +1678 mt on species 2 (+0.161%).

The initial equilibrium catch is redrawn by sim_mod()

It had no SIMULATE block, so sim_mod(), self_test() and run_mse() kept the real observation while redrawing everything else. That observation is the only one informing Finit, and therefore the initial age-structure and the SSB scale under initMode = 6 — so a self-test conditioned every replicate on data its own operating model had not generated, and reported the resulting Finit bias as if it were real. Drawn from the same lognormal as its density, reported as equil_catch_obs_sim, and written back into equil_catch_data.

Length grids are validated, and a non-uniform grid is now evaluated correctly

Two problems on the population length grid:

Two ageing-error and selectivity configurations that fitted the wrong model are refused

Both were found by reviewing 5.46.0's new features against the Stock Synthesis source.

Ageing_error_index's fallback is now validated. Omitting the column, or leaving it NA, uses the fleet's own species number — which is right while age_error is indexed by species, and wrong as soon as it carries its own Ageing_error_index column, because the fallback then points at whichever matrix sits at that position. data_check() validated only a supplied index, so the fallback could hand a fleet another species' ageing error, over another species' age range, silently: measured at 4,718 nats on GOA2018SS with two ageing eras on species 1, and at 94,000 nats where the fallback index had no rows at all and the smeared composition went flat. The effective index — the one rearrange_data() uses — is now checked to exist and to belong to the fleet's own species.

A DoubleNormalSS3 end parameter below −1000 is refused. Stock Synthesis reads P5 < -1000 as "selectivity nil through bin -1001 - P5" and P6 < -1000 as "constant beyond bin -1000 - P6, and anchor the descending limb there" (SS_selex.tpl, pattern 24) — different curves, and a sub--1000 P6 also moves peak2. Rceattle implements neither and honoured only -999, so a bridged SS3 control file using either encoding fit a different selectivity with no message.

A warm start can no longer strand a fitted value on a pinned linkage intercept

A pinned (Intercept) coefficient is 0 by construction: the process's own base parameter carries the level and the row is mapped out of estimation. fit_mod() did not re-impose that over supplied inits, and a slope-only spec produces a beta_linkage of the same length as an intercept-only one, so the length guard passed and a stale fitted slope was held on the mapped-out row for the whole fit. Refitting M1 = ~ 1 from the inits of an M1 = ~ 0 + temp fit put natural mortality at 6.69x its intended value, with no warning and nothing in convergence to show it.

The coefficient is now re-zeroed before the fit. This affects every process the linkage grammar covers, not just recruitment. A genuine refit — retrospective(), jitter(), self_test(), model_average(), run_mse() — already carries 0 there, so it is a no-op for those, and leaves all four golden references bit-identical.

Rceattle 5.47.0

link = "exponential": Stock Synthesis's environmental link type 1, on catchability

A fourth linkage link, for catchability only, completing the progression on the natural-scale parameter: "identity" adds to it, "log" multiplies it, and "exponential" raises it to a covariate-dependent power, q^exp(beta * x):

index_q_yr = exp((log_q + q_offset(yr)) * exp(sum beta * x(yr)) + q_dev(yr))
             + q_nat_offset(yr)

The name is SS3's own for it -- case 1: // exponential env link -- and is deliberately not "power": SS3 has a separate q power function (Q_setup option 3, pow(vbio, 1 + p), SS_expval.tpl:429-436), which is what Catchability = "PowerEquation" and the dormant index_q_pow are reserved for.

An SS3 parameter line requests type 1 with an env-var of 1xx. Verified against the pinned v3.30.22.1 source: parm_timevary is seeded with the base parameter (SS_timevaryparm.tpl:50), type 1 multiplies it by mfexp(beta * env) (:206-211) where type 2 adds (:215-220), the result is Svy_log_q (SS_expval.tpl:408), and the 1xx decoding for Q_parm is SS_readcontrol_330.tpl:3328. The deviation stays outside the multiply, matching SS3's order (env at :200-245, devs at :252-330).

Where it applies, and where it does not. SS3's type 1 multiplies a parameter on whatever scale SS3 stores it on, so the restriction follows that scale:

Worth knowing even on q, and it is SS3's property rather than ours: because beta multiplies a log, the effect scales with how far log q sits from zero, and below q = 1 its sign inverts. Both are pinned by tests so nobody reads them as a broken linkage.

Report beta with its base, never alone. To first order log q_y = log q * (1 + beta * x_y), so the index informs the product log q * beta. With an estimated base the fit may cross q = 1 and re-express the same curve with the opposite sign of beta: simulating from q = 0.3, beta = +0.4 and refitting returned q = 467.0, beta = -0.0775, preserving log q * beta to 1.1% (-0.4761 against -0.4816) with the fitted q series correlating 0.984 with the truth. Those figures are the simulate-and-refit check in tests/testthat/test-linkage-exponential-link.R at its own seed on one platform; the test asserts the invariant, not the values, since an optimizer path need not be identical everywhere. Interpret fit$quantities$index_q; a beta quoted alone is not comparable between models. A q prior pins the level if beta itself is wanted.

Because that is a silently-wrong-number shape rather than a documentation gap, a new convergence_diagnostics() record flags it on the fit itself: exponential_q_near_one (WARN) when a fleet's fitted catchability is within 0.1% of 1, where beta is unidentified. It reads the fitted base rather than the starting value, because an intercept init, fit_mod(inits = ) and a shared Catchability_index group each make Catchability_init something other than where index_log_q began.

The new reported quantity is q_linkage_log_mult -- the linear predictor whose exp() multiplies log q.

No fit changes for a model without a q linkage: the tensor stays at zero, exp(0) is exactly 1, and x + 0.0 and x * 1.0 are exact, so the result is bit-identical. For a model that does carry a log q linkage the sum reassociates from (log_q + dev) + offset to (log_q + offset) + dev: algebraically identical, but not bit-identical.

Motivation: the GOA Pacific cod SS3 bridge, whose LLSrv survey catchability carries this link. Rceattle-models/SS3-bridge/GOA-estimation-parity.md predicted +9.9645 nats of LLSrv index residual from its absence. Re-run on this version the bridge closes 11.254 nats with the link (Index data 53.7116 -> 42.4573), all of it in the Index data row, and no other jnll_comp row moves by more than 1e-6. The earlier 9.9646 was measured on an ancestor of the initial-level and regime-penalty commits, so the bridge's own baseline most likely moved rather than this release moving it; that was not proven, and would need a 5.45.3 A/B.

Rceattle 5.46.0

initMode = "FishedNonEquilibriumSelected" (6)

A sixth initial-age-structure mode, in which the initial fishing mortality is weighted by the fishery's selectivity at age before it accumulates, so the first year decays with \eqn{\sum_{a' < a}(M1_{a'} + F_{init} s_{a'})}. This is Stock Synthesis's InitF convention, and \eqn{F_{init}} is the apical initial F because selectivity is normalized to a maximum of 1.

The existing fished modes are neither of these: 3 charges every age the same \eqn{F_{init}}, and 4 applies it once rather than accumulating it. Under a size-selective fishery neither is an equilibrium -- 3 kills barely-selected young ages at the full initial F, and 4 does not decay the older ages with it at all.

The selectivity is the mean over the species' fishery fleets in the first hindcast year. That is exact for a single fishery, which is what SS3's per-fleet InitF reduces to here; Rceattle carries one \eqn{F_{init}} per species where SS3 carries one per fleet, so a multi-fishery stock gets the mean shape.

Every other mode leaves the weight at 1 and is bit-for-bit unchanged, including SPRFinit and therefore \eqn{R_{init}} under a stock-recruit curve.

On a species with more than one fishery, Finit is applied at the MEAN selectivity of those fisheries and so is not the apical initial F of any one of them. data_check() now warns, naming the species: the initial state sets the SSB scale.

Found while bridging the 2024 AI Pacific cod SS3 assessment; the derivation against SS3's Early_InitAge is in inst/dev/SESSION_HANDOFF.md.

Stock Synthesis growth, maturity and length-bin options

Four options that let an estimated-growth model reproduce Stock Synthesis 3.30's growth and spawning output exactly. Each defaults to the current behaviour, so existing models and the golden references are unchanged.

Spawning biomass, SB0/SBF, dynamic B0 and the SPR reference points now all read one reported array, spawn_output ([nspp, nages, nyrs], kg per fish).

With all four set, the Aleutian Islands Pacific cod bridge (SS3 3.30.22.1, model M24_1) reproduces SS3's length-at-age, Jan-1 age-length key and fecundity-at-age to within Report.sso's printed precision, against gaps of 1.5% and 5-6% before. SSB is within 1.5%, the rest being selectivity. Term-by-term numbers are in inst/dev/SESSION_HANDOFF.md.

sd_plus_group: when SS3's Growth_Age_for_L2 is 999, SS3 pins the plus group to CV_old, which is sd_plus_group = "WHAM" here, not "SS3". The documentation now says so.

Stock Synthesis selectivity

An initial equilibrium catch, so Finit has something to fit

SS3 scores the catch the stock yielded under its initial fishing mortality in the year before the hindcast, and creates an InitF parameter only when that observation exists and is non-zero. Rceattle estimated log_Finit with nothing to fit it to, so Finit sat on a ridge with init_dev -- 7.5x movement for 0.17 nats on AI Pacific cod. This supplies the missing half.

The observation is a catch_data row at styr - 1. No new columns: Fleet_code, Year, Catch and Log_sd already say everything, and write_data() / read_data() round-trip it unchanged.

clean_data() moves the row out of catch_data into a new data_list element, equil_catch_data, so catch_data keeps one row per fitted catch. Every predicted-catch quantity is indexed by that frame -- catch_hat, catch_sd, plot_catch(), residuals(source = "catch"), sim_mod() and run_mse()'s projection fill all pair with it row for row -- so a catch_data carrying an extra row puts the wrong year's catch in each of them. The element is read back by clean_data() itself, so cleaning twice is a no-op and a refit through retrospective(), jitter(), self_test() or run_mse() keeps the observation; it is combined by combine_data() and written back into the catch sheet by write_data().

That year is shared with ordinary catch history, so the rule is gated rather than marked. GOA2018SS carries 23 catch rows before styr, two of them on 1976, and a model has no way to say which a row is. A styr - 1 row is read as an equilibrium catch only under initMode = "FishedNonEquilibriumSelected"; under any other mode it is history and is dropped exactly as before.

The restriction is the prediction's, not a policy: the equilibrium catch is Baranov at Finit * s_a, and mode 6 is the only mode that builds the initial age structure with that same mortality. Mode 3 charges every age a flat Finit and mode 4 applies it once, so scoring this observation under either would fit Finit to a catch the population was never subject to. On a stock with more than one fishery there is a smaller version of the same gap, since Rceattle carries one Finit per species: N_eq decays at the mean fishery selectivity while the prediction uses the row's own fleet.

data_check() names the fleets whose rows it read whenever it reads any, so nothing is reinterpreted silently. It refuses two rows for one fleet, a non-positive catch, a non-fishery fleet, a Fleet_code that is not in fleet_control, a Species that does not match that fleet, and a non-positive Log_sd. A negative sentinel was considered and rejected: run_mse() reserves negative Year for rows it splices back in as the next assessment's data, and its window filters are on abs(Year).

rearrange_data() turns the rows it reads into equil_catch_ctl / equil_catch_obs, and drops any styr - 1 row still sitting in catch_data for a list that reached it uncleaned: such a row in the catch equation would index F_flt_age(..., -1), an out-of-bounds read rather than an error. data_check() reads the rows from either place, so it names the fleets whether or not the list has been cleaned, and gives them the checks a catch row gets -- a Fleet_code that is in fleet_control, a Species that matches it, and a positive Log_sd.

The prediction is Baranov on the deviation-free equilibrium age structure, using the fleet's own selectivity and weight in the first hindcast year:

C_eq = sum_a Finit * s_a * w_a * N_eq_a * (1 - exp(-Z_a)) / Z_a
Z_a  = M1_a + Finit * s_a

jnll_comp gains a row, Initial equilibrium catch, and equil_catch_hat joins the reported quantities; both are registered in the row-axis registry and the quantity dictionary. Every bundled model and all four golden references are unchanged.

If you read a jnll_comp row by position, read it by name instead. The new row is appended, so the last row is no longer the linkage random effects; the tests that did this now look the row up through .JNLL_ROW_AXIS.

An ageing error matrix per fleet

age_error held one matrix per species and every read was age_error(sp, ...). SS3 selects one per observation through its ageerr column, and a stock whose ageing protocol changed part way through a series needs more than one: GOA Pacific cod reads its pre-2007 survey otoliths about a year older than its post-2007 ones, a bias of roughly two standard deviations at every age, and Rceattle applied one of the two to the whole series.

The selection is per fleet, mirroring Age_transition_index, which already picks among several age_trans_matrix matrices the same way. Two eras of one survey are then two fleets sharing a Selectivity_index and a Catchability_index, so they estimate one selectivity and one catchability between them.

Additive, and silent on anything that does not use it:

Length-based selectivity is built on the population length bins

selectivity.hpp evaluated a length-based curve at the data bins while rearrange_data() aggregated the age-length key to the same bins, so a model whose composition bins are coarser than its population bins applied one bin-average selectivity across a whole data bin. SS3 forms sel(L) * P(L | age) at population resolution and bins the result.

Identical whenever the two grids coincide, which they do for any model supplying no pop_lengths -- every bundled and golden model. growth_matrix and sel_at_length move to the population grid, and the four places that combine them follow: the selectivity-at-age conversion and the selectivity-weighted weight-at-age sum every population bin, while pred_CAAL, which the length compositions are built from, sums each data bin's own run of population bins. pop_to_data_bin is monotone, so those runs are contiguous and are precomputed once; with a single grid every run is one bin and both the work and the arithmetic are what they were.

Everything that walks the curve walks the population grid with it: the normalization reference and the division, the zeroing below Bin_first_selected, and the projection copy that carries the terminal hindcast year forward. Bounded by the data-bin count instead, a finer grid kept only the lowest lengths, where an ascending curve is close to zero, so projected selectivity-at-age came back identically zero -- and with it projected F, catch and the reference points.

The columns that name a bin stay data-bin ordinals, which is what the user has and what data_check() bounds. They are not translated onto the population grid: a data ordinal cannot address a finer grid without picking a reading, and the readings differ. So on a species whose population grid is finer than its data grid, data_check() refuses a length-based fleet that is indexed by bin -- a non-parametric or AR1 form, a time-varying deviation penalty, or any of Bin_first_selected (above 1), N_sel_bins, Sel_norm_bin, Sel_norm_bin_upper, Sel_pen_first_bin, Sel_pen_last_bin or Sel_cap_bin. A parametric form that names no bin is a function of length and is unaffected, which is the case the population grid exists to serve. A species with no separate grid is unchanged.

DoubleNormalSS3 also gains the two things SS3 does that the first port did not: the ascending limb is anchored at startbin, the first population bin reaching the first composition data bin, and the bins below it take SS3's quadratic ramp.

Two things the refusal depended on, both fixed here:

Behaviour change: selected body weight for length-selective fleets

A fleet with length-based selectivity and estimated growth now weighs its catch and survey biomass by the mean weight of the fish it selects, sum_l P(l | a) s(l) w(l) / sum_l P(l | a) s(l), not by the age class's mean weight at the fleet's month. A length-selective fleet takes the larger fish of a young age class, so the age-class mean understated its catch weight; this is also Stock Synthesis's catch weight. Age-selective fleets and empirical-weight models are unchanged, including all four golden references.

What it moves, measured. The 2024 GOA Pacific cod approximation (Rceattle-models/GOA cod/2024_pcod.R, 224 parameters, length-based selectivity and estimated von Bertalanffy growth) is the one bundled-or-sibling model the change reaches. Refitting it on 5.45.3 and on this version:

| | 5.45.3 | 5.46.0 | |---|---|---| | Objective | 6415.8172 | 6416.2875 (+0.470 nats) | | Terminal SSB (mt) | 1,004,925 | 935,106 (-6.9%) | | SSB series | | mean -7.1%, largest -12.9% | | Convergence | OK | WARN, max gradient 0.0013 |

SSB sets B40% and depletion, so read that -7% before refitting an assessment on this version. The WARN is a maximum absolute gradient of 0.0013 on R0, worth at most 0.00085 standard errors, not a failure to converge.

This is not evidence the change is an improvement, and the comparison that would look like evidence is not one: that script is an approximation, sitting 63% below the accepted 2024 SAFE spawning biomass on 5.45.3 and 66% below it here, with SSB rising across a series over which SAFE declines. A 3-point move against a 63% gap says nothing. The case for the change rests on Stock Synthesis parity in GOA cod/Bridging/ and AI cod - Dev/Bridging/, where the bridges match SS3 to additive constants and SS3 weighs catch this way.

Rceattle 5.45.3

Bug fixes

Documentation

Rceattle 5.45.2

Documentation

Rceattle 5.45.1

The golden check no longer asks which local minimum the machine found

test-golden-regression.R re-fit its four reference models and compared the objective. That answered two questions at once: whether the likelihood had changed, which is what a golden check is for, and whether a cold phased optimization reaches the lower of two minima on this machine, which has a platform-dependent answer. goa_ss has a second local minimum 52.9 units up, a one-ULP change in a single log_F gradient element is enough for nlminb to reach it, and goa_ms inherits the basin through its warm start -- so the two GOA references moved together on the CI runners while reproducing exactly on local macOS. deep-checks, the only job that runs this file, was red on main for weeks over a summation order, which is the gate a release leans on.

The likelihood is now evaluated at the reference parameters, which removes the optimizer from the gate: the parameters are committed as tests/testthat/fixtures/golden-reference.rds (28 KB) and fit_mod(estimateMode = 3) builds the object and evaluates without optimizing. A change to the model moves the objective; a change to the optimizer's path cannot. The check still asserts the reference is a stationary point, so a pin cannot drift to a number that merely reproduces somewhere flat.

The fixture pins the parameters and the objective at the SAME point, which is not free: newtonsteps moves opt$par and opt$objective, while estimated_params and quantities stay at TMB's last.par.best. On goa_ss those are 2.3e-08 apart in parameter space -- 9.1e-12 in objective, and a gradient of 3.2e-06 rather than 1.3e-11. Pinning from one point (the polished one) makes the reproduction exact and leaves the gradient gate its margin.

Measured at 5.45.1: all four reproduce their pinned objective to 7e-12 absolute, which is the rounding of the 15-digit literal itself and not a model difference, at gradients between 1.3e-11 and 5.0e-11 against a 1e-8 threshold. The multispecies pair was the open question, since predation iterates inside fit_mod(), and the iteration reaches the same state from the reference parameters. The objective gate is 1e-10 relative, about 1e-06 nats. That is not a hundredfold gain in sensitivity over the 1e-6 it replaces -- the old gate's real discrimination floor on the GOA pair was the 52.9-nat basin gap, 4e-03 relative. What this buys is determinism. A 0.01 shift in the objective fails it, verified by mutating jnll in the template and reverting.

The cold-start fits move to tools/verify/verify-golden-cold-start.R. They are a harness rather than a test because their failure mode is the platform-dependent one this change exists to remove: whether the higher minimum is as well polished as the lower one is unmeasured, so a gradient assertion there could fail for a reason that is not a regression. The harness asserts the direction that cannot flake -- a cold fit must not land BELOW the reference, which is a likelihood change and not a basin lottery -- and reports the rest.

What this gives up: an edit confined to the optimization path no longer fails the gate. A phase order, a starting value or a bound that is inactive at the reference can now move a cold fit into the other basin with the gate green. Run the cold-start harness when touching those.

Regenerate the fixture with tools/verify/regenerate-golden-reference.R, and only when a model change is intended. The script refuses to write a reference whose gradient is above 1e-4.

/golden-check and the deep-checks failure message describe the new meaning. No package code changed: this is the test harness, a fixture and documentation.

The README's install pin is checked against DESCRIPTION

README.md's pinning example names a version, and it went stale four times in the 5.34.0-5.45.0 release -- at 5.41.0, 5.42.1, 5.43.0 and 5.45.0 -- each time because a version landed on dev after the release paperwork was written. Three were caught in review and one shipped to main, where it told an assessor to install a tag that does not exist. test-docs-anchors.R now fails when the pin and DESCRIPTION disagree.

Rceattle 5.45.0

A parameter past a bound is now its own verdict, at FAIL

fit_mod() passes build_bounds()'s range to nlminb, which respects it. The Newton refinement that fit_control(newtonsteps) asks for afterwards does not: in both .fit_tmb() paths it is a plain unconstrained step, so a parameter nlminb parked on a bound can be pushed straight through it, and the value that comes back is the one saved in the fit.

convergence_diagnostics() reported that as parameters_on_bounds at WARN, whose message read "at a configured bound" -- because the test par <= lo + tol is also true below lo. A value outside the range the model declared plausible now gets its own record, parameters_outside_bounds, at FAIL, naming the parameter and pointing at newtonsteps. A parameter genuinely sitting on a bound still reports parameters_on_bounds at WARN and is no longer double-counted. A tolerance band remains, scaled to each parameter's declared range, so a value just inside lo - tol still reads as "at" the bound.

The par > -900 filter is gone, which is the substantive part: it hid any parameter that had diverged DOWNWARDS -- a value at -1e6 against a lower bound of -10 drew no record at all -- and that is the direction an unconstrained Newton step most plausibly takes. The -999 sentinel it was there for (an unfished fleet-year in log_F, an init_dev above the plus group) is now matched by NAME and value instead. That skip is defensive rather than load-bearing: every -999 slot is mapped out before the bounds vector is built, so none reaches this check on a fit built from scratch, but a warm start can carry one into a slot whose catch is now non-zero. An unnamed parameter vector keeps every element, rather than emptying both records.

No fit changes. The Newton steps themselves are left alone in both paths -- clamping the one in .fit_tmb()'s fallback was considered and rejected, because that path runs only when TMBhelper is absent, so neither the test suite nor the golden references reach it, and an unverifiable change to how a fit is computed is worse than a reported one. The diagnostic covers both paths.

newtonsteps defaults to 0, so no default fit is affected. Measured on a GOA Pacific cod bridge at newtonsteps = 3: a growth parameter returned at 5.08e-05 against a lower bound of 1e-3, with a maximum gradient of 0.263 and a Hessian that would not invert. At newtonsteps = 0, the same code and the same data, that parameter sat at 1.00e-03, on the bound, with a maximum gradient of 0.00246 and a clean sdreport -- so the three steps bought 0.0017 nats of objective and cost the feasible region and the standard errors. The stale comment in R/0-convergence.R claiming optimization is unbounded in fit_mod() is corrected.

Rceattle 5.44.0

A shared catchability now says when it starts somewhere no fleet asked for

Fleets sharing a Catchability_index share ONE index_log_q, and TMB starts a shared parameter at the mean of its members' starting values. index_log_q is log(Catchability_init), so the group starts at the geometric mean of the members' inits and no fleet keeps the value in its own row. That has always been the behaviour; vignette("model-options-and-functionality") has described it correctly since 5.9.0, which corrected docs that said otherwise. Nothing said so at run time, though -- unlike the deviation standard deviations, which build_map() has warned about for some time.

build_map() now warns for the catchability itself, naming the group, the differing inits and the geometric mean it will start from. Two things the warning is careful about:

Catchability_init's schema entry now says it must be positive on a fleet sharing an estimated Catchability_index group too, not only on a fleet carrying index rows, and Catchability_prior_sd's says only the lead's is read. inst/extdata/meta_data_names.xlsx is regenerated from the schema accordingly.

data_check()'s existing reports are unchanged: the analytical case, a Fixed lead leaving fleets on different inits, and a Catchability or Time_varying_q differing within a group. The gap was the ordinary case -- forms agreeing, inits not, a q estimated for the group.

No fit changes. This is a warning and a documentation fix.

Why it is worth a warning rather than a note in a vignette: a shared q at the mean scales a survey's whole predicted index by a constant factor, and no residual pattern distinguishes that from a real change in abundance. It was found on an SS3 bridge for GOA Pacific cod, where a fleet created by a converter kept a default init and dragged the survey's q to sqrt(1.4964) = 1.2233. Measured on a forward pass -- estimateMode = 3, so the objective is evaluated AT the starting values and the start IS the fitted value -- that was 18% low across all 16 index observations and 6.23 nats, with the standard deviations right to 5e-07 and both composition components agreeing to under 0.001. In a fit that estimates q the start matters less, though it still chooses which optimum is found.

The internal helper .warn_shared_dev_sd() is now .warn_shared_block_start(), since it no longer reports only standard deviations.

Rceattle 5.43.0

Breaking changes

Convergence messages name the quantity and the coordinate

Rceattle 5.42.1

Found reviewing the release PR that carries 5.34.0 through 5.42.0 (#158), over several adversarial passes. Corrections to 5.42.0's own guard, to the tests meant to hold it, and to documentation it left behind.

Bug fixes

Documentation

Rceattle 5.42.0

Found reviewing the 5.34.0-5.41.0 release (# Rceattle 5.41.0 and the versions below it). The negative-weight defect is much older than that review: the one-sided shape penalty has had no sign branch since it landed in December 2024, and 4.10.0 added a second way in through Sel_shape_dir. 5.40.0 only extended it to the integrable forms.

Breaking changes

Bug fixes

Documentation

Rceattle 5.41.0

One-step-ahead residuals from the conditional CDF

Ease of use

Rceattle 5.40.0

New features

Rceattle 5.39.0

Stock-recruit curves under predation

Rceattle 5.38.0

New features

Rceattle 5.37.0

Breaking changes

Documentation

Rceattle 5.36.0

Results change

Bug fixes

Documentation

Rceattle 5.35.0

Results change

Breaking changes

None of the refused configurations below fitted the model it described, so this release stays a minor version.

Bug fixes

Rceattle 5.34.0

Results change

Rceattle 5.33.0

Breaking changes

None of the refused configurations below fitted the model it described, so this release stays a minor version. Stored fits with them no longer refit.

Deprecated

Results change

Bug fixes

Documentation

Rceattle 5.32.1

Documentation

Rceattle 5.32.0

Breaking changes

Rceattle 5.31.0

New features

Rceattle 5.30.0

New features

Bug fixes

Rceattle 5.29.0

Bug fixes

Deprecated

Plotting

Output format

Rceattle 5.28.1

Documentation

Bug fixes

Rceattle 5.28.0

Input format

Bug fixes

Rceattle 5.27.0

New features

Bug fixes

Rceattle 5.26.0

New features

Bug fixes

Rceattle 5.25.1

Bug fixes

Rceattle 5.25.0

Breaking changes

No exported argument is removed or renamed. Two configurations that previously ran are now refused, and neither produced a number anyone should use. A workbook with the same column under two spellings is also refused where it previously read, silently keeping one of them -- one file in the sibling repositories does (GOA cod/Data/2024_cod_pcod_test.xlsx, whose Bin_first_selected and Age_first_selected disagree on every row):

Bug fixes

New features

Rceattle 5.24.2

Bug fixes

Rceattle 5.24.1

Bug fixes

Rceattle 5.24.0

Reading a fitted model

Corrections

Rceattle 5.23.0

Reading a fitted model

Rceattle 5.22.0

Diagnostics

Bug fixes

Documentation

Rceattle 5.21.0

Diagnostics

Rceattle 5.20.0

Diagnostics

Other changes

Rceattle 5.19.1

Bug fixes

Documentation

Rceattle 5.19.0

Bug fixes

Rceattle 5.18.1

Documentation

Rceattle 5.18.0

MSE

Performance

Rceattle 5.17.0

MSE

Documentation

Rceattle 5.16.0

Breaking changes

Bug fixes

New features

Internals

Documentation

Rceattle 5.15.0

Behavior changes

Bug fixes

Rceattle 5.14.1

Documentation

Rceattle 5.14.0

Behavior changes

Minor improvements

Internal

Rceattle 5.13.0

Bug fixes

MSE

Other changes

Rceattle 5.12.0

Breaking changes

Bug fixes

New features

Minor improvements

Rceattle 5.11.1

Documentation

Internal

Rceattle 5.11.0

New features

Bug fixes

Deprecations

Rceattle 5.10.0

New features

Bug fixes -- figures whose numbers change

Three predation plotters drew quantities that did not match their axis labels. All are corrected, so figures regenerated from them will differ from earlier runs of the same model.

Bug fixes

Behavior changes

Dependencies

Rceattle 5.9.0

New features

Deprecations

Breaking changes

Bug fixes

Documentation

Rceattle 5.8.1

Documentation-only release. Four changes that landed earlier in the 5.x line reached main without a NEWS entry; they are recorded here so the release notes describe what actually shipped. No code changes.

Changes that move results, recorded late

Deprecations

Known issues

Rceattle 5.8.0

New features

Bug fixes

Documentation

Rceattle 5.7.0

Breaking changes

Bug fixes

Documentation

Rceattle 5.6.1

Bug fixes

Rceattle 5.6.0

New features

Bug fixes

Behavior changes

Documentation

Rceattle 5.5.1

Released from main as 4.9.1, before this development line was merged back. The two sections carry the same changes under different version numbers.

Bug fixes

Rceattle 5.5.0

Released from main as 4.9.0, before this development line was merged back. The two sections carry the same changes under different version numbers.

New features

Bug fixes

Rceattle 5.4.0

New features

Bug fixes

Rceattle 5.3.0

Also covers the unreleased 5.2.0-5.2.4 development increments, whose entries were folded into this section.

New features

Performance

Bug fixes

Behavior changes

Documentation

Rceattle 5.1.1

New features

Bug fixes

Rceattle 5.1.0

New features

Bug fixes

Rceattle 5.0.6

Bug fixes

Rceattle 5.0.5

Bug fixes

Internal

Documentation

Rceattle 5.0.4

Bug fixes

Rceattle 5.0.3

New features

Rceattle 5.0.2

Bug fixes

Rceattle 5.0.1

New features

Rceattle 5.0.0

Also covers the unreleased 4.14.0 development increment, whose entries were folded into this section.

Breaking changes

Bug fixes

New features

Rceattle 4.13.1

Bug fixes

Rceattle 4.13.0

New features

Bug fixes

Rceattle 4.12.0

New features

Bug fixes

Rceattle 4.11.0

New features

Bug fixes

Rceattle 4.10.0

New features

Bug fixes

Deprecations

Rceattle 4.9.0

New features

Behavior changes

Rceattle 4.8.0

New features

Bug fixes

Behavior changes

Internal

Rceattle 4.7.0

New features

Breaking changes

Bug fixes

Rceattle 4.6.0

New features

Bug fixes

API

Documentation

Rceattle 4.5.0

New features

Bug fixes

Internal / R CMD check

Rceattle 4.4.2

Code organisation (no change to fitted results)

The pre-fit pipeline files in R/ were reorganised so they are easier to navigate. None of these changes alter model output.

Rename / deprecation

Export hygiene

Internal / R CMD check

Rceattle 4.4.1

Rename

profile_param() was renamed profile() and turned into an S3 class.

Rceattle 4.4.0

New features

Bug fixes

Data checks

Documentation

Deprecations

Rceattle 4.3.1

Bug fixes

Reparameterised intercept handling for the linkage system

Linkage rows whose design_col == "(Intercept)" no longer carry the parameter level themselves. Instead:

For slope-only formulas (~ 0 + temp) the behaviour is unchanged: the base parameter is still mapped NA at its build_params() default, and the linkage row carries the year-by-year offset.

Recruitment offset semantics

Year 0 of the recruitment block no longer bakes the year-0 covariate contribution into R0. R0 is computed from rec_pars(sp, 0) alone, and the linkage offset multiplies against R0 each year (including year 0):

R(yr) = R0 * exp(rec_dev(yr) + linkage_offset(yr))

This makes the legacy srr_fun = 1 / 3 / 5 quirk (which double-counted Temp[0]) obsolete. Users migrating from the legacy paths should now get clean log-linear behaviour without surprise offsets.

Schema additions

Tests

tests-Dynamics/test-linkage-auto-map.R, tests-Dynamics/test-recruitment-linkage.R, and tests-Dynamics/test-growth-linkage-species.R were updated to assert the new contract (base parameters estimable, intercept rows fixed at 0, slope-only offsets in the year-by-year tensor).

Rceattle 4.3.0

Tidy long-format extraction: as.data.frame.Rceattle()

A new S3 method on as.data.frame() flattens derived population quantities into a long data.frame with columns year, species, sex, age, quantity, value, lwr, upr so that custom plotting and post-processing don't have to walk the nested quantities list or rely on the dimnames decisions in rename_output(). Two shapes are supported and combined into one frame:

lwr / upr are populated from the TMB sdreport for any quantity that was ADREPORT'd (currently biomass, ssb, R); other quantities and fits produced with getsd = FALSE get NA for the band. The ci_level argument (default 0.95) controls width.

Optional data fields, continued (Phases B, C, D)

Continuing the Phase A work from 4.2.0, three more classes of inputs that were previously required as non-NULL can now be omitted, with data_check() enforcing them only when the model actually needs them:

Rceattle 4.2.0

Optional data fields & data_check cleanup

Several fields in data_list that were previously required as non-NULL data.frames are now truly optional. Users who do not need composition data, conditional age-at-length, empirical selectivity, fixed numbers-at-age, ration data, or diet data can omit them entirely; clean_data() default-fills the missing fields with empty data.frames that carry the metadata columns the downstream code expects, and data_check() enforces the field only under the conditions where the model actually needs it.

Rceattle 4.1.0

Environmental linkages: a unified, formula-driven API

A new long-format linkage table lets users express how process parameters depend on environmental covariates and on stratifying factors (species, sex, age) through a single formula-driven helper, linkage_spec(). Each row of the table corresponds to exactly one estimated coefficient. fit_mod() pools every spec into a shared design matrix X and a per-row parameter vector beta_linkage; the TMB template iterates the table once and accumulates per-process offsets on the linear predictor of the underlying parameter.

Scheduled removal (v4.5.0)

Schedule update (v4.4.0): The removal originally targeted for v4.2.0 has been pushed to v4.5.0 to give downstream users a longer migration window for the natural-scale linkage API rolled out in v4.4.0. The soft-deprecation warnings continue to point users at the equivalent linkage-table call. The cleanup checklist below is unchanged.

The soft-deprecated API surfaces below remain functional and emit one-time warnings pointing users at the linkage table. They will be removed entirely in 4.5.0. To migrate, replace:

| Legacy | New | |---------------------------------------|----------------------------------------------------------| | build_srr(srr_indices = ...) | build_srr(linkages = list(log_R0 = linkage_spec(...))) | | build_srr(srr_fun = 1) | build_srr(srr_fun = 0) + linkage on log_R0 | | build_srr(srr_fun = 3) | build_srr(srr_fun = 2) + linkage on log_alpha | | build_srr(srr_fun = 5) | build_srr(srr_fun = 4) + linkage on log_alpha | | build_M1(M1_indices = ...) | build_M1(linkages = list(log_M1 = linkage_spec(...))) | | build_M1(M1_model = 4) | build_M1(M1_model = 1) + linkage on log_M1 | | build_M1(M1_model = 5) | build_M1(M1_model = 2) + linkage on log_M1 |

Cpp cleanup checklist (search for LEGACY in src/TMB/ceattle_v01_11.cpp):

R-side cleanup:

Rceattle 4.0.3

API

New methods

Documentation

Rceattle 4.0.2

Bug fixes

Documentation

Tests

Parallelism

Installation / dependencies

API

New methods

Build / packaging

Rceattle 4.0.1

The 4.0.1 development cycle reorganized several data_list columns and fit_mod / build_* arguments. Models or data files saved against earlier 4.x revisions may need updating; see the renames below. Compiled from inst/Running_list_of_updates.qmd plus the dev branch commit log.

Data renames

API renames

New features -- composition and diet likelihoods

New features -- selectivity, catchability, growth

New features -- recruitment and reference points

New features -- MSE and projection

New features -- diagnostics and tooling

Behavior changes

Rceattle 4.0.0