Fallback recovery for "EGI epoch first/last samps could not be parsed" (ValueError in read_raw_egi)
Los mantenedores suelen responder en 1 día
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 48/100
Línea de trabajo
Comienza en mne/io/egi/egimff.py, especialmente en _read_header y en la validación que genera el error al analizar los epochs; después, inspecciona el punto de entrada _get_blocks utilizado con signal1.bin. Reproduce el problema con un archivo MFF afectado, si está disponible, y compara epochs.xml con la información de los bloques binarios. Se considera terminado cuando read_raw_egi puede manejar la discrepancia reportada sin modificar los datos originales y el comportamiento está cubierto por pruebas.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Description of the problem
mne.io.read_raw_egi throws a ValueError when reading certain EGI .mff files
because n_samps_epochs (from epochs.xml) does not match the actual block samples in signal1.bin.
This mismatch occurs due to: Improperly closed recordings , asynchronous writes during PSG/PNS recording and pauses or impedance checks during recording that create irregular fractional epochs and maybe something else
I wrote a lightweight fallback function that reuses MNE's internal _get_blocks to calculate the exact duration based on the actual binary payload, recreating a valid epochs.xml on the fly - so n_samps_epochs == n_samps_block.
Would you be open to a Pull Request adding this as a fallback mechanism (e.g., recover_epochs=True) in read_raw_egi?
As a permanent solution within MNE, rather than rewriting the XML on disk,we can modifying the validation block in mne/io/egi/egimff.py (inside _read_header, (lines 97-108). Instead of raising a RuntimeError when bad == True, it could dynamically overwrite the epochs dictionary in memory with n_samps_block and signal_blocks["n_blocks"] (perhaps guarded by a recover_epochs=True parameter). So we can fix epoch file without altering the user's original raw data on disk
Thanks
Steps to reproduce
import xml.etree.ElementTree as ET
import re
import shutil
from pathlib import Path
from mne.io.egi.general import _get_blocks
def _recover_egi_epochs_xml(mff_path):
"""Recover or recreate a corrupted epochs.xml for EGI .mff files.
This function parses the binary `signal1.bin` using MNE's internal `_get_blocks`
to calculate the exact number of blocks and samples. It then retrieves the time
divisor from `info.xml` and calculates the precise ``endTime`` in microseconds
to reconstruct a valid ``epochs.xml``.
Parameters
----------
mff_path : str | Path
The path to the .mff directory.
"""
mff_path = Path(mff_path)
signal_bin = mff_path / "signal1.bin"
epochs_xml = mff_path / "epochs.xml"
info_xml_path = mff_path / "info.xml"
if not signal_bin.exists() or not info_xml_path.exists():
raise FileNotFoundError(f"Missing essential MFF files in {mff_path}")
# 1. Use MNE's built-in parser to get exact block and sample counts
# This guarantees our new epochs.xml will perfectly match MNE's expectations
signal_blocks = _get_blocks(str(signal_bin))
total_samples = int(signal_blocks["samples_block"].sum())
last_block_idx = int(signal_blocks["n_blocks"])
sfreq = int(signal_blocks["sfreq"])
# 2. Extract record time format from info.xml to determine fractional multiplier
tree = ET.parse(info_xml_path)
root = tree.getroot()
record_time_elem = root.find(".//{http://www.egi.com/info_mff}recordTime")
if record_time_elem is None:
raise ValueError("Could not find <recordTime> in info.xml")
record_time = record_time_elem.text
match = re.match(r".*\.(\d{6}(?:\d{3})?)[+-]", record_time)
if not match:
raise ValueError(f"Unexpected recordTime format: {record_time}")
frac = match.group(1)
# Determine if microseconds (len 6) or nanoseconds (len 9)
div = 1000 if len(frac) == 6 else 1000000
# 3. Calculate exact duration in microseconds
# endTime is expected in microseconds.
duration_us = int((total_samples / sfreq) * (div * 1000))
# 4. Backup existing corrupted epochs.xml (if it exists)
if epochs_xml.exists():
backup_epochs_xml = mff_path.parent / f"{mff_path.name}__epochs_corrupted.xml"
if not backup_epochs_xml.exists():
shutil.copy2(epochs_xml, backup_epochs_xml)
# 5. Write the corrected epochs.xml
xml_str = (
f'<?xml version="1.0" encoding="utf-8"?>\n'
f'<epochs xmlns="http://www.egi.com/epochs_mff">\n'
f' <epoch>\n'
f' <beginTime>0</beginTime>\n'
f' <endTime>{duration_us}</endTime>\n'
f' <firstBlock>1</firstBlock>\n'
f' <lastBlock>{last_block_idx}</lastBlock>\n'
f' </epoch>\n'
f'</epochs>\n'
)
epochs_xml.write_text(xml_str, encoding="utf-8")
print(f"Successfully created/recovered epochs.xml for {mff_path.name} with {total_samples} samples and {last_block_idx} blocks.")
Link to data
No response
Expected results
MNE either loads the file (e.g. throwing a warning), or offers a recover_epochs=True parameter that uses its internal _get_blocks to rebuild the epochs.xml on the fly to salvage the clinical data.
Actual results
RuntimeError: EGI epoch first/last samps could not be parsed:
[0]
[17680358]
Additional information
- Lenguaje dominante
- Python
- Estrellas
- 3.6k
- Forks
- 1.6k
- Merge medio
- 1 d 4 h
- PR fusionados (30 d)
- 73
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de mne-tools/mne-python
-
ENH: Add `description` filter parameter to `Raw.crop_by_annotations()`Posiblemente ocupada @aman-coder03 la tomó hace 198 días. AbiertoENH
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
mne-tools/mne-python#13743 · 2 comentarios ·
Los mantenedores suelen responder en 1 día
-
auto-issue
Dificultad 3/5 1-2 días Aptitud para principiantes 48/100
mne-tools/mne-python#14442 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
mne-bids cannot find split files saved with split_naming="bids" when the name has descPosiblemente ocupada @BabaSanfour la tomó hace 3 días. AbiertoBUG
Dificultad 3/5 1-2 días Aptitud para principiantes 78/100
mne-tools/mne-python#14431 · 5 comentarios ·
Los mantenedores suelen responder en 1 día
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 35/100
mne-tools/mne-python#14428 · 5 comentarios ·
Los mantenedores suelen responder en 1 día
-
ENH
Dificultad 3/5 1-2 días Aptitud para principiantes 58/100
mne-tools/mne-python#14427 · 2 comentarios ·
Los mantenedores suelen responder en 1 día
Todos los issues de mne-tools/mne-python
Issues similares
-
Validate non-divisible effective batch sizes in the Unsloth LoRA backendPosiblemente ocupada @debesh-biswas la tomó hoy. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
-
`rx.Model` event arguments emit `PydanticDeprecatedSince20` once per payload keyPosiblemente ocupada Un pull request vinculado a esta issue está abierto o ya se fusionó. Abierto
Dificultad 1/5 Menos de una hora Aptitud para principiantes 88/100
reflex-dev/reflex#7634 · 1 comentario ·
Los mantenedores suelen responder en 2 días
-
documentation
Dificultad 1/5 1-3 horas Aptitud para principiantes 82/100
topoteretes/cognee#6239 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
ranaroussi/yfinance#3009 ·
Los mantenedores suelen responder en 1 día
-
Test discovery aborts on GPU builds without cuStateVec: check_cuStateVec expects a failed result but set_config() now raises RuntimeErrorPosiblemente ocupada @dmeliksetian la tomó hoy. Abiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
Qiskit/qiskit-aer#2495 ·
Los mantenedores suelen responder en 1 día