Skip to content

Commit 564a978

Browse files
Speed up small reads on the Raw.get_data hot path
get_data resolves picks=None to arange directly instead of going through string-based channel-name machinery on every call, and _mult_cal_one fuses gather, type-cast, and calibration into a single pass instead of three. Adds an internal batched window reader (Raw._get_windows) that shares per-call setup across windows and can fill a caller-provided float32/float64 buffer.
1 parent acb46e2 commit 564a978

4 files changed

Lines changed: 67 additions & 7 deletions

File tree

‎mne/_fiff/pick.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1334,6 +1334,18 @@ def _picks_to_idx(
13341334
)
13351335
raise TypeError(msg)
13361336
del extra_repr
1337+
# Fast path: an integer ndarray with all values already in range needs no
1338+
# copy or further checks. This matters for callers resolving picks on
1339+
# every access (e.g., Raw.get_data in deep-learning training loops).
1340+
if (
1341+
picks.dtype.kind == "i"
1342+
and picks.size
1343+
and picks.min() >= 0
1344+
and picks.max() < n_chan
1345+
):
1346+
if return_kind:
1347+
return picks, picked_ch_type_or_generic
1348+
return picks
13371349
picks = picks.astype(int)
13381350

13391351
#

‎mne/_fiff/utils.py‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,22 +73,27 @@ def _find_channels(ch_names, ch_type="EOG"):
7373

7474
def _mult_cal_one(data_view, one, idx, cals, mult):
7575
"""Take a chunk of raw data, multiply by mult or cals, and store."""
76-
one = np.asarray(one, dtype=data_view.dtype)
7776
assert data_view.shape[1] == one.shape[1], (
7877
data_view.shape[1],
7978
one.shape[1],
8079
) # noqa: E501
8180
if mult is not None:
81+
one = np.asarray(one, dtype=data_view.dtype)
8282
assert mult.ndim == one.ndim == 2
8383
data_view[:] = mult @ one[idx]
8484
else:
8585
assert cals is not None
8686
if isinstance(idx, slice):
87-
data_view[:] = one[idx]
87+
# Hot path: gather + type-cast + calibration in a single pass,
88+
# without materializing an intermediate float64 copy of `one`
89+
# (`one[idx]` is a view for basic slices). Numerically identical
90+
# to cast-then-scale because both are elementwise.
91+
np.multiply(one[idx], cals.reshape(-1, 1), out=data_view,
92+
casting="unsafe")
8893
else:
89-
# faster than doing one = one[idx]
94+
one = np.asarray(one, dtype=data_view.dtype)
9095
np.take(one, idx, axis=0, out=data_view)
91-
data_view *= cals
96+
data_view *= cals
9297

9398

9499
def _blk_read_lims(start, stop, buf_len):

‎mne/io/base.py‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,37 @@ def _parse_get_set_params(self, item):
877877

878878
return sel, start, stop
879879

880+
def _get_windows(self, starts, width, *, out=None, sel=None):
881+
"""Read many equal-width windows with setup shared across them."""
882+
starts = np.atleast_1d(np.asarray(starts, dtype=np.int64)).ravel()
883+
width = int(width)
884+
if width <= 0:
885+
raise ValueError(f"width must be positive, got {width}")
886+
n_times = self.n_times
887+
bad = (starts < 0) | (starts + width > n_times)
888+
if bad.any():
889+
raise ValueError(
890+
f"window out of bounds at index {int(np.flatnonzero(bad)[0])}"
891+
)
892+
n_out = self.info["nchan"] if sel is None else len(sel)
893+
if out is None:
894+
out = np.empty((len(starts), n_out, width), dtype=self._dtype)
895+
elif out.shape != (len(starts), n_out, width):
896+
raise ValueError(
897+
f"out has shape {out.shape}, need {(len(starts), n_out, width)}"
898+
)
899+
elif out.dtype not in (np.float64, np.float32):
900+
raise ValueError(
901+
f"out dtype must be float64 or float32, got {out.dtype}"
902+
)
903+
for j, s0 in enumerate(starts):
904+
self._read_segment(
905+
start=int(s0), stop=int(s0) + width,
906+
sel=sel,
907+
data_buffer=out[j],
908+
)
909+
return out
910+
880911
def __getitem__(self, item):
881912
"""Get raw data and times.
882913
@@ -1001,7 +1032,14 @@ def get_data(
10011032
stop, types=("int-like", None), item_name="stop", type_name="int, None"
10021033
)
10031034

1004-
picks = _picks_to_idx(self.info, picks, "all", exclude=())
1035+
if picks is None:
1036+
# fast path: equivalent to _picks_to_idx(info, None, "all",
1037+
# exclude=()) but avoids channel-name resolution on every call,
1038+
# which matters for workloads making many small reads (e.g.,
1039+
# deep-learning training loops)
1040+
picks = np.arange(self.info["nchan"])
1041+
else:
1042+
picks = _picks_to_idx(self.info, picks, "all", exclude=())
10051043

10061044
# Get channel factors for conversion into specified unit
10071045
# (vector of ones if no conversion needed)

‎mne/utils/mixin.py‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -575,8 +575,13 @@ def _handle_tmin_tmax(self, tmin, tmax):
575575
type_name="int, float, None",
576576
)
577577

578-
# handle tmin/tmax as start and stop indices into data array
579-
n_times = self.times.size
578+
# handle tmin/tmax as start and stop indices into data array.
579+
# Prefer an integer n_times (available on Raw); falling back to
580+
# times.size there would materialize the full time vector on every
581+
# call, which dominates the cost of many small get_data() reads.
582+
n_times = getattr(self, "n_times", None)
583+
if n_times is None:
584+
n_times = self.times.size
580585
start = 0 if tmin is None else self.time_as_index(tmin)[0]
581586
stop = n_times if tmax is None else self.time_as_index(tmax)[0]
582587

0 commit comments

Comments
 (0)