Repository: mne-tools/mne-python

Issue:
read_dig_captrack ignores eeg ref
see this:
https://github.com/mne-tools/mne-python/blob/29b05f2f593f997168d2f29a41829f68f56ad5df/mne/channels/_dig_montage_utils.py#L185-L189


Discussion:

Resolving patch:
diff --git a/mne/channels/_dig_montage_utils.py b/mne/channels/_dig_montage_utils.py
--- a/mne/channels/_dig_montage_utils.py
+++ b/mne/channels/_dig_montage_utils.py
@@ -182,13 +182,6 @@ def _parse_brainvision_dig_montage(fname, scale=BACK_COMPAT):
     for s in sensors:
         name = s.find('Name').text
 
-        # Need to prune "GND" and "REF": these are not included in the raw
-        # data and will raise errors when we try to do raw.set_montage(...)
-        # XXX eventually this should be stored in ch['loc'][3:6]
-        # but we don't currently have such capabilities here
-        if name in ['GND', 'REF']:
-            continue
-
         is_fid = name in FID_NAME_MAP
         coordinates = np.array([float(s.find('X').text),
                                 float(s.find('Y').text),
diff --git a/mne/channels/montage.py b/mne/channels/montage.py
--- a/mne/channels/montage.py
+++ b/mne/channels/montage.py
@@ -1458,7 +1458,9 @@ def _backcompat_value(pos, ref_pos):
                 return np.concatenate((pos, ref_pos))
 
         ch_pos = _mnt._get_ch_pos()
-        eeg_ref_pos = ch_pos.pop('EEG000', np.zeros(3))
+        refs = set(ch_pos.keys()) & {'EEG000', 'REF'}
+        assert len(refs) <= 1
+        eeg_ref_pos = np.zeros(3) if not(refs) else ch_pos.pop(refs.pop())
 
         # This raises based on info being subset/superset of montage
         _pick_chs = partial(
@@ -1478,7 +1480,7 @@ def _backcompat_value(pos, ref_pos):
             _names = _mnt._get_dig_names()
             info['dig'] = _format_dig_points([
                 _mnt.dig[ii] for ii, name in enumerate(_names)
-                if name in matched_ch_names.union({None, 'EEG000'})
+                if name in matched_ch_names.union({None, 'EEG000', 'REF'})
             ])
 
         if _mnt.dev_head_t is not None:


Test patch:
diff --git a/mne/channels/tests/test_montage.py b/mne/channels/tests/test_montage.py
--- a/mne/channels/tests/test_montage.py
+++ b/mne/channels/tests/test_montage.py
@@ -938,6 +938,16 @@ def test_egi_dig_montage():
     assert object_diff(dig_montage.ch_names, dig_montage_egi.ch_names) == ''
 
 
+def _pop_montage(dig_montage, ch_name):
+    # remove reference that was not used in old API
+    ref_idx = dig_montage.ch_names.index(ch_name)
+    n_fids = 3
+    del dig_montage.dig[ref_idx + n_fids]
+    del dig_montage.ch_names[ref_idx]
+    for k in range(ref_idx + n_fids, len(dig_montage.dig)):
+        dig_montage.dig[k]['ident'] -= 1
+
+
 @testing.requires_testing_data
 def test_bvct_dig_montage_old_api():  # XXX: to remove in 0.20
     """Test BrainVision CapTrak XML dig montage support."""
@@ -947,6 +957,9 @@ def test_bvct_dig_montage_old_api():  # XXX: to remove in 0.20
     with pytest.deprecated_call():
         dig_montage = read_dig_montage(bvct=bvct_dig_montage_fname)
 
+    _pop_montage(dig_montage, 'REF')
+    _pop_montage(dig_montage, 'GND')
+
     # test round-trip IO
     temp_dir = _TempDir()
     fname_temp = op.join(temp_dir, 'bvct_test.fif')
@@ -972,7 +985,7 @@ def test_bvct_dig_montage_old_api():  # XXX: to remove in 0.20
                                    test_raw_bv.info['chs']):
         assert_equal(ch_raw['ch_name'], ch_test_raw['ch_name'])
         assert_equal(ch_raw['coord_frame'], FIFF.FIFFV_COORD_HEAD)
-        assert_allclose(ch_raw['loc'], ch_test_raw['loc'], atol=1e-7)
+        assert_allclose(ch_raw['loc'][:3], ch_test_raw['loc'][:3], atol=1e-7)
 
     assert_dig_allclose(raw_bv.info, test_raw_bv.info)
 
@@ -984,10 +997,10 @@ def test_read_dig_captrack(tmpdir):
         'AF3', 'AF4', 'AF7', 'AF8', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'CP1',
         'CP2', 'CP3', 'CP4', 'CP5', 'CP6', 'CPz', 'Cz', 'F1', 'F2', 'F3', 'F4',
         'F5', 'F6', 'F7', 'F8', 'FC1', 'FC2', 'FC3', 'FC4', 'FC5', 'FC6',
-        'FT10', 'FT7', 'FT8', 'FT9', 'Fp1', 'Fp2', 'Fz', 'O1', 'O2', 'Oz',
-        'P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7', 'P8', 'PO10', 'PO3', 'PO4',
-        'PO7', 'PO8', 'PO9', 'POz', 'Pz', 'T7', 'T8', 'TP10', 'TP7', 'TP8',
-        'TP9'
+        'FT10', 'FT7', 'FT8', 'FT9', 'Fp1', 'Fp2', 'Fz', 'GND', 'O1', 'O2',
+        'Oz', 'P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7', 'P8', 'PO10', 'PO3',
+        'PO4', 'PO7', 'PO8', 'PO9', 'POz', 'Pz', 'REF', 'T7', 'T8', 'TP10',
+        'TP7', 'TP8', 'TP9'
     ]
     montage = read_dig_captrack(
         fname=op.join(data_path, 'montage', 'captrak_coords.bvct')
@@ -996,7 +1009,7 @@ def test_read_dig_captrack(tmpdir):
     assert montage.ch_names == EXPECTED_CH_NAMES
     assert montage.__repr__() == (
         '<DigMontage | '
-        '0 extras (headshape), 0 HPIs, 3 fiducials, 64 channels>'
+        '0 extras (headshape), 0 HPIs, 3 fiducials, 66 channels>'
     )
 
     montage = transform_to_head(montage)  # transform_to_head has to be tested
@@ -1009,15 +1022,19 @@ def test_read_dig_captrack(tmpdir):
             atol=1e-5,
         )
 
-    # I think that comparing dig should be enough. cc: @sappelhoff
     raw_bv = read_raw_brainvision(bv_raw_fname)
+    raw_bv.set_channel_types({"HEOG": 'eog', "VEOG": 'eog', "ECG": 'ecg'})
 
-    with pytest.warns(RuntimeWarning, match='Did not set 3 channel positions'):
-        raw_bv.set_montage(montage, raise_if_subset=False)
+    raw_bv.set_montage(montage)
 
     test_raw_bv = read_raw_fif(bv_fif_fname)
 
-    assert_dig_allclose(raw_bv.info, test_raw_bv.info)
+    # compare after set_montage using chs loc.
+    for actual, expected in zip(raw_bv.info['chs'], test_raw_bv.info['chs']):
+        assert_allclose(actual['loc'][:3], expected['loc'][:3])
+        if actual['kind'] == FIFF.FIFFV_EEG_CH:
+            assert_allclose(actual['loc'][3:6],
+                            [-0.005103, 0.05395, 0.144622], rtol=1e-04)
 
 
 def test_set_montage():