Skip to content

feat(asr): add Artifact Subspace Reconstruction module (standard, rASR, adaptive, Juggler)#36

Open
snesmaeili wants to merge 42 commits into
mainfrom
feature/ASR
Open

feat(asr): add Artifact Subspace Reconstruction module (standard, rASR, adaptive, Juggler)#36
snesmaeili wants to merge 42 commits into
mainfrom
feature/ASR

Conversation

@snesmaeili

Copy link
Copy Markdown
Contributor

Summary

Adds an Artifact Subspace Reconstruction (ASR) module to mne-denoise. ASR
repairs short, high-amplitude, spatially-structured EEG artifacts by calibrating
a clean-data covariance model and reconstructing burst-contaminated subspaces
(clean_rawdata lookahead + moving-covariance + raised-cosine blending).

The estimators follow the package conventions: scikit-learn
BaseEstimator/TransformerMixin with fit/transform/fit_transform
(+ partial_fit for streaming), accepting MNE Raw/Epochs or NumPy arrays,
and exposing fitted *_ attributes.

What's included

Variants

  • ASR(method="standard") — Euclidean clean_rawdata ASR (the default).
  • ASR(method="riemannian") — MATLAB rASRMatlab-faithful backend (kept behind
    experimental=True).
  • ASR(method="riemannian_windowed") — Riemannian robust calibration with a
    per-window eigendecomposition, so the cutoff knob behaves like standard ASR.
  • AdaptiveASR(variant="psp"|"psw"|"mw") — Hebbian/anti-Hebbian adaptive ASR,
    with a moving-window mode (mw_mode="final_state"|"sliding").
  • JugglerASR(strategy="dbscan"|"gev") — pointwise-amplitude reference
    selection for high-motion / MoBI EEG.

Unified API across all three estimators: get_diagnostics(),
get_calibration_mask() (+ calibration_mask_kind_), and
to_annotations(kind="repair"|"rejection"|"calibration").

Visualizationmne_denoise.viz gains 10 plot_asr_* helpers (overlay,
cutoff sweep, PSD comparison, variance topomap, repair timeline, calibration
fraction, component reconstruction, blink reduction, grand average, method
comparison), following the existing viz contract ((fig, ax), MNE or ndarray
inputs, ax=/show=/fname=).

Docs & examplesdocs/asr.rst narrative + API reference, and 5 runnable
gallery examples under examples/asr/.

Validation

Each variant is validated against its source paper and reference implementation
(Mullen 2013, Chang 2020, Blum 2019, Tsai et al., Kim 2025), with MATLAB
cross-check fixtures for the standard, rASR, adaptive, and riemannian_windowed
backends. Reproducibility tooling lives under scripts/.

Testing

  • Full suite green: 699 passed, 12 skipped (skips are MATLAB-Engine-gated
    parity tests).
  • 90% statement coverage on mne_denoise/asr + mne_denoise/viz/asr.py.
  • ruff check and ruff format --check clean across the repo.

Notes

  • MATLAB parity .mat fixtures are generated locally and gitignored; the parity
    tests skip when the fixtures (or MATLAB) are unavailable.
  • The riemannian backend is cutoff-invariant on real EEG by construction and
    stays behind experimental=True; riemannian_windowed is the recommended
    Riemannian path.

snesmaeili added 14 commits May 6, 2026 17:57
Honor max_mem_mb in calibrate_asr, process_asr, and AdaptiveASR processing
by streaming covariances in chunks when the full covariance stack would
exceed the requested budget. The full-memory path is preserved for small
datasets, and diagnostics now expose memory_mode (full / rolling / chunked /
riemannian), estimated_full_cov_bytes, peak_cov_buffer_bytes, and
chunk_samples so downstream pipelines can audit the choice.

Adds unit tests confirming that low-memory rolling/chunked paths reproduce
the full-memory output bit-for-bit at atol=1e-10, plus the ASRpy
blocksize-remainder=2 edge case.
Two reproducible validation harnesses for users with local EEG datasets:

- scripts/run_asr_real_data_validation.py: runs every public ASR variant
  on local recordings, injects known burst artifacts, and reports CPU /
  RSS / memory-mode / metadata-preservation / artifact-reduction metrics
  as JSON, CSV, and Markdown.
- scripts/run_asr_paper_validation.py: sweeps the ASR cutoff and
  reproduces the headline figures from Chang et al. 2020 (cutoff sweep,
  % windows-with-rejection, % background-variance reduced), Blum et al.
  2019 (per-variant timing, frontal blink amplitude reduction), and Kim
  et al. 2025 (reference-data fraction, 1/f-bump PSD parameter).

Documents the scripts and their outputs in docs/asr.rst.
Walks the user through ASR's calibration + burst-repair pipeline on a
32-channel, 60 s synthetic recording with eight injected bursts, then
sweeps the cutoff parameter k from 1 to 100 to reproduce the Chang 2020
Figure 2a trade-off (% windows touched vs % background-variance reduced).
Includes the canonical reconstruction equation X_clean = M (V_clean^T M)^+
V^T X, lists the Kothe pre-processing prerequisites, and points readers
to the Riemannian, Juggler, and Adaptive variant examples.
meegkit.asr imports pyriemann at module load time; skipping with a clear
message keeps the parity suite green on environments that have the
python-meegkit checkout but not pyriemann installed.
Pure formatting changes from `ruff format`. No behavioral changes.
Adds `.serena/`, `.claude/`, `reports/`, `docs/asr/papers_md/`, the
fir-specific sbatch wrapper, the local paper-conversion utility, and
WIP tutorial notebooks staged on other branches. Prevents accidental
commits of agent metadata, large validation outputs, and paper full-text
mirrors.
…ule, harmonized API

Complete the ASR estimator family and make it merge-ready:

- New variants: JugglerASR (DBSCAN/GEV reference selection), AdaptiveASR
  moving-window mode (final_state + sliding), and the riemannian_windowed
  backend (per-window eigendecomposition that restores cutoff sensitivity
  while keeping the Riemannian robust calibration covariance).
- Harmonize the public API across ASR/AdaptiveASR/JugglerASR: one
  fit/transform/fit_transform/partial_fit surface, unified get_diagnostics(),
  get_calibration_mask() (+ calibration_mask_kind_), and
  to_annotations(kind="repair"|"rejection"|"calibration"); consistent
  calibration_* parameter names. Removes the redundant update/reconstruct
  aliases.
- New mne_denoise.viz.asr module with 10 plot_asr_* helpers following the
  package viz contract (return (fig, ax), MNE or ndarray inputs,
  ax=/show=/fname=); wired into mne_denoise.viz.
- Docs (api.rst, asr.rst) and a new visualization gallery example.
- Tests: variant parity/self-consistency, robustness sweeps, viz contract,
  and targeted coverage (90% statement coverage on the asr package and
  viz/asr).
… tooling

Reproducibility scripts that validate the ASR variants against their source
papers and datasets (Chang 2020, Mullen 2013, Blum 2019, Tsai, Kim 2025) and
study robustness across substrates (real-EOG, long recordings, channel-count
extremes, per-trial ICA). Developer tooling under scripts/; not imported by
the shipped package.
…ripts

PEP 604 isinstance unions (UP038) and minor lint cleanups in non-ASR modules
surfaced by a repo-wide ruff pass. No functional changes.
…e clash

examples/asr/plot_01_basic_usage.py shared a basename with
examples/zapline/plot_01_basic_usage.py. sphinx-gallery flattens generated
output by basename, so the duplicate triggered a warning that failed the docs
build under `-W`. Renamed to plot_01_asr_basics.py (unique across the gallery).
The CI lint job installs the latest ruff, whose formatter differs slightly from
the locally pinned version. Reformat the affected files; no functional changes.
@codecov

codecov Bot commented May 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.65930% with 54 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.84%. Comparing base (c097b7d) to head (805241b).

Files with missing lines Patch % Lines
mne_denoise/asr/_reconstruction.py 94.79% 8 Missing and 11 partials ⚠️
mne_denoise/viz/asr.py 81.63% 10 Missing and 8 partials ⚠️
mne_denoise/viz/signals.py 18.75% 10 Missing and 3 partials ⚠️
mne_denoise/asr/_annotations.py 94.28% 2 Missing ⚠️
mne_denoise/asr/_distribution.py 98.64% 0 Missing and 2 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #36      +/-   ##
==========================================
+ Coverage   95.93%   96.84%   +0.90%     
==========================================
  Files          41       58      +17     
  Lines        4599     6901    +2302     
  Branches      849     1179     +330     
==========================================
+ Hits         4412     6683    +2271     
- Misses         88      103      +15     
- Partials       99      115      +16     
Flag Coverage Δ
unittests 96.84% <97.65%> (+0.90%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
mne_denoise/__init__.py 100.00% <100.00%> (ø)
mne_denoise/_logging.py 100.00% <100.00%> (ø)
mne_denoise/asr/__init__.py 100.00% <100.00%> (ø)
mne_denoise/asr/_calibration.py 100.00% <100.00%> (ø)
mne_denoise/asr/_covariance.py 100.00% <100.00%> (ø)
mne_denoise/asr/_filters.py 100.00% <100.00%> (ø)
mne_denoise/asr/_learner.py 100.00% <100.00%> (ø)
mne_denoise/asr/_spd.py 100.00% <100.00%> (ø)
mne_denoise/asr/_types.py 100.00% <100.00%> (ø)
mne_denoise/asr/_validation.py 100.00% <100.00%> (ø)
... and 17 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Add coverage for the viz/asr.py error/edge branches (empty cutoff sweep,
missing sfreq in PSD comparison, unfitted/degenerate component-reconstruction
inputs, repair-span edge cases, epochs concatenation, ax reuse) and the
JugglerASR constructor-level parameter guards.
`patch: target: auto` required new code to match the mature package's ~96%
per-line coverage, which is an unrealistic bar for a large feature's defensive
guards, plotting branches, and deep numerical fallbacks. Use a fixed 85% (with
a 2% tolerance) instead -- well above Codecov's 70% default, and comfortably
met by the new, well-tested code. The project gate (80%) is unchanged.
@snesmaeili snesmaeili requested a review from BabaSanfour May 29, 2026 13:34
Comment thread docs/asr/ASR_IMPLEMENTATION_PLAN.md Outdated
Comment thread docs/changes/devel/27.feature.rst Outdated
Comment thread docs/changes/devel/28.feature.rst Outdated
Comment thread docs/changes/devel/29.bugfix.rst Outdated
Comment thread docs/changes/devel/30.feature.rst Outdated
Comment thread scripts/_fill_aasr_readme_numbers.py Outdated
Comment thread scripts/build_long_recording_substrate.py
Comment thread tests/parity/matlab_reference/rasr_matlab_shims/grassmannfactory.m Outdated
Comment thread tests/parity/matlab_reference/ASR_REFERENCE_README.md Outdated
Comment thread tests/asr/test_adaptive.py
Resolve the viz/spectra.py conflict by taking main's refactored version, and
adapt the ASR module to main's centralized channel-picking API. main's
extract_data_from_mne now auto-picks a homogeneous channel type by default and
returns (data, sfreq, mne_type, orig_inst, picks, ch_names); the ASR estimators
do their own channel selection, so every call site passes auto_pick=False (full
data) and unpacks the new 6-tuple. Full test suite green (693 passed).
… split)

- Remove planning/scaffolding files (docs/asr/ASR_IMPLEMENTATION_PLAN.md, the
  paper4 tutorial notebook, scripts/_fill_aasr_readme_numbers.py, the MATLAB
  rASR shims + ASR_REFERENCE_README) and consolidate the per-PR changelog
  fragments into 36.feature.rst / 36.bugfix.rst.
- utils: drop the UP038 noqa in favour of a PEP 604 union.
- adaptive: expand the module docstring, align the mne import with juggler.py,
  and move the Yule-Walker statistics-filter design into _aasr_filter.py.
- core: support MEG via picks="mag"/"grad"/"meg" with a scale-invariant
  variance check (MEG works like EEG), and split the SPD/Riemannian primitives
  into _spd.py.
- viz: keep the three ASR-specific diagnostics (repair timeline, calibration
  fraction, component reconstruction); reuse generic plot_signal_overlay /
  plot_psd_comparison / plot_power_ratio_map for before/after, PSD and topomap.
- examples: plot_01/02 use plot_signal_overlay for the before/after trace;
  plot_05 showcases the generic + ASR-specific split.
- docs: api.rst + asr.rst list the three diagnostics and point to the generics.
- tests: drop dead viz tests, rewrite test_asr_viz for the kept three, and
  reformat test_aasr_mw to the zapline/dss/icanclean house style.
@snesmaeili snesmaeili requested a review from BabaSanfour June 4, 2026 19:11
Move the ASRState dataclass out of core.py into a dedicated _types.py so
every variant (standard/adaptive/Juggler/Riemannian) can depend on the
fitted-state container without importing the calibration pipeline. core.py
re-imports it; public API and behavior are unchanged.
Move parameter/array validation and dimension resolvers to _validation.py and
the statistics-only IIR filter + reflected lookahead helpers to _filters.py.
core.py re-exports them (listed in __all__) so adaptive/juggler/tests/scripts
keep importing from mne_denoise.asr.core unchanged. No behavior change.
Move fit_eeg_distribution (+ histogram / robust-stats helpers) to
_distribution.py and the block/rolling covariance builders, robust aggregation,
and memory-budgeting helpers to _covariance.py. core.py re-exports them via
__all__; no behavior change.
Move window start/weight/RMS helpers, clean_rawdata-style clean-window
selection + grid diagnostics, and the sample-mask/span utilities to
_windows.py. core.py re-exports them via __all__; no behavior change.
Move calibrate_asr and _fit_component_thresholds to _calibration.py, importing
the window/distribution/covariance/filter/SPD helpers from their new homes.
core.py re-exports via __all__; no behavior change.
Move process_asr + the Riemannian processing backends to _reconstruction.py and
compute_asr_qa_metrics / compute_asr_rejection_mask to _qa.py (the latter
type-imports ASR under TYPE_CHECKING to avoid a runtime cycle). core.py
re-exports everything via __all__; no behavior change. core.py is now ~1045
lines with only the ASR estimator class left to move.
The ASR BaseEstimator/TransformerMixin class now lives in _estimator.py. core.py
is reduced from ~2900 lines to a ~130-line re-export facade (public API + the
private helpers that tests/scripts import as mne_denoise.asr.core.*), kept for
backward compatibility. No behavior change.
adaptive.py, juggler.py and the package __init__ now import shared helpers and
the public API from their canonical modules (_calibration, _covariance,
_distribution, _estimator, _filters, _reconstruction, _types, _validation,
_windows) instead of from .core. The 'import hub' is gone: core.py is a pure
backward-compat facade no longer loaded on normal import. No behavior change.
Move the ASR unit tests into tests/asr/ mirroring the source package
(test_estimator, test_coverage, test_robustness, test_viz, test_adaptive,
test_juggler). Parity tests stay under tests/parity/. File-level relocation
only; test bodies unchanged.
…t ref

Add a 'Typical preprocessing pipeline' section showing where ASR sits between
high-pass filtering and ICA, and make 'Choosing a variant' self-contained
(removed the reference to the gitignored reports/ decision guide).
Add a mne_denoise.asr package logger and set_log_level_from_verbose() helper
(MNE-style: True->INFO, False->WARNING, level name/int direct, None->inherit),
following the mne-denoise per-module logger convention. ASR/AdaptiveASR/
JugglerASR now apply verbose at fit/transform/partial_fit and ASR emits a
calibration summary at INFO. Previously verbose was an inert placeholder.
Resolve the 26 pre-existing mypy errors with proper typing (no behavior
change): @overload on fit_eeg_distribution for the return_info flag, explicit
dict[str, Any] annotations on the combined-diagnostics dicts, float() around
np.linalg.norm in the SPD geometric medians, and is-not-None / isinstance
narrowing asserts. mypy now reports no issues for mne_denoise/asr.
Extend the ASR gallery from 5 to 13 examples so every variant and common
use-case is shown, each method example grounded in and citing its source paper:

- plot_06 cutoff tuning (Chang 2020)
- plot_07 Riemannian vs standard ASR on real blinks (Blum 2019)
- plot_08 adaptive psp/psw/mw + moving-window trajectory (Tsai)
- plot_09 Juggler dbscan vs gev reference selection (Kim 2025)
- plot_10 choosing a variant (runnable decision guide)
- plot_11 ASR on Epochs and on MEG magnetometers
- plot_12 diagnostics + QC tour
- plot_13 filter -> ASR -> ICA pipeline (capstone)

Synthetic data for concept examples; plot_07/plot_13 (and the MEG part of
plot_11) use mne.datasets.sample (DSS-gallery precedent). Reuses existing viz +
QA helpers; README regrouped. Each runs headless, ruff + format clean.
Convert the References footnote markup (.. [N]) to bullet lists (sphinx-gallery
renders example headers as plain RST, where unreferenced footnotes are warnings)
and extend two over-long section-title underlines. No content/behavior change.
Use a continuously-rotating artifact subspace (genuine non-stationarity), add
value labels so the small variant differences are legible, and reframe honestly:
on cleanly-separable synthetic data the variants clean comparably (mw marginally
best); the MW adaptation trajectory is the variant-specific highlight.
@BabaSanfour BabaSanfour mentioned this pull request Jun 8, 2026

@BabaSanfour BabaSanfour left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The work is shaping nicely; one round of modifications before I take it from there

Comment thread docs/changes/devel/36.bugfix.rst Outdated
Comment thread examples/asr/plot_01_asr_basics.py Outdated
Comment thread examples/asr/plot_02_mne_raw_qc.py Outdated
Comment thread mne_denoise/asr/core.py Outdated
Comment thread tests/parity/matlab_reference/generate_rasr_reference.m Outdated
Comment thread tests/asr/test_coverage.py Outdated
Comment thread tests/asr/test_robustness.py Outdated
BabaSanfour and others added 3 commits June 9, 2026 12:32
refactor(asr): split core.py into responsibility modules + completeness polish
docs(asr): complete, paper-grounded example gallery (5 -> 13)
- plot_signal_overlay: add optional reference / highlight_mask / highlight_spans kwargs and fold the manual overlay code out of plot_01 and plot_02 examples
- tests/asr: redistribute test_coverage.py and test_robustness.py into the existing per-module suites (test_estimator/adaptive/juggler) by the module each test exercises, and remove the purpose-named files
- remove docs/changes/devel/36.bugfix.rst (memory-bounding note folded into 36.feature.rst, since it is part of the new feature) and the dev-only tests/parity/matlab_reference/generate_rasr_reference.m
@BabaSanfour

Copy link
Copy Markdown
Member

nice ! @snesmaeili I will take it from here

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