Skip to content

FIX: add recover_epochs fallback for EGI .mff epoch metadata mismatch#13992

Open
kveni12 wants to merge 4 commits into
mne-tools:mainfrom
kveni12:fix/egi-mff-recover-epochs-13951
Open

FIX: add recover_epochs fallback for EGI .mff epoch metadata mismatch#13992
kveni12 wants to merge 4 commits into
mne-tools:mainfrom
kveni12:fix/egi-mff-recover-epochs-13951

Conversation

@kveni12

@kveni12 kveni12 commented Jun 27, 2026

Copy link
Copy Markdown

Summary

Fixes #13951.

mne.io.read_raw_egi crashes with RuntimeError when reading certain EGI .mff files whose epochs.xml sample counts don't match the actual binary data. This happens with improperly closed recordings, PSG/PNS async writes, or impedance check pauses that create irregular fractional epochs.

This PR adds a recover_epochs=False keyword argument to read_raw_egi. When True, instead of raising, the epoch boundaries are rebuilt from the actual binary block structure (signal1.bin) and a RuntimeWarning is emitted.

Changes

  • mne/io/egi/egimff.py: _read_mff_header and _read_header accept recover_epochs; on mismatch, recover from signal_blocks["samples_block"] instead of raising
  • mne/io/egi/egi.py: read_raw_egi exposes recover_epochs=False with docstring
  • mne/io/egi/tests/test_egi.py: two new tests — one asserts the default still raises, one asserts recover_epochs=True loads successfully and emits RuntimeWarning

Test plan

  • test_recover_epochs_raises_by_default — corrupted epochs.xml with recover_epochs=False raises RuntimeError
  • test_recover_epochs_succeeds_with_flag — same file with recover_epochs=True loads and emits RuntimeWarning
  • All existing EGI tests pass

…mne-tools#13951)

When epochs.xml sample counts don't match the binary signal data (e.g.
improperly closed recordings, PSG/PNS async writes, impedance pauses),
read_raw_egi now accepts recover_epochs=True to rebuild epoch boundaries
from the actual binary block structure instead of raising RuntimeError.
Default behavior (recover_epochs=False) is unchanged.
@welcome

welcome Bot commented Jun 27, 2026

Copy link
Copy Markdown

Hello! 👋 Thanks for opening your first pull request here! ❤️ We will try to get back to you soon. 🚴

@larsoner

Copy link
Copy Markdown
Member

Seems like a reasonable idea to me, thoughts @PragnyaKhandelwal since you're interacting with this code a bit now?

@requires_testing_data
def test_recover_epochs_raises_by_default(tmp_path):
"""Test that mismatched epoch metadata raises RuntimeError by default."""
import re as _re

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.

No need to nest this import

Comment thread mne/io/egi/egi.py
Comment on lines 137 to +146

recover_epochs : bool
If True and an MFF file has inconsistent epoch metadata (e.g., from an
improperly closed recording or interrupted acquisition), attempt to recover
the epoch structure from the binary signal data instead of raising an error.
A :class:`RuntimeWarning` is emitted when recovery is performed.
Default is False.

.. versionadded:: 1.13.0

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.

Style CIs will complain about these extra newlines

Suggested change
recover_epochs : bool
If True and an MFF file has inconsistent epoch metadata (e.g., from an
improperly closed recording or interrupted acquisition), attempt to recover
the epoch structure from the binary signal data instead of raising an error.
A :class:`RuntimeWarning` is emitted when recovery is performed.
Default is False.
.. versionadded:: 1.13.0
recover_epochs : bool
If True and an MFF file has inconsistent epoch metadata (e.g., from an
improperly closed recording or interrupted acquisition), attempt to recover
the epoch structure from the binary signal data instead of raising an error.
A :class:`RuntimeWarning` is emitted when recovery is performed.
Default is False.
.. versionadded:: 1.13.0

Comment thread mne/io/egi/egimff.py


def _read_mff_header(filepath):
def _read_mff_header(filepath, recover_epochs=False):

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.

Suggested change
def _read_mff_header(filepath, recover_epochs=False):
def _read_mff_header(filepath, *, recover_epochs=False):

@PragnyaKhandelwal

PragnyaKhandelwal commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Seems like a reasonable idea to me, thoughts @PragnyaKhandelwal since you're interacting with this code a bit now?

Looks good to me! The approach is clean and the default recover_epochs=False keeps existing behaviour intact.

One thing worth flagging for the future: the recovery fallback relies on _get_blocks from mne/io/egi/general.py, which is a legacy binary-parsing helper targeted for removal as part of the ongoing mffpy migration (#13926). That won't happen until Phase 2 (when PNS binary reading is switched to mffpy), so there's no conflict with current work. But when _get_blocks does eventually go away, the recovery logic here will need to be updated to use mffpy's block structure API instead — worth keeping in mind.

@larsoner

Copy link
Copy Markdown
Member

Okay @kveni12 you should be good to address the comments I made above ☝️ and push . Let me know when you do and I should be able to look and mark for merge-when-green!

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.

Fallback recovery for "EGI epoch first/last samps could not be parsed" (ValueError in read_raw_egi)

3 participants