Skip to content

Commit 34d9c7e

Browse files
MAINT: Replace internal EEG sample-reading helper with mffpy (mne-tools#13973)
Co-authored-by: Scott Huberty <seh33@uw.edu>
1 parent 5bf978b commit 34d9c7e

2 files changed

Lines changed: 54 additions & 59 deletions

File tree

doc/changes/dev/13973.other.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Replace the internal binary-block EEG sample reader in :func:`mne.io.read_raw_egi` with ``mffpy``-backed reading via ``mffpy.Reader.get_physical_samples_from_epoch``, removing manual block-header parsing and ``np.fromfile`` calls, by `Pragnya Khandelwal`_.

mne/io/egi/egimff.py

Lines changed: 53 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,46 @@
3535
REFERENCE_NAMES = ("VREF", "Vertex Reference")
3636

3737

38+
def _get_mff_reader(input_fname):
39+
"""Open an mffpy.Reader for the MFF directory, with raw (uncalibrated) EEG.
40+
41+
mffpy applies its own GCAL scaling by default, but MNE's ``cals`` already
42+
incorporate that gain (see ``_get_eeg_calibration_info``), so it is
43+
disabled here to avoid applying it twice.
44+
"""
45+
mffpy = _import_mffpy("read EGI MFF data")
46+
mff_reader = mffpy.Reader(input_fname)
47+
mff_reader.set_calibration("EEG", None)
48+
return mff_reader
49+
50+
51+
def _disk_range_to_epochs(egi_info, disk_start, disk_stop):
52+
"""Map a contiguous range of on-disk EEG samples to mffpy epoch windows.
53+
54+
EGI MFF signal blocks are stored back-to-back on disk with no gaps
55+
between recording epochs. This generator yields, for each recording epoch,
56+
the epoch index plus the relative ``(t0, dt)`` window (in seconds) and the
57+
corresponding ``[out_start, out_stop)`` slice into the caller's output
58+
array.
59+
"""
60+
first_samps = np.asarray(egi_info["first_samps"])
61+
last_samps = np.asarray(egi_info["last_samps"])
62+
sfreq = egi_info["sfreq"]
63+
epoch_lengths = last_samps - first_samps
64+
epoch_disk_offsets = np.concatenate([[0], np.cumsum(epoch_lengths)])
65+
66+
for ei in range(len(epoch_lengths)):
67+
ep_disk_start = epoch_disk_offsets[ei]
68+
ep_disk_stop = epoch_disk_offsets[ei + 1]
69+
ov_start = max(disk_start, ep_disk_start)
70+
ov_stop = min(disk_stop, ep_disk_stop)
71+
if ov_start >= ov_stop:
72+
continue
73+
t0 = (ov_start - ep_disk_start) / sfreq
74+
dt = (ov_stop - ov_start) / sfreq
75+
yield ei, t0, dt, ov_start - disk_start, ov_stop - disk_start
76+
77+
3878
def _read_mff_header(filepath):
3979
"""Read mff header."""
4080
_soft_import("mffpy", "reading EGI MFF data")
@@ -489,6 +529,7 @@ def __init__(
489529

490530
file_bin = op.join(input_fname, egi_info["eeg_fname"])
491531
egi_info["egi_events"] = egi_events
532+
egi_info["mff_path"] = input_fname
492533

493534
# Check how many channels to read are from EEG
494535
keys = ("eeg", "sti", "pns")
@@ -584,10 +625,6 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult):
584625
egi_info = self._raw_extras[fi]
585626
one = np.zeros((egi_info["kind_bounds"][-1], stop - start))
586627

587-
# info about the binary file structure
588-
n_channels = egi_info["n_channels"]
589-
samples_block = egi_info["samples_block"]
590-
591628
# Check how many channels to read are from each type
592629
bounds = egi_info["kind_bounds"]
593630
if isinstance(idx, slice):
@@ -620,61 +657,18 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult):
620657
stop = disk_samps[disk_use_idx[-1]] + 1
621658
assert len(disk_use_idx) == stop - start
622659

623-
# Get starting/stopping block/samples
624-
block_samples_offset = np.cumsum(samples_block)
625-
offset_blocks = np.sum(block_samples_offset <= start)
626-
offset_samples = start - (
627-
block_samples_offset[offset_blocks - 1] if offset_blocks > 0 else 0
628-
)
629-
630-
# TODO: Refactor this reading with the PNS reading in a single function
631-
# (DRY)
632-
samples_to_read = stop - start
633-
with open(self.filenames[fi], "rb", buffering=0) as fid:
634-
# Go to starting block
635-
current_block = 0
636-
current_block_info = None
637-
current_data_sample = 0
638-
while current_block < offset_blocks:
639-
this_block_info = _block_r(fid)
640-
if this_block_info is not None:
641-
current_block_info = this_block_info
642-
fid.seek(current_block_info["block_size"], 1)
643-
current_block += 1
644-
645-
# Start reading samples
646-
while samples_to_read > 0:
647-
logger.debug(f" Reading from block {current_block}")
648-
this_block_info = _block_r(fid)
649-
current_block += 1
650-
if this_block_info is not None:
651-
current_block_info = this_block_info
652-
653-
to_read = current_block_info["nsamples"] * current_block_info["nc"]
654-
block_data = np.fromfile(fid, dtype, to_read)
655-
block_data = block_data.reshape(n_channels, -1, order="C")
656-
657-
# Compute indexes
658-
samples_read = block_data.shape[1]
659-
logger.debug(f" Read {samples_read} samples")
660-
logger.debug(f" Offset {offset_samples} samples")
661-
if offset_samples > 0:
662-
# First block read, skip to the offset:
663-
block_data = block_data[:, offset_samples:]
664-
samples_read = samples_read - offset_samples
665-
offset_samples = 0
666-
if samples_to_read < samples_read:
667-
# Last block to read, skip the last samples
668-
block_data = block_data[:, :samples_to_read]
669-
samples_read = samples_to_read
670-
logger.debug(f" Keep {samples_read} samples")
671-
672-
s_start = current_data_sample
673-
s_end = s_start + samples_read
674-
675-
one[eeg_one, disk_use_idx[s_start:s_end]] = block_data[eeg_in]
676-
samples_to_read = samples_to_read - samples_read
677-
current_data_sample = current_data_sample + samples_read
660+
if len(eeg_one):
661+
mff_reader = _get_mff_reader(egi_info["mff_path"])
662+
mff_epochs = mff_reader.epochs
663+
for ei, t0, dt, out_start, out_stop in _disk_range_to_epochs(
664+
egi_info, start, stop
665+
):
666+
logger.debug(f" Reading from epoch {ei} t0={t0} dt={dt}")
667+
eeg_chunk, _ = mff_reader.get_physical_samples_from_epoch(
668+
mff_epochs[ei], t0=t0, dt=dt, channels=["EEG"]
669+
)["EEG"]
670+
cols = disk_use_idx[out_start:out_stop]
671+
one[eeg_one, cols] = eeg_chunk[eeg_in]
678672

679673
if len(pns_one) > 0:
680674
# PNS Data is present and should be read:

0 commit comments

Comments
 (0)