Skip to content

Commit 9bee5a7

Browse files
Merge branch 'main' into perf/mef-session-cache
2 parents a186726 + 982d7a0 commit 9bee5a7

7 files changed

Lines changed: 72 additions & 41 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Speed up :func:`mne.io.read_raw_egi` on simple-binary files by reading the event channels in blocks instead of one sample at a time, by `Bruno Aristimunha`_.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Speed up :func:`mne.io.read_raw_persyst` and :func:`mne.io.read_raw_nihon` by decoding data in cache-sized blocks rather than materializing the whole request, by `Bruno Aristimunha`_.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Speed up :func:`mne.io.read_raw_fif` on integer-format files by normalizing the byte order of each data buffer before calibrating it, by `Bruno Aristimunha`_.

mne/_fiff/utils.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,19 @@ def _mult_cal_one(data_view, one, idx, cals, mult):
8888
# (was three passes plus a full float64 temporary).
8989
# Benchmark (128 ch x 1024 samples): ~85 -> ~30 us per call
9090
# on BrainVision/FIF window reads.
91-
np.multiply(one[idx], cals.reshape(-1, 1), out=data_view, casting="unsafe")
91+
one = one[idx]
92+
swapped_ints = not one.dtype.isnative and one.dtype.kind in "iu"
93+
if swapped_ints and not one.flags.c_contiguous:
94+
# FIFF stores sample-major, so `one` is the transpose of a
95+
# big-endian integer tag and consecutive samples of a channel
96+
# sit a row apart. np.multiply has no fast loop for input that
97+
# is byte-swapped *and* strided, so it swaps element by element
98+
# while it also casts and transposes; swapping up front in one
99+
# pass is cheaper. Floats are excluded because there the swap
100+
# costs about what it saves, and contiguous input already has a
101+
# fast loop.
102+
one = one.astype(one.dtype.newbyteorder("="))
103+
np.multiply(one, cals.reshape(-1, 1), out=data_view, casting="unsafe")
92104
else:
93105
one = np.asarray(one, dtype=data_view.dtype)
94106
np.take(one, idx, axis=0, out=data_view)

mne/io/egi/egi.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,27 @@ def my_fread(*x, **y):
7979
return info
8080

8181

82+
# read whole frames a few MB at a time rather than the whole recording at once
83+
_EVENT_BLOCK_BYTES = 4 * 1024**2
84+
85+
8286
def _read_events(fid, info):
8387
"""Read events."""
84-
events = np.zeros([info["n_events"], info["n_segments"] * info["n_samples"]])
88+
n_samples = info["n_samples"]
89+
# Each sample is one frame of n_channels data values followed by n_events
90+
# event values, so read whole frames and keep the event rows. Seeking past
91+
# the data channels instead costs a seek and a read per sample, which
92+
# dominates the time to open a long recording.
93+
n_rows = info["n_channels"] + info["n_events"]
94+
events = np.zeros([info["n_events"], info["n_segments"] * n_samples])
8595
fid.seek(36 + info["n_events"] * 4, 0) # skip header
86-
for si in range(info["n_samples"]):
87-
# skip data channels
88-
fid.seek(info["n_channels"] * info["dtype"].itemsize, 1)
89-
# read event channels
90-
events[:, si] = np.fromfile(fid, info["dtype"], info["n_events"])
96+
n_block = max(1, _EVENT_BLOCK_BYTES // (n_rows * info["dtype"].itemsize))
97+
for start in range(0, n_samples, n_block):
98+
n_read = min(n_block, n_samples - start)
99+
frames = np.fromfile(fid, info["dtype"], n_read * n_rows)
100+
events[:, start : start + n_read] = frames.reshape(n_read, n_rows).T[
101+
info["n_channels"] :
102+
]
91103
return events
92104

93105

mne/io/nihon/nihon.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,10 @@ def _map_ch_to_specs(ch_name, chan_labels_upper):
418418
return out
419419

420420

421+
# decode in cache-sized blocks rather than one huge one (1.8x on a 106 MB file)
422+
_BLOCK_BYTES = 1024**2
423+
424+
421425
@fill_doc
422426
class RawNihon(BaseRaw):
423427
"""Raw object from a Nihon Kohden EEG file.
@@ -566,13 +570,20 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult):
566570
rel_start = start - ends[start_block - 1]
567571
start_offset = datastart + rel_start * n_channels * 2
568572

573+
# Decode a few MB at a time: each step below builds a temporary the
574+
# size of the block, so reading the whole request at once pushes
575+
# them all out of cache.
576+
n_times = stop - start
577+
n_block = max(1, _BLOCK_BYTES // 2 // n_channels)
569578
with open(self.filenames[fi], "rb") as fid:
570-
to_read = (stop - start) * n_channels
571579
fid.seek(start_offset)
572-
block_data = np.fromfile(fid, "<u2", to_read) + 0x8000
573-
block_data = block_data.astype(np.int16)
574-
block_data = block_data.reshape(n_channels, -1, order="F")
575-
block_data = block_data[:-1] * cal # cast to float64
576-
block_data += offsets
577-
block_data *= gains
578-
_mult_cal_one(data, block_data, idx, cals, mult)
580+
for sample_start in range(0, n_times, n_block):
581+
n_read = min(n_block, n_times - sample_start)
582+
block_data = np.fromfile(fid, "<u2", n_read * n_channels) + 0x8000
583+
block_data = block_data.astype(np.int16)
584+
block_data = block_data.reshape(n_channels, -1, order="F")
585+
block_data = block_data[:-1] * cal # cast to float64
586+
block_data += offsets
587+
block_data *= gains
588+
data_view = data[:, sample_start : sample_start + n_read]
589+
_mult_cal_one(data_view, block_data, idx, cals, mult)

mne/io/persyst/persyst.py

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from ..._fiff.constants import FIFF
1515
from ..._fiff.meas_info import create_info
16-
from ..._fiff.utils import _mult_cal_one
16+
from ..._fiff.utils import _read_segments_file
1717
from ...annotations import Annotations
1818
from ...utils import _check_fname, fill_doc, logger, verbose, warn
1919
from ..base import BaseRaw
@@ -55,6 +55,11 @@ def read_raw_persyst(
5555
return RawPersyst(fname, preload, verbose)
5656

5757

58+
# read in cache-sized blocks rather than one huge one (2.3x on a 107 MB file);
59+
# see _read_segments_file() for why a smaller block is faster
60+
_BLOCK_BYTES = 16 * 1024**2
61+
62+
5863
@fill_doc
5964
class RawPersyst(BaseRaw):
6065
"""Raw object from a Persyst file.
@@ -267,31 +272,19 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult):
267272
binary files. In addition, it stores the calibration to convert
268273
data to uV in the lay file.
269274
"""
270-
dtype = self._raw_extras[fi]["dtype"]
271-
n_chs = self._raw_extras[fi]["n_chs"]
272-
dat_fname = self.filenames[fi]
273-
274-
# compute samples count based on start and stop
275-
time_length_samps = stop - start
276-
277-
# read data from .dat file into array of correct size, then calibrate
278-
# records = recnum rows x inf columns
279-
count = time_length_samps * n_chs
280-
281-
# seek the dat file
282-
with open(dat_fname, "rb") as dat_file_ID:
283-
# allow offset to occur
284-
dat_file_ID.seek(n_chs * dtype.itemsize * start, 1)
285-
286-
# read in the actual record starting at possibly offset
287-
record = np.fromfile(dat_file_ID, dtype=dtype, count=count)
288-
289-
# chs * rows
290-
# cast as float32; more than enough precision
291-
record = np.reshape(record, (n_chs, -1), order="F").astype(np.float32)
292-
293-
# calibrate to convert to V and handle mult
294-
_mult_cal_one(data, record, idx, cals, mult)
275+
_read_segments_file(
276+
self,
277+
data,
278+
idx,
279+
fi,
280+
start,
281+
stop,
282+
cals,
283+
mult,
284+
dtype=self._raw_extras[fi]["dtype"],
285+
n_channels=self._raw_extras[fi]["n_chs"],
286+
max_block_bytes=_BLOCK_BYTES,
287+
)
295288

296289

297290
def _get_subjectinfo(patient_dict):

0 commit comments

Comments
 (0)