Repository: mne-tools/mne-python

Issue:
All EEG and EOG channels have type 'ecg' when reading HCP data using read_raw_bti()
This bug report is based on [this discussion](https://mne.discourse.group/t/why-does-mne-minimum-norm-apply-inverse-raw-call-check-reference-when-using-only-meg-channels/5321) in the MNE group.

I am using MNE and MNE-HCP to analyze HCP data. When I use the following function to read the HCP data, all ECG, EOG, and EEG channels have type 'ecg':

```
files = hcp.file_mapping.get_file_paths(subject=subj, data_type=datatype,
                                        output='raw', hcp_path=hcp_dir, run_index=run_index)
# read the raw data
raw = mne.io.read_raw_bti(files[0], head_shape_fname=None, convert=False, preload=True)
```

The same also happens when I simply use hcp.read_raw(). Here are the info files for the two variants.

mne.io.read_raw_bti:

> <Info | 8 non-empty values
>  bads: []
>  ch_names: MEG 001, MEG 002, MEG 003, MEG 004, MEG 005, MEG 006, MEG 007, ...
>  chs: 248 Magnetometers, 2 Stimulus, 23 Reference Magnetometers, 13 ECG, 1 misc
>  custom_ref_applied: False
>  dev_ctf_t: CTF MEG device -> CTF/4D/KIT head transform
>  highpass: 0.0 Hz
>  lowpass: 1017.3 Hz
>  meas_date: 2014-05-21 14:38:37 UTC
>  nchan: 287
>  projs: []
>  sfreq: 2034.5 Hz
> >

hcp.read_raw:

> <Info | 8 non-empty values
>  bads: []
>  ch_names: TRIGGER, RESPONSE, MLzA, MLyA, MLzaA, MLyaA, MLxA, A22, MLxaA, ...
>  chs: 2 Stimulus, 23 Reference Magnetometers, 248 Magnetometers, 13 ECG, 1 misc
>  custom_ref_applied: False
>  dev_ctf_t: CTF MEG device -> CTF/4D/KIT head transform
>  highpass: 0.0 Hz
>  lowpass: 1017.3 Hz
>  meas_date: 2014-05-21 14:38:37 UTC
>  nchan: 287
>  projs: []
>  sfreq: 2034.5 Hz
> >

Furthermore, when using mne.io.read_raw_bti(), the ECG+ and ECG- channels are not named correctly anymore. I am currently using these functions as a workaround:
```

def _set_custom_ref_to_off(raw):

    from mne.io.constants import FIFF
    with raw.info._unlock():
        raw.info['custom_ref_applied'] = FIFF.FIFFV_MNE_CUSTOM_REF_OFF


def set_ecg_eog_channels(raw):
    """
    Set correct channel type for EEG/ECG/EOG channels and re-reference
    ECG, VEOG, and HEOG channels using a bipolar referencing scheme.

    Operates in place.
    """
    # somehow all channels have type ECG -> set to EEG first
    ecg_picks = mne.pick_types(raw.info, ecg=True)
    for pick in ecg_picks:
        ch_name = raw.info['ch_names'][pick]
        raw.set_channel_types({ch_name: 'eeg'})

    # set correct channel names for ECG
    ecg_ch_mapping = {'EEG 001': 'ECG+', 'EEG 004': 'ECG-'}
    mne.channels.rename_channels(raw.info, ecg_ch_mapping)

    # combine ECG+/-, VEOG+/-, and HEOG+/- into three channels
    # and set appropriate channel types
    hcp.preprocessing.set_eog_ecg_channels(raw)

    # hcp.preprocessing.set_eog_ecg_channels() results in flag custom_ref_applied
    # being set to True -> set back to False to avoid errors
    # when calling mne.minimum_norm.apply_inverse_raw()
    _set_custom_ref_to_off(raw)

```

Versions:
MNE-HCP: 0.1.dev12
MNE: 1.03


Discussion:
@ckiefer0 can you share a problematic file with me?

Resolving patch:
diff --git a/mne/io/bti/bti.py b/mne/io/bti/bti.py
--- a/mne/io/bti/bti.py
+++ b/mne/io/bti/bti.py
@@ -8,6 +8,7 @@
 #
 #          simplified BSD-3 license
 
+import functools
 import os.path as op
 from io import BytesIO
 from itertools import count
@@ -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)
 
 
@@ -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():
@@ -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
@@ -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)
 


Test patch:
diff --git a/mne/io/bti/tests/test_bti.py b/mne/io/bti/tests/test_bti.py
--- a/mne/io/bti/tests/test_bti.py
+++ b/mne/io/bti/tests/test_bti.py
@@ -2,6 +2,7 @@
 #
 # License: BSD-3-Clause
 
+from collections import Counter
 from io import BytesIO
 import os
 import os.path as op
@@ -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
@@ -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