Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BUG: Improve logic for bti #11102

Merged
merged 4 commits into from
Aug 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/changes/latest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ Bugs
- Fix bug in :class:`mne.viz.Brain` constructor where the first argument was named ``subject_id`` instead of ``subject`` (:gh:`11049` by `Eric Larson`_)
- Fix bug in :ref:`mne coreg` where the MEG helmet position was not updated during ICP fitting (:gh:`11084` by `Eric Larson`_)
- Document ``height`` and ``weight`` keys of ``subject_info`` entry in :class:`mne.Info` (:gh:`11019` by :newcontrib:`Sena Er`)
- Fixed bug in :func:`mne.viz.plot_filter` when plotting filters created using ``output='ba'`` mode with ``compensation`` turned on. (by `Marian Dovgialo`_)
- Fix bug in :func:`mne.viz.plot_filter` when plotting filters created using ``output='ba'`` mode with ``compensation`` turned on. (:gh:`11040` by `Marian Dovgialo`_)
- Fix bug in :func:`mne.io.read_raw_bti` where EEG, EMG, and H/VEOG channels were not detected properly, and many non-ECG channels were called ECG. The logic has been improved, and any channels of unknown type are now labeled as ``misc`` (:gh:`11102` by `Eric Larson`_)
- Fix bug in :func:`mne.viz.plot_topomap` when providing ``sphere="eeglab"`` (:gh:`11081` by `Mathieu Scheltienne`_)
- Applying a montage where EEG locations are not in head space (or unknown space) without fiducials will now raise an error message. (:gh:`11080` by `Marijn van Vliet`_)

Expand Down
37 changes: 29 additions & 8 deletions mne/io/bti/bti.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#
# simplified BSD-3 license

import functools
import os.path as op
from io import BytesIO
from itertools import count
Expand Down Expand Up @@ -48,7 +49,7 @@ def _instantiate_default_info_chs():
unit=FIFF.FIFF_UNIT_V,
cal=1.0,
scanno=None,
kind=FIFF.FIFFV_ECG_CH,
kind=FIFF.FIFFV_MISC_CH,
logno=None)


Expand Down Expand Up @@ -997,6 +998,23 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult):
_mult_cal_one(data_view, one, idx, cals, mult)


@functools.lru_cache(1)
def _1020_names():
from mne.channels import make_standard_montage
return set(ch_name.lower()
for ch_name in make_standard_montage('standard_1005').ch_names)


def _eeg_like(ch_name):
# Some bti recordigs look like "F4-POz", so let's at least mark them
# as EEG
if ch_name.count('-') != 1:
return
ch, ref = ch_name.split('-')
eeg_names = _1020_names()
return ch.lower() in eeg_names and ref.lower() in eeg_names


def _make_bti_digitization(
info, head_shape_fname, convert, use_hpi, bti_dev_t, dev_ctf_t):
with info._unlock():
Expand Down Expand Up @@ -1184,7 +1202,7 @@ def _get_bti_info(pdf_fname, config_fname, head_shape_fname, rotation_x,
chan_info['coil_type'] = \
FIFF.FIFFV_COIL_MAGNES_OFFDIAG_REF_GRAD

elif chan_4d.startswith('EEG'):
elif chan_4d.startswith('EEG') or _eeg_like(chan_4d):
chan_info['kind'] = FIFF.FIFFV_EEG_CH
chan_info['coil_type'] = FIFF.FIFFV_COIL_EEG
chan_info['coord_frame'] = eeg_frame
Expand All @@ -1194,14 +1212,17 @@ def _get_bti_info(pdf_fname, config_fname, head_shape_fname, rotation_x,
chan_info['kind'] = FIFF.FIFFV_STIM_CH
elif chan_4d == 'TRIGGER':
chan_info['kind'] = FIFF.FIFFV_STIM_CH
elif chan_4d.startswith('EOG') or chan_4d in eog_ch:
elif chan_4d.startswith('EOG') or \
chan_4d[:4] in ('HEOG', 'VEOG') or chan_4d in eog_ch:
chan_info['kind'] = FIFF.FIFFV_EOG_CH
elif chan_4d == ecg_ch:
elif chan_4d.startswith('EMG'):
chan_info['kind'] = FIFF.FIFFV_EMG_CH
elif chan_4d == ecg_ch or chan_4d.startswith('ECG'):
chan_info['kind'] = FIFF.FIFFV_ECG_CH
elif chan_4d.startswith('X'):
chan_info['kind'] = FIFF.FIFFV_MISC_CH
elif chan_4d == 'UACurrent':
chan_info['kind'] = FIFF.FIFFV_MISC_CH
# Our default is now misc, but if we ever change that,
# we'll need this:
# elif chan_4d.startswith('X') or chan_4d == 'UACurrent':
# chan_info['kind'] = FIFF.FIFFV_MISC_CH

chs.append(chan_info)

Expand Down
69 changes: 63 additions & 6 deletions mne/io/bti/tests/test_bti.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#
# License: BSD-3-Clause

from collections import Counter
from io import BytesIO
import os
import os.path as op
Expand Down Expand Up @@ -38,12 +39,10 @@
for a in archs]
tmp_raw_fname = op.join(base_dir, 'tmp_raw.fif')

fname_2500 = op.join(testing.data_path(download=False), 'BTi', 'erm_HFH',
'c,rfDC')
fname_sim = op.join(testing.data_path(download=False), 'BTi', '4Dsim',
'c,rfDC')
fname_sim_filt = op.join(testing.data_path(download=False), 'BTi', '4Dsim',
'c,rfDC,fn50,o')
testing_path_bti = testing.data_path(download=False) / 'BTi'
fname_2500 = testing_path_bti / 'erm_HFH' / 'c,rfDC'
fname_sim = testing_path_bti / '4Dsim' / 'c,rfDC'
fname_sim_filt = testing_path_bti / '4Dsim' / 'c,rfDC,fn50,o'

# the 4D exporter doesn't export all channels, so we confine our comparison
NCH = 248
Expand Down Expand Up @@ -383,3 +382,61 @@ def test_bti_set_eog():
preload=False,
eog_ch=('X65', 'X67', 'X69', 'X66', 'X68'))
assert_equal(len(pick_types(raw.info, eog=True)), 5)


@testing.requires_testing_data
def test_bti_ecg_eog_emg(monkeypatch):
"""Test that EOG/ECG/EMG are set properly in BTi."""
kwargs = dict(rename_channels=False, head_shape_fname=None)
raw = read_raw_bti(fname_2500, **kwargs)
ch_types = raw.get_channel_types()
got = Counter(ch_types)
# Before improving the triaging in gh-, these values were:
# want = dict(mag=148, ref_meg=11, ecg=32, stim=2, misc=1)
want = dict(mag=148, ref_meg=11, ecg=1, stim=2, misc=1, eeg=31)
assert set(want) == set(got)
for key in want:
assert want[key] == got[key], key

# replace channel names with some from HCP (starting from the end)
# not including UACurrent (misc) or TRIGGER/RESPONSE (stim) b/c they
# already exist
got_map = dict(zip(raw.ch_names, ch_types))
kind_map = dict(
stim=['TRIGGER', 'RESPONSE'],
misc=['UACurrent'],
)
for kind, ch_names in kind_map.items():
for ch_name in ch_names:
assert got_map[ch_name] == kind
kind_map = dict(
misc=['SA1', 'SA2', 'SA3'],
ecg=['ECG+', 'ECG-'],
eog=['VEOG+', 'HEOG+', 'VEOG-', 'HEOG-'],
emg=['EMG_LF', 'EMG_LH', 'EMG_RF', 'EMG_RH'],
)
new_names = sum(kind_map.values(), list())
assert len(new_names) == 13
assert set(new_names).intersection(set(raw.ch_names)) == set()

def _read_bti_header_2(*args, **kwargs):
bti_info = _read_bti_header(*args, **kwargs)
for ch_name, ch in zip(new_names, bti_info['chs'][::-1]):
ch['chan_label'] = ch_name
return bti_info

monkeypatch.setattr(mne.io.bti.bti, '_read_bti_header', _read_bti_header_2)
raw = read_raw_bti(fname_2500, **kwargs)
got_map = dict(zip(raw.ch_names, raw.get_channel_types()))
got = Counter(got_map.values())
want = dict(mag=148, ref_meg=11, misc=1, stim=2, eeg=19)
for kind, ch_names in kind_map.items():
want[kind] = want.get(kind, 0) + len(ch_names)
assert set(want) == set(got)
for key in want:
assert want[key] == got[key], key
for kind, ch_names in kind_map.items():
for ch_name in ch_names:
assert ch_name in raw.ch_names
err_msg = f'{ch_name} type {got_map[ch_name]} !+ {kind}'
assert got_map[ch_name] == kind, err_msg