From 0c8efdc38c1ed9d637f257ef30e232402f3f9254 Mon Sep 17 00:00:00 2001 From: Bru Date: Sat, 29 Aug 2026 21:57:15 +0200 Subject: [PATCH 1/2] Decode Persyst and Nihon Kohden data in cache-sized blocks Both readers materialize the entire requested range before calibrating it, so every intermediate is as large as the request and none of it stays in cache. Persyst had a hand-rolled copy of _read_segments_file plus one extra full-size temporary: np.reshape(record, (n_chs, -1), order='F').astype(np.float32). _mult_cal_one already casts, so that .astype was a whole-array copy for nothing. The method is now a call to the shared helper -- a net deletion. Nihon Kohden reads the whole request and then builds four full-size temporaries over it (+ 0x8000, .astype(int16), * cal to float64, += / *=). Same work, now a block at a time. raw.get_data(), interleaved cross-process medians: Persyst 107 MB 88.7 -> 39.3 ms 2.26x Nihon 106 MB 267.6 -> 147.7 ms 1.81x Persyst shipped 1.05x Nihon shipped 0.995x (1600 reps; fits one block) Persyst's gain splits cleanly: 1.32x from dropping the redundant .astype alone, the rest from the smaller block. The two constants differ (16 MiB Persyst, 1 MiB Nihon) because the optimum tracks time points per block, not bytes -- Nihon has 25 channels and builds four temporaries per block, and its sweep improves down to ~256 KiB before falling off a cliff at 64 KiB, so 1 MiB is chosen for margin. One behaviour note: dropping Persyst's .astype(np.float32) changes results for a 32-bit .dat whose raw counts exceed 2**24, where float32 was silently rounding them. That is a precision fix rather than a regression; 16-bit files and the shipped 32-bit fixture are unaffected. Output is bit-identical across every readable Persyst and Nihon fixture, including the synthesised large ones. --- mne/io/nihon/nihon.py | 27 ++++++++++++++++------- mne/io/persyst/persyst.py | 45 +++++++++++++++++---------------------- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/mne/io/nihon/nihon.py b/mne/io/nihon/nihon.py index 61dabd4a03f..feb2067e7a2 100644 --- a/mne/io/nihon/nihon.py +++ b/mne/io/nihon/nihon.py @@ -418,6 +418,10 @@ def _map_ch_to_specs(ch_name, chan_labels_upper): return out +# decode in cache-sized blocks rather than one huge one (1.8x on a 106 MB file) +_BLOCK_BYTES = 1024**2 + + @fill_doc class RawNihon(BaseRaw): """Raw object from a Nihon Kohden EEG file. @@ -566,13 +570,20 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): rel_start = start - ends[start_block - 1] start_offset = datastart + rel_start * n_channels * 2 + # Decode a few MB at a time: each step below builds a temporary the + # size of the block, so reading the whole request at once pushes + # them all out of cache. + n_times = stop - start + n_block = max(1, _BLOCK_BYTES // 2 // n_channels) with open(self.filenames[fi], "rb") as fid: - to_read = (stop - start) * n_channels fid.seek(start_offset) - block_data = np.fromfile(fid, " Date: Sat, 29 Aug 2026 21:59:02 +0200 Subject: [PATCH 2/2] Add changelog entry --- doc/changes/dev/14251.newfeature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/dev/14251.newfeature.rst diff --git a/doc/changes/dev/14251.newfeature.rst b/doc/changes/dev/14251.newfeature.rst new file mode 100644 index 00000000000..db0fb37c89c --- /dev/null +++ b/doc/changes/dev/14251.newfeature.rst @@ -0,0 +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`_.