Skip to content

Adaptive line-noise removal (“Zapline+”) for MNE (core mne.preprocessing.zapline) #13422

@snesmaeili

Description

@snesmaeili

Describe the new feature or enhancement

What

Add an adaptive, component-based line-noise remover to the MNE ecosystem, based on the Zapline-plus method (Klug & Kloosterman, 2022) implemented in clean-room MIT Python. The method can:
- auto-detects narrowband peaks at power-line frequency and harmonics (50/60 Hz; handles slow drift),
- chunks the data by spatial stability to handle non-stationarity,
- removes the minimal number of DSS/PCA components needed per peak,
- aims to preserve rank and minimize broadband distortion.

Why

mne.filter.notch_filter is robust but fixed-frequency; repeated or wide notches can cause spectral collateral damage and may alter rank when interference drifts or changes spatially (common in MoBI/mobile EEG and some MEG/iEEG/LFP). An adaptive approach reduces narrowband artifacts while protecting broadband content.

Paper.
MATLAB refrence: MariusKlug/zapline-plus.
My Python library (MIT): PyZapline_plus.

Describe your proposed implementation

Implement an adaptive, component-based line-noise remover as a new public function in mne.preprocessing, with an optional thin Raw/Epochs/Evoked method wrapper. The function mirrors MNE APIs (inst, picks, copy, verbose) and returns the cleaned instance (plus optional diagnostics) while preserving info, annotations, events, and rank.
The proposed public API looks like this :

mne.preprocessing.zapline(
    inst,
    picks="eeg",
    freqs="line",             # "line" => auto-detect 50/60 Hz (+ harmonics) with drift
    n_remove="auto",          # int | "auto" | list[int] per harmonic
    chunk_dur=None,           # seconds | "auto"; handles non-stationarity
    dss="auto",               # "auto" | dict (DSS/PCA config)
    guard=1.0,                # Hz around each peak to protect
    n_jobs=None,
    random_state=None,
    return_info=False,
    copy=True,
    verbose=None,
)

Algorithm

  1. Peak detection

    • Compute a robust PSD aggregated across picks (e.g., median of channel PSDs via Welch).
    • If freqs="line", locate the dominant peak in 45–65 Hz, snap to 50 or 60 Hz, then generate harmonics < sfreq/2.
    • If freqs is float/list, use provided peaks directly.
    • Apply a guard band (±guard Hz) around each peak to quantify pre/post power and to avoid over-cleaning.
  2. Chunking for non-stationarity

    • If chunk_dur is None/"auto", segment the data where spatial patterns are stable (e.g., windowed channel-covariance similarity; minimum length enforced).
    • If chunk_dur is a float, segment by duration. Chunk edges are cross-faded to avoid discontinuities.
  3. Component selection & removal (per chunk × harmonic)

  • Build a DSS/PCA transform emphasizing narrowband energy around the target peak (whitened data + band-focused criterion).
  • Rank components by narrowband-to-broadband ratio (z-score).
  • n_remove="auto" chooses the minimal number of components whose cumulative removal reduces the peak below a conservative threshold; otherwise use the user-given n_remove.
  • Remove selected components and invert the transform to reconstruct the cleaned chunk.
  1. Stitch & finalize
  • Concatenate chunks with cross-fades if needed; preserve inst.info, bads, annotations, projections, and event timing.
  • Compute diagnostics (pre/post PSD at peaks, components removed, chunk map).

Describe possible alternatives

From an API perspective, there are two packaging alternatives. One is to keep this as an official extension (e.g., mne-zapline). That gives faster releases and iteration but lives outside core. The other is to augment notch_filter with auto-peak detection; although convenient, it would mix fundamentally different concepts (filtering vs. spatial decomposition) and make diagnostics harder to expose. A dedicated mne.preprocessing.zapline function (or a thin apply_zapline method) keeps behavior clear and discoverable, surfaces diagnostics (peaks found, chunks, components removed, before/after PSD), and aligns with MNE’s preprocessing style.

Additional context

I have already implemented the method in Python as a clean-room library, PyZapline_plus (MIT), available here: PyZapline_plus
. The implementation follows the algorithmic ideas from Zapline-plus (Klug & Kloosterman, 2022)
I’m happy to adapt naming, defaults, thresholds, diagnostics, and API shape to match MNE conventions, and to refactor internals to meet core standards (docstrings, tests, tutorial, “What’s new”). I’m also open to either path—starting as an official extension (mne-zapline) or merging directly into core under mne.preprocessing—and I will revise the code based on maintainer feedback in this issue. If needed for a core merge, I’m willing to relicense my contribution under BSD-3 to align with MNE-Python. For reviewers’ convenience, I attached before/after PSD screenshots, a short synthetic benchmark notebook, and links to small test fixtures demonstrating peak detection, chunking for non-stationarity, and minimal DSS/PCA component removal.

Image Image Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    ENHneeds-discussionissues requiring a dev meeting discussion before the way forward is clear

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions