Replace linear channel-name lookups with dictionaries - #14245
Merged
larsoner merged 1 commit intoAug 28, 2026
Conversation
Three places matched channel names by scanning a list, which is quadratic in
the channel count:
- _make_projector() called col_names.index(name) inside a loop over ch_names.
A set was already used for the membership test, but not for the lookup.
- pick_channels() did three list scans per included channel: 'in ch_names',
'not in exclude', and ch_names.index(name).
- _picks_str_to_idx() called info['ch_names'].index(pick) per pick.
Each becomes a single dict lookup. In _picks_str_to_idx the map is built with
setdefault so it keeps list.index()'s first-match semantics exactly, even
though ambiguous names are rejected further down.
Medians of 3 interleaved A/B rounds against a pristine main worktree:
128 ch 306 ch 1000 ch
pick_channels 2.1x 3.6x 10.2x
_picks_to_idx 1.4x 2.3x 6.4x
_make_projector 1.6x 2.9x 3.3x
End to end, apply_proj() on a 392-channel raw goes 21.7 -> 17.8 ms.
Results are unchanged: 24/24 projector matrices bit-identical across six real
MEG files with and without bads, and 700 fuzzed pick_channels/_picks_to_idx
cases identical including duplicate names and the exceptions raised.
bruAristimunha
requested review from
agramfort,
drammock and
larsoner
as code owners
August 28, 2026 14:34
larsoner
approved these changes
Aug 28, 2026
larsoner
left a comment
Member
There was a problem hiding this comment.
Nice, I think it actually makes the code cleaner in a couple of places, too!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reference issue (if any)
None. Found while profiling a read → pick → filter → epoch → average workflow.
What does this implement/fix?
Three places in
mne/_fiffmatched channel names by scanning a list, which isquadratic in the channel count:
_make_projector()calledcol_names.index(name)inside a loop overch_names. Asetwas already used for the membership test, but the lookupnext to it still scanned.
pick_channels()did three list scans per included channel:name in ch_names,name not in exclude, andch_names.index(name)._picks_str_to_idx()calledinfo["ch_names"].index(pick)once per pick.Each becomes a single dict lookup.
ch_namesis already asserted unique inpick_channels, and duplicatecol_namesalready raise in_make_projector.In
_picks_str_to_idxthe map is built withsetdefault, so it keepslist.index()'s first-match semantics exactly, even though ambiguous names arerejected further down anyway.
Medians of 3 interleaved A/B rounds against a pristine
mainworktree:pick_channels_picks_to_idx_make_projectorEnd to end,
apply_proj()on a 392-channel raw goes 21.7 → 17.8 ms. Thespeedup grows with channel count, so it matters most for whole-head MEG and
high-density EEG.
Correctness
mainacross six real MEG files,with and without bad channels.
pick_channels/_picks_to_idxcases identical, includingduplicate names, absent names,
ordered=True/False, and the exceptionsraised.
pytest mne/_fiff mne/io mne/tests mne/preprocessing mne/channels:3506 passed, 0 failed.
Two tests are added. They pin name-based mapping specifically: the projector
test uses
col_namespermuted relative toch_names, so a positional matchannihilates the wrong direction and fails. Mutation-tested — substituting
position for name in either function is caught, as is dropping the
badsfilter, ignoring
exclude, and silently skipping missing names.Additional information
AI disclosure: I directed the work and reviewed and tested everything; Claude
Code (Claude Opus 5) did the profiling that found these, made the edits, and
ran the A/B measurements and fuzzing under my direction.