Skip to content

Add ferx engine support and reduce phase 1 cleaning sensitivity - #4

Merged
JordanBrooks33 merged 7 commits into
mainfrom
add-ferx
Jul 1, 2026
Merged

Add ferx engine support and reduce phase 1 cleaning sensitivity#4
JordanBrooks33 merged 7 commits into
mainfrom
add-ferx

Conversation

@JordanBrooks33

Copy link
Copy Markdown
Collaborator

Summary

  • ferx engine support: Adds engine = c("nonmem", "ferx") and ferx_method parameters to remove_erroneous_obs(), enabling iterative CWRES-based observation removal using the ferx R package as an alternative to NONMEM/PsN. NONMEM remains the default. New internal helpers in R/utils_ferx.R normalize ferx fit output to the same format used by all downstream plotting, stability, and reporting functions.
  • Reduced phase 1 sensitivity: Updates gross cleaning defaults to be less aggressive — IQR multiplier 3→5, concentration increase threshold 1.5→2.0, minimum bin/subject size 4→8 — reducing false-positive flags (e.g. covariate outliers 8→2, conc bin outliers 6→2 on busulfan_sim).
  • ferx vignette: Adds busulfan_pt2_ferx.Rmd mirroring the NONMEM Part 2 workflow.

Test plan

  • All 128 existing tests pass (0 failures)
  • New unit tests for ferx_read_pars(), ferx_parse_param_labels(), ferx_write_data(), engine validation
  • End-to-end manual test: remove_erroneous_obs(engine = "ferx") runs 3 iterations on busulfan_sim (5 subjects), produces correct par/rem/phi/rmse/param_labels/stability output
  • Downstream compatibility: plot_removal_metrics() (pOFV, thetas, omegas, sigmas, nRMSE) and check_model_stability() work with ferx results
  • Phase 1 defaults: assess_data_quality() with new defaults flags fewer observations than old defaults on busulfan_sim
  • R CMD check: 0 errors, 0 notes (1 pre-existing warning from unrelated file)

🤖 Generated with Claude Code

Add ferx as an alternative estimation engine for iterative CWRES-based
observation removal, eliminating the NONMEM/PsN dependency when ferx is
available. The new `engine` and `ferx_method` parameters in
`remove_erroneous_obs()` control engine selection, with NONMEM remaining
the default. Internal helpers in `R/utils_ferx.R` normalize ferx output
to match the existing parameter/results format used by all downstream
plotting and stability functions.

Reduce phase 1 gross cleaning defaults to be less aggressive:
- IQR multiplier: 3 -> 5 (assess_data_quality, apply_exclusion_criteria)
- Concentration increase threshold: 1.5 -> 2.0
- Minimum bin/subject size for IQR computation: 4 -> 8

Also adds a ferx workflow vignette mirroring the NONMEM Part 2 vignette.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
JordanBrooks33 and others added 2 commits June 24, 2026 16:28
Incorporate atomic file writes (tmp + rename) and PsN exit code
checking from main into the engine-branched iteration loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pak needs the Remotes field to resolve ferx from GitHub since it is
not yet on CRAN.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 257 lines in your changes missing coverage. Please review.
✅ Project coverage is 0.00%. Comparing base (8179a48) to head (e889fa8).

Files with missing lines Patch % Lines
R/remove_erroneous_obs.R 0.00% 150 Missing ⚠️
R/utils_ferx.R 0.00% 105 Missing ⚠️
R/gross_cleaning.R 0.00% 2 Missing ⚠️
Additional details and impacted files
@@          Coverage Diff           @@
##            main      #4    +/-   ##
======================================
  Coverage   0.00%   0.00%            
======================================
  Files         10      11     +1     
  Lines       2416    2584   +168     
======================================
- Misses      2416    2584   +168     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

JordanBrooks33 and others added 2 commits June 24, 2026 18:41
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Exercises the full ferx code path in remove_erroneous_obs() and
utility functions: ferx_run_iteration, ferx_read_pars, param labels,
downstream plotting, and stability checks. Tests skip when ferx is
not installed (skip_if_not_installed).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread R/remove_erroneous_obs.R Outdated
save_temp_dir = FALSE,
columns = list(ID = "ID", TIME = "TIME", DV = "DV"),
engine = c("nonmem", "ferx"),
ferx_method = "focei",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ferx_method is unnecessary argument IMO, can also be set in model file. If we do want to be able to set method and other options in function call, then use ... and pass through to ferx_fit( , ...)

Comment thread R/remove_erroneous_obs.R
@@ -1,12 +1,13 @@
#' Iteratively detect potentially erroneous pharmacokinetic observations
#'
#' Performs iterative NONMEM-based detection of potentially erroneous

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this function is becoming very long. Would ask CC to split up in subfunctions, so it's more modular.

@roninsightrx

Copy link
Copy Markdown
Contributor

Code review — ferx engine (high effort, workflow-backed)

Headline: findings 1, 2, 5, 6 share one root cause — the ferx float-equality matcher in remove_erroneous_obs.R. Replacing it with .row_idx-based removal (like the NONMEM path) resolves all four at once. Prioritize that fix.

🔴 Critical

1. R/remove_erroneous_obs.R:454 — float == match can abort the run
ferx maps the flagged sdtab row back to the dataset via data[[time_col]] == result_rem$TIME & data[[dv_col]] == result_rem$DV. But TIME/DV come from sdtab after a CSV round-trip (write.csv → ferx read). Any rounding/reformatting → match length 0 → stop("Could not match removed observation...") after potentially long model fits. The old positional .row_idx removal could never fail this way.

2. R/remove_erroneous_obs.R:459 — multi-match silently removes wrong row
The guard only checks for zero matches. Duplicate ID+TIME+DV (or same nominal time + equal conc) → match_idx length > 1 → removes match_idx[1L], dropping the wrong observation while the genuinely flagged high-CWRES record is retained. Silently incorrect cleaned dataset. Add a length > 1 guard / disambiguation.

3. R/remove_erroneous_obs.R:171 — positional arg shift breaks existing callers
New engine / ferx_method params inserted between columns and verbose. A prior positional call like remove_erroneous_obs(dat, mod, run_id, n, cols, FALSE) now binds FALSEengine, so match.arg(engine) aborts. Append new params at the end instead.

🟠 High

4. R/utils_ferx.R:191 (also :80) — THETA label desync
Index advances only on lines matching a hard-coded prefix list (theta/tv/pop, etc.). Any non-matching parameter line desyncs the index → labels attached to the wrong THETA/OMEGA in removal plots.

5. R/remove_erroneous_obs.R:427 — non-numeric subject IDs break the match (plausible)
as.numeric(result_rem$ID) → NA for character IDs (e.g. SUBJ01), so the match returns length 0 and the run stops. Hits even with default columns.

6. R/remove_erroneous_obs.R:427 — custom columns coupled to literal sdtab names (plausible)
Mixes data[[columns$ID]] with hard-coded across(c("TIME","DV","ID")). Non-default columnsColumn "TIME" doesn't exist or a match abort. The ferx engine effectively only works with default ID/TIME/DV naming.

🟡 Medium

7. R/utils_ferx.R:218quote = FALSE can corrupt the CSV (plausible)
A character column containing a comma is emitted unquoted, shifting all subsequent fields; ferx then reads misaligned columns. Use quote = TRUE.

8. R/remove_erroneous_obs.R:415 — ferx branch skips the MDV == 0 filter (plausible)
Builds obs_result from the full sdtab, assuming observation-only rows. Dose/non-observation rows → wrong max|CWRES| selection or skewed nRMSE. The NONMEM branch filters MDV; ferx does not.

🔵 Intentional (per PR title — flagged for visibility)

9. R/exclusion_criteria.R:89conc_increase_threshold 1.5 → 2.0, dose_iqr_multiplier 3 → 5. Concentration jumps of 1.6–1.9× without an intervening dose now pass unflagged.

10. R/gross_cleaning.R:421 — minimum bin/group size 4 → 8, iqr_multiplier 3 → 5. Groups/bins with 4–7 observations are skipped entirely; small/sparse cohorts (pediatric, rare-disease) lose outlier detection.


Verified findings from a multi-agent review (one finder per angle + independent verifier per candidate). 🤖 Generated with Claude Code

…ularity

- Replace float-equality matching with positional .row_idx removal (fixes
  rounding failures, multi-match bugs, non-numeric ID and custom column issues)
- Move engine param to end of signature to avoid positional arg shift
- Remove ferx_method param; use ... passthrough to ferx_fit() instead
- Fix param label desync: treat unrecognized prefixes as thetas
- Use quote=TRUE in ferx_write_data to prevent CSV corruption
- Add MDV==0 filter for ferx sdtab when column exists
- Extract .reo_run_nonmem_iter() and .reo_prepare_obs() helpers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@roninsightrx roninsightrx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

lgtm

The remove-obs chunk used eval=has_ferx, causing the full 20-iteration
ferx fitting to run in CI (2+ hours). Changed to eval=FALSE and made
the fallback chunk (pre-computed results) always run.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@JordanBrooks33
JordanBrooks33 merged commit 515e6a5 into main Jul 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants