Skip to content

[Epic] Fable 5 Findings — whole-codebase code-quality remediation#206

Merged
arnodelorme merged 32 commits into
developfrom
fable5/epic
Jun 12, 2026
Merged

[Epic] Fable 5 Findings — whole-codebase code-quality remediation#206
arnodelorme merged 32 commits into
developfrom
fable5/epic

Conversation

@suraj-ranganath

@suraj-ranganath suraj-ranganath commented Jun 12, 2026

Copy link
Copy Markdown
Member

Complete the Fable 5 code-quality remediation epic by merging phases #200, #202, #203, #201, and #204 into fable5/epic. Adds integration fixes for FDR non-finite parity, clean_artifacts copy-on-entry, Picard seed mapping, pop_newset help, and final parity matrix validation. This is the advisor-gated umbrella PR to develop and supersedes #199.

Part of #193

suraj-ranganath and others added 23 commits June 8, 2026 20:46
Part of the Fable 5 Findings epic (#193); addresses issue #198.

Standalone/runtime hygiene:
- Move developer/parity tooling out of the installed wheel into tools/:
  visual_capture.py -> tools/visual_parity/, stage_comparison.py ->
  tools/eeg_bids/, iclabel_net_load_py_measures.py -> tools/iclabel/
  (refactored to import the canonical ICLabelNet, removing the
  float64/float32 model drift). pop_reref_helper.py -> scripts/.
- Make eeglabcompat import side-effect-free (lazy temp dir), so importing
  core modules no longer creates a temp directory at import time.
- Resolve the packaged montages dir via importlib.resources instead of a
  __file__ dirname chain in pop_load_frombids.

Dependencies & docs:
- Drop the redundant eeglabio optional extra that duplicated the mandatory
  dependency. eeglabio stays required (used transitively via mne.export for
  .set writing); installation docs updated to match.
- Remove conda-forge install instructions for this unpublished pre-release;
  make the EEGobj example in contributing.rst actually runnable.
- Fix the README docs badge and correct CI branch triggers (main -> develop).

Dead code:
- Remove _currentset_list (menu_actions), _reject_unsupported_options
  (pop_newcrossf), the unused withindex parameter (pop_chansel), the
  custom_randperm no-op wrapper (runica; hoist `import time`), an unraisable
  except in eegrej, and the unread top-level STUDY['preclust'] seed
  (_study_utils).
Part of the Fable 5 Findings epic (#193); addresses issue #194.

- Remove assertion-swallowing try/except->skipTest wrappers across
  test_clean_artifacts, test_clean_drifts, test_eeg_picard,
  test_ICL_feature_extractor, and test_eeg_mne2eeg so assertions run
  instead of converting failures into skips. Genuine optional-dependency
  skips are kept as explicit @skipUnless guards.
- Deduplicate the per-module create_test_eeg fixtures onto the shared
  tests/fixtures.py fixture.
- Tighten the eeg_autocorr_fftw vs eeg_autocorr cross-check from
  rtol/atol=0.1 to 1e-4/1e-7 (documented: the only divergence is the
  single-precision FFT cast in eeg_autocorr).
- Add a real (unmocked) calc_projector test that runs the actual
  sphericalSplineInterpolate and validates channel mapping/assembly.
- Replace the ASR numpy-error-injection log-string tests with real
  degenerate-input tests asserting finite, sane cleaned output.
- Assert real clustering behavior (shared labels within tight pairs,
  far point isolated) in the study clustering helper test.

test_eeg_picard_different_random_states now xfails honestly: eeg_picard
converges identically regardless of random_state (tracked under the epic
for Phase 2/3); marked xfail rather than re-hidden.
…iants

Part of the Fable 5 Findings epic (#193); addresses issue #195.

Make side-effect-free functions actually side-effect-free by copying at
entry, and fix the ICA sign-normalization invariant break.

- Deep-copy EEG at entry in eeg_runica, eeg_amica, load_amica_model,
  eeg_picard, pop_select, pop_headplot, pop_clean_rawdata, clean_windows and
  clean_asr; copy the data array at entry in runica and clean_asr; build
  pop_saveset's MATLAB structures from copies so the caller keeps 0-based
  urchan/urevent; deep-copy the clean_artifacts high-pass snapshot.
- eeg_rpsd no longer reseeds the global NumPy RNG (uses a local RandomState).
- coords_to_mm / coords_RAS_to_ALS no longer mutate the caller's array.
- eeg_from_data no longer silently transposes a tall 2-D array on a
  shape>shape guess; it raises and asks for nbchan when orientation is
  ambiguous.
- posact sign-flips now negate the matching icaweights row and icawinv
  column directly instead of recomputing icaweights via pinv(icawinv), which
  had folded the non-identity sphere into the weights; icasphere is left
  untouched, preserving icawinv == pinv(icaweights @ icasphere) and
  icaact == icaweights @ icasphere @ data. eeg_amica and eeg_picard now
  flatten epoched data in Fortran order to match eeg_runica and EEGLAB.
- Adds regression tests asserting inputs are unchanged and the ICA
  invariants hold.
Part of the Fable 5 Findings epic (#193); addresses issue #196.

Replace silent fallbacks with real dispatch, make ignored options take
effect or fail loudly, and fix index/boundary errors — each with a test
that reproduces the bug first.

- Loading: pop_loadset dispatches HDF5 vs scipy by the actual file instead
  of swallowing every error into an HDF5 retry; pop_fileio dispatches .mat
  by content instead of silently importing a broken .set as a raw array;
  pop_loadset_h5 drops the hard-coded emoji fixture and decodes by one rule.
- Indexing/events: pop_select chantype selection no longer crashes
  (eeg_decodechan return unpacked) and the console zero-bases pop_select
  numeric channels so 1-based history replays correctly; eeg_eegrej no
  longer drops first/last events by latency; pop_editeventvals epoched
  latency display/edit is symmetric (ms<->points) and no longer raises.
- sigproc: epoch() boundary check fixed (no negative-index slice); eegrej()
  shifts boundary latencies by base region spans; runamica() cleans its
  temp dir on failure without swallowing the error.
- Session: console CURRENTSET reassignment guarded by a regression test;
  IMPLEMENTED_ACTIONS now has a test enforcing it matches dispatch routing;
  GUI dataset commit applies history only after the abort check; the
  headplot menu action commits its spline edit through the session.
- CLI: pipeline gains the negative-edge filter guard the subcommand has;
  --quiet/--verbose/--no-progress are wired; the self-describing schema
  covers all required params and stops marking --output required under
  --overwrite; bids dispatches by format and propagates exit codes.
- ASR/clean: clean_flatlines walrus precedence fixed; asr_calibrate raises
  on unsupported sample rates; clean_artifacts/clean_asr/asr_process no
  longer swallow failures and re-insert ignored channels per EEGLAB.
- Other: newtimef _is_on uses correct whitelist semantics and its dead
  params fail loudly; compute_pac passes through title/vert/newfig;
  std_precomp recompute and std_makedesign delfiles take effect; EEGobj
  raises on unknown attributes; gen_derived_fpath honors ${root}; ICLabel
  validates icawinv before dereferencing it.
The mutation regression test snapshotted icaweights via list(), which breaks
when the shared tests/fixtures.py fixture (Phase 1) provides a pre-ICA dataset
with icaweights=None. Assert the caller's ICA fields are the exact same objects
after the call instead, which is what 'does not mutate' means and works for both
a None and an array-valued icaweights.
clean_asr now returns a fresh dataset; reword the three comments that still
described it mutating EEG in place (AGENTS.md: delete stale comments in
touched code).
@claude

claude Bot commented Jun 12, 2026

Copy link
Copy Markdown

Claude finished @suraj-ranganath's task in 7m 28s —— View job


Code review — PR #206 (Fable 5 Findings epic)

  • Overall assessment: Safe from a code-quality standpoint. One doc gap worth closing.
  • Highest-risk area: the few behavioral changes hiding inside an otherwise-mechanical cleanup — MNE→EEG ICA reconstruction, the GUI long-task threading, and session.apply_workspace_state. All three were checked closely and hold up.
  • Merge recommendation: Needs human review (you've correctly flagged this targets develop and supersedes [Console] Install console runtime in dev sync #199). No blocking code issues found.

I reviewed the full diff (git diff origin/develop...HEAD, 67 files) with four parallel agents covering correctness/regressions, EEGLAB parity & data structures, conventions/test-coverage, and AGENTS.md compliance, then validated findings against the source.

The vast majority of the −2337 lines are genuinely dead: if __name__ == "__main__" smoke blocks, in-module test_* helpers (pytest only collects tests/), hardcoded /Users/arno/… debug paths, and the retired autoreview skill. No load-bearing code or live callers were removed. printlogger conversions are complete in the changed runtime modules and guarded by the new test_processing_logging_contract.py.

Blocking

None.

Important

None — but one doc gap is close to the line:

pop_newset "Edit description" button is now functional, with no user-facing doc/help update. src/eegprep/functions/popfunc/pop_newset.py changes the button from a "use the command line" message into a real multiline editor that writes EEG['comments'] and threads it into history. AGENTS.md: "Any user-facing EEGPrep change must include matching Sphinx updates under docs/source/ … unless the change has no user-visible behavior" and "When adding a user-facing function or GUI dialog with a Help button, add or update the corresponding Markdown help resource." src/eegprep/resources/help/pop_newset.md still describes only setname/overwrite/retrieve, and gui_console_session.rst's new paragraph only covers the pop_runica long-task. Add one sentence to pop_newset.md noting the Edit-description editor edits the dataset comments field from the GUI. Fix this →

Nits

  • eeg_mne2eeg_epochs.py is a silent correctness fix, not just a "rewrite." The old code assigned icasphere=ica_weights, icaweights=ica_sphere (swapped) and derived icaact from MNE's own sources. The new code reconstructs the true EEGLAB unmixing (unmixing_matrix_ @ pca_components_ @ pre_whitener), sets icaweights/icasphere/icawinv consistently, and recomputes icaact = unmixing @ data so EEGLAB's icaact = (icaweights*icasphere)*data invariant holds. Correct and now covered by test_eeg_mne2eeg_epochs_ica_fields. Worth calling out explicitly in the PR/changelog since it changes the EEG dict of every dataset this function produces. (Note: icaact intentionally omits MNE's pca_mean_ centering — correct for EEGLAB parity, so it won't byte-match get_sources().)
  • console.py:245 ConsoleEEGPrepModule.__getattr__ does a bare namespace["eegh"] subscript. Safe today (_bind_base_namespace always binds eegh before exports, and it's tested), but .get(...) with a fallback would avoid a KeyError-instead-of-AttributeError if the namespace were ever cleared. Low priority.

Test gaps

None that block. Coverage for the new behaviors is solid: test_gui_long_task.py, test_gui_pop_runica.py (success / error-no-mutation / main-thread callback), test_pop_newset.py (edited vs untouched description), test_console_workspace.py + test_gui_main_window.py (apply_workspace_state validation & ALLCOM sync), and the visual-capture manifest-coverage test that guards the if/elif→dict refactor going forward.

EEGLAB parity notes

  • MNE conversions (eeg_mne2eeg_epochs, eeg_eeg2mne): verified channel-major (nbchan, pnts, trials) layout, F-order reshape roundtrip, and preload-before-tempdir-delete safety. Sound.
  • session.apply_workspace_state: preserves 1-based CURRENTSETALLEEG indexing (alleeg[index-1]), validates bounds before mutating, mirrors edits back to ALLEEG, and avoids duplicate history entries. Consistent with EEGLAB session semantics.
  • pop_runica GUI split: the interrupt='on' default moved into pop_runica_gui_options but applies to the same options dict, so the emitted history command is unchanged.
  • One thing to confirm with your advisor (intended, not a defect): the eeglabmigrate CLI rename also changes JSON schema_version strings (eegprep.eeglab.history.v1eegprep.migrate.history.v1), which breaks any automation keying on the old values. Consistent with AGENTS.md "no backward compatibility by default," just flagging the contract change.

Checked correctness/regressions, EEGLAB parity, data-structure compatibility, changed-behavior tests, and AGENTS.md/CLAUDE.md compliance.
• branch fable5/epic

# Conflicts:
#	tests/test_utils_asr.py
…ing)

Part of the Fable 5 Findings epic (#193); addresses issue #197. Built on the
integration of Phases 1/2/3/5 so the deduped code keeps those fixes.

Behavior-preserving consolidations (existing tests pass unchanged):
- studyfunc: extract the k-means kernel into _cluster_kmeans.py so pop_clust,
  optimal_kmeans and robust_kmeans import it downward (layering inversion
  fixed); share unique_preserving_order, trialinfo_rows, default_measure_target
  and the MEASURE_DATA_FIELDS map.
- timefreq: newtimef/newcrossf use the canonical statistics.fdr and the shared
  bootstrap/threshold helpers instead of forked copies; _is_on imports the
  canonical whitelist predicate.
- popfunc: remove the duplicated _is_on copies (import _pop_utils.is_on),
  pop_topoplot reuses the shared python_literal, pop_topochansel delegates to
  pop_chansel, pop_comperp collapses the repeated lowpass calls, pop_saveset
  serializes chanlocs through the single _chanlocs_to_struct_array, and the
  near-identical pop_rejkurt/pop_jointprob share their rejection plumbing.
- gui/cli/extensions: menu_actions collapses the per-name dispatch branches;
  the pop-result interpretation contract is shared; the dead run_main() is
  removed; extension_catalog splits its manager and curation halves
  (extension_catalog_validation.py) and reuses the canonical entry-point
  helpers; plugin_menu derives bundled metadata from the registry.
- plugins: firws/firwsord move to the firfilt plugin (correct ownership) with
  clean_rawdata importing them.

Findings that would change observable behavior (figure titles, history
strings, cluster partition/provenance, error strings) or need files outside
this scope are intentionally left for a follow-up, as are the four large-file
decompositions (pop_load_frombids, statistics/_core, pop_prop_extended,
QtDialogRenderer) — those are tracked as Phase 4b.
@suraj-ranganath

Copy link
Copy Markdown
Member Author

🤖 Integration status update: all five phase PRs are merged into fable5/epic in the planned order (#200#202#203#201#204). I resolved the expected ASR test conflict by keeping all coverage and the fail-loud component-selection assertion, rebased #204 onto the composed epic branch, and fixed additional verified issues found during review: FDR now ignores non-finite p-values for the BH denominator while masking those positions false, clean_artifacts no longer mutates callers missing EEG['etc'], Picard maps seed to random_state, pop_newset help documents Edit description, and the final parity matrix validates against the committed snapshot when the vendored EEGLAB checkout is sparse. Local verification passed: ruff check, ruff format --check, ty check, ./pre-commit.py --changed-from origin/develop, focused integration suites, and EEGPREP_SKIP_MATLAB=1 uv run --no-sync pytest -m "not slow" (2023 passed, 249 skipped, 16 deselected, 1 expected xfail).

@suraj-ranganath

Copy link
Copy Markdown
Member Author

🤖 Final integration status: refreshed CI is green after normalizing BIDS derived output paths for Windows. All checks now pass on PR #206: documentation build, pre-commit, Ruff/ty, Ubuntu Python 3.10/3.11/3.12, macOS Python 3.12, and Windows Python 3.12. Merge state is CLEAN.

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.

2 participants