Skip to content

fix(convert_output): repair the Rceattle branch and correct its units - #401

Open
grantdadams wants to merge 3 commits into
nmfs-ost:mainfrom
grantdadams:fix/rceattle-convert-output
Open

grantdadams wants to merge 3 commits into
nmfs-ost:mainfrom
grantdadams:fix/rceattle-convert-output

Conversation

@grantdadams

@grantdadams grantdadams commented Oct 8, 2026 •

Copy link
Copy Markdown

What is the feature?

How have you implemented the solution?

  • Wrong element names (index_log_q, catch_hat, deprecated Q_block), positional module selection, file[[ instead of dat[[, comp_data overwritten with catch data, and a cross_join() on the predictions. Per-defect detail with the measured before/after is in the commit messages.
  • Verified on GOAatf, bundled with Rceattle: 35 columns, 21,768 rows (was 150,372), including 11,340 selectivity and 3,780 natural mortality rows
  • Tests pin the composition and index alignment by value against the fit's own arrays. devtools::test() has 0 failures, with the same 67 warnings as main.
  • New 40 KB fixture from the public GOAatf, with prepare_rceattle_output_file.R alongside it so the recipe survives the merge.

Does the PR impact any other area of the project, maybe another repo?

  • No other model's path is touched; every code change is inside the model == "rceattle" branch.
  • data/example_data.rda regenerates bit-identically, so the update-example-data trigger has nothing to rebuild.
  • [fix]: Rceattle converter publishes wrong composition, age and uncertainty values #405 is the companion — six pre-existing defects this repair makes reachable, three of which publish wrong numbers rather than missing ones. Worth reading before merging; I left them alone because each is a decision about what the standard means.
  • Only 4 of the 35 corrected unit cells reach the output today (the sheet has no data_list block); the rest are dormant until quantities is processed. Also in [fix]: Rceattle converter publishes wrong composition, age and uncertainty values #405.
  • The checks here are at action_required — first PR from a fork, so they need approval to run.

@Schiano-NOAA Schiano-NOAA linked an issue Oct 8, 2026 that may be closed by this pull request
@grantdadams
grantdadams force-pushed the fix/rceattle-convert-output branch from f1c6f84 to 6516e75 Compare October 8, 2026 19:51
@grantdadams

grantdadams commented Oct 8, 2026 •

Copy link
Copy Markdown
Author

I reviewed this before anyone had to and force-pushed a corrected version. What changed, in case you read the first one:

  • Three things I'd called defects were not defects. rbind() works once the renames in this PR pad every frame, so I reverted to it — it also catches a renamed Fleet_code, which bind_rows() turned into a silent 36th column. Setting era on the catch frame is cosmetic, since the mutate at the end of the branch already recomputes it. And you already refuse a multispecies fit on the index data, so mine is a second guard, not the first.
  • One defect I'd introduced. Selecting modules by name meant a getsd = FALSE fit returned a table with no biomass, SSB or recruitment and still printed "Conversion finished!". It now warns.
  • Tests. Mine asserted row counts but no values, leaving the composition fix's central premise unpinned; it's now checked against the fit's own array.
  • Also trimmed this description by about 95% and moved the findings to [fix]: Rceattle converter publishes wrong composition, age and uncertainty values #405.

@Schiano-NOAA
Schiano-NOAA self-requested a review October 8, 2026 19:58
@Schiano-NOAA Schiano-NOAA added the bug Something isn't working label Oct 8, 2026
@Schiano-NOAA

Copy link
Copy Markdown
Collaborator

@grantdadams thank you so much Grant! I appreciate you tackling a first review. I plan to do a review tomorrow, but my main checks will be to verify that this works in convert_output and that all of our figures and tables look reasonable as well.

This is a huge help with the changes to Rceattle! I will lyk if I have any questions, but I think I can take it from here.

Fixes nmfs-ost#400.

`convert_output(model = "rceattle")` errored on every Rceattle fit, in both
input modes:

- `index_ln_q` is `index_log_q` and `catch_h` is `catch_hat`, so neither was
  found; `Q_block` is deprecated, now selected with `any_of()`.
- Module selection was positional, but a fit's element list depends on how it
  was fitted, so a fixed offset lands on different modules for different fits.
  Selected by name instead. `sdrep` holds every derived time series, so a
  `getsd = FALSE` fit now warns rather than returning only the input data.
- `file[["data_list"]]` read the argument, not the loaded object, so the path
  mode could never work; `dat[["data_list"]]` in 16 places, and the `.rds`
  check is anchored and case-insensitive.
- `comp_data` was overwritten with the catch data, and predicted composition
  used `cross_join(age_hat)`, repeating the 2,478 correct predictions 59
  times for 146,202 against 2,928 observed.
- The catch and composition frames set no `module_name`, leaving 5,586 of
  6,648 rows without one.
- A multispecies fit has no species dimension in this schema; refused on
  `nspp`, read with `[[` so a renamed element cannot partial-match past it.

Output is 35 columns, 6,648 rows, and biomass equal to the fit to 2.33e-10.
Adds a fixture built from the bundled `GOAatf` with its rebuild script, and
tests pinning the composition and index alignment by value.
Groundwork for nmfs-ost#388-nmfs-ost#392, which will read units from the converter: once a
figure takes its unit from this sheet, a wrong one becomes a wrong axis label.

All 35 unit-carrying rows in rceattle_var_names.csv were wrong -- 31 said kg
and 4 said numbers. CEATTLE carries numbers-at-age in thousands of fish and
weight-at-age in kg, so their product is metric tons. Corrected against
`Rceattle::quantity_dictionary()`: biomass and the B0/SB0 family to mt,
numbers-at-age to thousands of fish, the depletions and compositions to
proportion, log_ssb to log mt, pop_scalar to multiplier, nbyagespr to fish
per recruit, consumption_at_age to kg per fish per yr.

catch_hat, max_catch_hat, comp_weights and diet_comp_weights are blanked
rather than corrected: a fleet's catch follows its own Observation_units
column, so no single value is right for every fleet, and a composition weight
is a likelihood multiplier (log Dirichlet-multinomial theta under that
likelihood) rather than a mass.

Flimit is the FOFL proxy (F35% at Tier 3) and Ftarget the maximum FABC proxy
(F40%), but the two alt_labels were crossed, so each reference point would
publish under the other's name. The ln_flimit / ln_ftarget rows in the same
sheet are already the right way round.

Only the four sdrep rows reach the output today, since the converter emits
sdrep and data_list and the sheet has no data_list block; the rest are
corrections for when quantities is processed. See nmfs-ost#405.
@grantdadams
grantdadams force-pushed the fix/rceattle-convert-output branch from a8038fc to df8915a Compare October 8, 2026 21:17
@grantdadams grantdadams changed the title fix: make the Rceattle branch of convert_output() work, and correct its units fix(convert_output): repair the Rceattle branch and correct its units Oct 8, 2026
Comment thread R/convert_output.R Outdated
i = "Refit with {.code fit_control(getsd = TRUE)} to standardize the derived time series."
))
}
for (module_name in intersect(c("sdrep", "data_list"), names(dat))) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a condition where sdrep or data_list would not be in names(dat)?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sdrep, yes. fit_mod() leaves it out when getsd = FALSE, when estimateMode = 3 (no estimation), for a projection-only fit with a constant-F control rule, and when the Hessian is not positive definite. The output then has no biomass, SSB, recruitment or parameter estimates, so the converter warns. The input data, selectivity and M still come through.

data_list is always on a fit returned by fit_mod(). The check is for a fit that was trimmed or built by hand. Without it the converter can't attach years or read the data, so it stops with a clear message.

@Schiano-NOAA Schiano-NOAA left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@grantdadams This generally looks good to me. A couple questions before I merge:

(1) is there selectivity somewhere in the output file?
(2) is there error for recruitment deviations?
(3) is natural mortality reported anywhere? is it a single point or a time series?

Please lmk where they are located if these values are in the output file. Thanks!

…r errors

* Read `sel_at_age` and `M_at_age` from `quantities` as `selectivity_age` and
  `natural_mortality`, by fleet, sex, age and year. Ages are offset by
  `minage`, since the array's age dimension is the bin index.
* Give the fixed effects a standard error from `sdrep$cov.fixed`, so
  recruitment deviations now carry one.
* Read `sdrep$par.random`, so recruitment deviations are kept when fitted as
  random effects (`random_rec = TRUE`).
* The fixture now keeps `minage`, the 2 arrays and the fixed-effect variances.
@grantdadams

Copy link
Copy Markdown
Author

@Schiano-NOAA thanks! All 3 are in the Rceattle fit, and my commit adds them to the converted output.

  1. Selectivity. It's read from fit$quantities$sel_at_age (fleet x sex x age x year) as selectivity_age, so plot_selectivity() works. The fishery curve for GOAatf peaks around 1.7. That is the model's actual output, because Rceattle normalizes non-parametric selectivity to a mean of 1, not a maximum of 1.
  2. Recruitment deviation error. The deviations now have a standard error, taken from the diagonal of sdrep$cov.fixed, and plot_recruitment_deviations() draws the bars. They're also kept when recruitment is fitted as a random effect (random_rec = TRUE), in which case they come from sdrep$par.random.
  3. Natural mortality. It's read from fit$quantities$M_at_age as natural_mortality, by sex, age and year. For GOAatf it's fixed at 0.2 for females and 0.35 for males, the same at every age and year. Rceattle can estimate M and let it vary by year, so in general it's a time series. plot_natural_mortality() works with it.
  4. Selectivity at length (quantities$sel_at_length) is not converted yet. It is only filled for fleets with Selectivity_dimension = "Length" (all zeros on GOAatf), and the fit stores length bin ordinals, not lengths, unless pop_lengths was supplied. Converting it needs a decision on what goes in length_bins, and a separate module_name so plot_selectivity() does not silently pick age or length when both are present.

Ages on these rows are the actual ages: the array's age dimension is offset by minage. The GOAatf conversion now has 21,768 rows.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: RCeattle convert_output

2 participants