Skip to content

Commit e293d51

Browse files
committed
MRG, BUG: Ensure channel entries are valid (#8416)
* BUG: Ensure channel entries are valid * FIX: One more
1 parent 60c563b commit e293d51

File tree

7 files changed

+50
-15
lines changed

7 files changed

+50
-15
lines changed

doc/changes/0.21.inc

+2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ Bugs
4848

4949
- Fix bug in :func:`mne.viz.plot_alignment` where ECoG and sEEG channels were not plotted and fNIRS channels were always plotted in the head coordinate frame by `Eric Larson`_ (:gh:`8393`)
5050

51+
- Fix bug in :func:`mne.set_bipolar_reference` where ``ch_info`` could contain invalid channel information keys by `Eric Larson`_
52+
5153
- Fix bug in :func:`mne.viz.plot_source_estimates`, which wouldn't accept the valid parameter value ``backend='pyvista'``, by `Guillaume Favelier`_
5254
5355
- Attempting to remove baseline correction from preloaded `~mne.Epochs` will now raise an exception by `Richard Höchenberger`_ (:gh:`8435`)

mne/channels/tests/test_layout.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ def _get_test_info():
3939
loc = np.array([0., 0., 0., 1., 0., 0., 0., 1., 0., 0., 0., 1.],
4040
dtype=np.float32)
4141
test_info['chs'] = [
42-
{'cal': 1, 'ch_name': 'ICA 001', 'coil_type': 0, 'coord_Frame': 0,
42+
{'cal': 1, 'ch_name': 'ICA 001', 'coil_type': 0, 'coord_frame': 0,
4343
'kind': 502, 'loc': loc.copy(), 'logno': 1, 'range': 1.0, 'scanno': 1,
4444
'unit': -1, 'unit_mul': 0},
45-
{'cal': 1, 'ch_name': 'ICA 002', 'coil_type': 0, 'coord_Frame': 0,
45+
{'cal': 1, 'ch_name': 'ICA 002', 'coil_type': 0, 'coord_frame': 0,
4646
'kind': 502, 'loc': loc.copy(), 'logno': 2, 'range': 1.0, 'scanno': 2,
4747
'unit': -1, 'unit_mul': 0},
4848
{'cal': 0.002142000012099743, 'ch_name': 'EOG 061', 'coil_type': 1,

mne/io/meas_info.py

+22-3
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@
6464
)
6565

6666

67+
_SCALAR_CH_KEYS = ('scanno', 'logno', 'kind', 'range', 'cal', 'coil_type',
68+
'unit', 'unit_mul', 'coord_frame')
69+
_ALL_CH_KEYS_SET = set(_SCALAR_CH_KEYS + ('loc', 'ch_name'))
70+
# XXX we need to require these except when doing simplify_info
71+
_MIN_CH_KEYS_SET = set(('kind', 'cal', 'unit', 'loc', 'ch_name'))
72+
73+
6774
def _get_valid_units():
6875
"""Get valid units according to the International System of Units (SI).
6976
@@ -191,6 +198,19 @@ def _format_trans(obj, key):
191198
obj[key] = Transform(t['from'], t['to'], t['trans'])
192199

193200

201+
def _check_ch_keys(ch, ci, name='info["chs"]', check_min=True):
202+
ch_keys = set(ch)
203+
bad = sorted(ch_keys.difference(_ALL_CH_KEYS_SET))
204+
if bad:
205+
raise KeyError(
206+
f'key{_pl(bad)} errantly present for {name}[{ci}]: {bad}')
207+
if check_min:
208+
bad = sorted(_MIN_CH_KEYS_SET.difference(ch_keys))
209+
if bad:
210+
raise KeyError(
211+
f'key{_pl(bad)} missing for {name}[{ci}]: {bad}',)
212+
213+
194214
# XXX Eventually this should be de-duplicated with the MNE-MATLAB stuff...
195215
class Info(dict, MontageMixin):
196216
"""Measurement information.
@@ -733,15 +753,14 @@ def _check_consistency(self, prepend_error=''):
733753
self[key] = float(self[key])
734754

735755
# Ensure info['chs'] has immutable entries (copies much faster)
736-
scalar_keys = ('unit_mul range cal kind coil_type unit '
737-
'coord_frame scanno logno').split()
738756
for ci, ch in enumerate(self['chs']):
757+
_check_ch_keys(ch, ci)
739758
ch_name = ch['ch_name']
740759
if not isinstance(ch_name, str):
741760
raise TypeError(
742761
'Bad info: info["chs"][%d]["ch_name"] is not a string, '
743762
'got type %s' % (ci, type(ch_name)))
744-
for key in scalar_keys:
763+
for key in _SCALAR_CH_KEYS:
745764
val = ch.get(key, 1)
746765
if not _is_numeric(val):
747766
raise TypeError(

mne/io/reference.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from scipy import linalg
1010

1111
from .constants import FIFF
12+
from .meas_info import _check_ch_keys
1213
from .proj import _has_eeg_average_ref_proj, make_eeg_average_ref_proj
1314
from .proj import setup_proj
1415
from .pick import pick_types, pick_channels, pick_channels_forward
@@ -469,15 +470,16 @@ def set_bipolar_reference(inst, anode, cathode, ch_name=None, ch_info=None,
469470

470471
# Merge specified and anode channel information dictionaries
471472
new_chs = []
472-
for an, ci in zip(anode, ch_info):
473+
for ci, (an, ch) in enumerate(zip(anode, ch_info)):
474+
_check_ch_keys(ch, ci, name='ch_info', check_min=False)
473475
an_idx = inst.ch_names.index(an)
474476
this_chs = deepcopy(inst.info['chs'][an_idx])
475477

476478
# Set channel location and coil type
477479
this_chs['loc'] = np.zeros(12)
478480
this_chs['coil_type'] = FIFF.FIFFV_COIL_EEG_BIPOLAR
479481

480-
this_chs.update(ci)
482+
this_chs.update(ch)
481483
new_chs.append(this_chs)
482484

483485
if copy:

mne/io/tests/test_meas_info.py

+10
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,16 @@ def test_check_consistency():
485485
if key == 'ch_name':
486486
info['ch_names'][idx] = old
487487

488+
# bad channel entries
489+
info2 = info.copy()
490+
info2['chs'][0]['foo'] = 'bar'
491+
with pytest.raises(KeyError, match='key errantly present'):
492+
info2._check_consistency()
493+
info2 = info.copy()
494+
del info2['chs'][0]['loc']
495+
with pytest.raises(KeyError, match='key missing'):
496+
info2._check_consistency()
497+
488498

489499
def _test_anonymize_info(base_info):
490500
"""Test that sensitive information can be anonymized."""

mne/io/tests/test_reference.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -319,9 +319,12 @@ def test_set_bipolar_reference():
319319
raw = read_raw_fif(fif_fname, preload=True)
320320
raw.apply_proj()
321321

322-
reref = set_bipolar_reference(raw, 'EEG 001', 'EEG 002', 'bipolar',
323-
{'kind': FIFF.FIFFV_EOG_CH,
324-
'extra': 'some extra value'})
322+
ch_info = {'kind': FIFF.FIFFV_EOG_CH, 'extra': 'some extra value'}
323+
with pytest.raises(KeyError, match='key errantly present'):
324+
set_bipolar_reference(raw, 'EEG 001', 'EEG 002', 'bipolar', ch_info)
325+
ch_info.pop('extra')
326+
reref = set_bipolar_reference(
327+
raw, 'EEG 001', 'EEG 002', 'bipolar', ch_info)
325328
assert (reref.info['custom_ref_applied'])
326329

327330
# Compare result to a manual calculation
@@ -347,7 +350,6 @@ def test_set_bipolar_reference():
347350
assert_equal(bp_info[key], FIFF.FIFFV_EOG_CH)
348351
else:
349352
assert_equal(bp_info[key], an_info[key])
350-
assert_equal(bp_info['extra'], 'some extra value')
351353

352354
# Minimalist call
353355
reref = set_bipolar_reference(raw, 'EEG 001', 'EEG 002')
@@ -366,8 +368,8 @@ def test_set_bipolar_reference():
366368
['EEG 001', 'EEG 003'],
367369
['EEG 002', 'EEG 004'],
368370
['bipolar1', 'bipolar2'],
369-
[{'kind': FIFF.FIFFV_EOG_CH, 'extra': 'some extra value'},
370-
{'kind': FIFF.FIFFV_EOG_CH, 'extra': 'some extra value'}],
371+
[{'kind': FIFF.FIFFV_EOG_CH},
372+
{'kind': FIFF.FIFFV_EOG_CH}],
371373
)
372374
a = raw.copy().pick_channels(['EEG 001', 'EEG 002', 'EEG 003', 'EEG 004'])
373375
a = np.array([a._data[0, :] - a._data[1, :],

mne/preprocessing/ica.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -932,8 +932,8 @@ def _export_info(self, info, container, add_channels):
932932
ch_info.append(dict(
933933
ch_name=name, cal=1, logno=ii + 1,
934934
coil_type=FIFF.FIFFV_COIL_NONE, kind=FIFF.FIFFV_MISC_CH,
935-
coord_Frame=FIFF.FIFFV_COORD_UNKNOWN, unit=FIFF.FIFF_UNIT_NONE,
936-
loc=np.array([0., 0., 0., 1.] * 3, dtype='f4'),
935+
coord_frame=FIFF.FIFFV_COORD_UNKNOWN, unit=FIFF.FIFF_UNIT_NONE,
936+
loc=np.zeros(12, dtype='f4'),
937937
range=1.0, scanno=ii + 1, unit_mul=0))
938938

939939
if add_channels is not None:

0 commit comments

Comments
 (0)