Repository: mne-tools/mne-bids

Issue:
write_raw_bids() creates BIDS datasets that read_raw_bids() refuses to read if channels were renamed before writing
### Problem description
@MaelysSolal ran into this.

Yesterday, we converted a dataset to BIDS after renaming some channels.. Today, the same data cannot be read.


### Reproduction
MWE:

```python
import mne
from mne.datasets import eegbci

from mne_bids import read_raw_bids, write_raw_bids, BIDSPath


subject = '001'
task = 'RestEyesClosed'
bids_root = '/tmp/bci-bids'
bids_path = BIDSPath(subject=subject, task=task, root=bids_root)

raw_fname = eegbci.load_data(subject=1, runs=2, update_path=True)[0]
raw = mne.io.read_raw_edf(raw_fname)
raw.info['line_freq'] = 50

raw.rename_channels({'Fc5.': 'FC5'})  # <--- !!!

write_raw_bids(raw=raw, bids_path=bids_path, overwrite=True)
read_raw_bids(bids_path=bids_path)
```
Output:

> RuntimeError                              Traceback (most recent call last)
> /private/tmp/mwe-rename-chs.py in 
>      18 
>      19 write_raw_bids(raw=raw, bids_path=bids_path, overwrite=True)
> ---> 20 read_raw_bids(bids_path=bids_path)
> 
> ~/Development/mne-bids/mne_bids/read.py in read_raw_bids(bids_path, extra_params, verbose)
>     515                                             on_error='warn')
>     516     if channels_fname is not None:
> --> 517         raw = _handle_channels_reading(channels_fname, bids_fname, raw)
>     518 
>     519     # Try to find an associated electrodes.tsv and coordsystem.json
> 
> ~/Development/mne-bids/mne_bids/read.py in _handle_channels_reading(channels_fname, bids_fname, raw)
>     377         if not (ch_names_raw[-1] == 'STI 014' and
>     378                 ch_names_raw[:-1] == ch_names_tsv):
> --> 379             raise RuntimeError(msg)
>     380 
>     381     # Now we can do some work.
> 
> RuntimeError: Channels do not correspond between raw data and the channels.tsv file. For MNE-BIDS, the channel names in the tsv MUST be equal and in the same order as the channels in the raw data.
> 
> 64 channels in tsv file: "/tmp/bci-bids/sub-001/eeg/sub-001_task-RestEyesClosed_channels.tsv"
> 
>  --> ['FC5', 'Fc3.', 'Fc1.', 'Fcz.', 'Fc2.', 'Fc4.', 'Fc6.', 'C5..', 'C3..', 'C1..', 'Cz..', 'C2..', 'C4..', 'C6..', 'Cp5.', 'Cp3.', 'Cp1.', 'Cpz.', 'Cp2.', 'Cp4.', 'Cp6.', 'Fp1.', 'Fpz.', 'Fp2.', 'Af7.', 'Af3.', 'Afz.', 'Af4.', 'Af8.', 'F7..', 'F5..', 'F3..', 'F1..', 'Fz..', 'F2..', 'F4..', 'F6..', 'F8..', 'Ft7.', 'Ft8.', 'T7..', 'T8..', 'T9..', 'T10.', 'Tp7.', 'Tp8.', 'P7..', 'P5..', 'P3..', 'P1..', 'Pz..', 'P2..', 'P4..', 'P6..', 'P8..', 'Po7.', 'Po3.', 'Poz.', 'Po4.', 'Po8.', 'O1..', 'Oz..', 'O2..', 'Iz..']
> 
> 64 channels in raw file: "sub-001_task-RestEyesClosed_eeg.edf"
> 
>  --> ['Fc5.', 'Fc3.', 'Fc1.', 'Fcz.', 'Fc2.', 'Fc4.', 'Fc6.', 'C5..', 'C3..', 'C1..', 'Cz..', 'C2..', 'C4..', 'C6..', 'Cp5.', 'Cp3.', 'Cp1.', 'Cpz.', 'Cp2.', 'Cp4.', 'Cp6.', 'Fp1.', 'Fpz.', 'Fp2.', 'Af7.', 'Af3.', 'Afz.', 'Af4.', 'Af8.', 'F7..', 'F5..', 'F3..', 'F1..', 'Fz..', 'F2..', 'F4..', 'F6..', 'F8..', 'Ft7.', 'Ft8.', 'T7..', 'T8..', 'T9..', 'T10.', 'Tp7.', 'Tp8.', 'P7..', 'P5..', 'P3..', 'P1..', 'Pz..', 'P2..', 'P4..', 'P6..', 'P8..', 'Po7.', 'Po3.', 'Poz.', 'Po4.', 'Po8.', 'O1..', 'Oz..', 'O2..', 'Iz..']

### Workaround
In this specific case, enforcing a format conversion to BrainVision works:
```python
write_raw_bids(raw=raw, bids_path=bids_path, overwrite=True,
               format='BrainVision')
read_raw_bids(bids_path=bids_path)
```
Output:
```
<RawBrainVision | sub-001_task-RestEyesClosed_eeg.eeg, 64 x 9760 (61.0 s), ~68 kB, data not loaded>
```

### Expected behavior
Either we should fail early (during writing) or not at all. (I'm all for the latter: it should just work)

I think renaming (sanitizing, in our case) channels before writing the BIDS data is a totally sane use case and should be properly supported.

Since we've started moving away from the "sidecar metadata must match raw header metadata" paradigm, we probably can simply remove the check for matching channel names between the two, which is causing the exception here?

cc @sappelhoff @agramfort @jasmainak @adam2392 


Discussion:
as discussed in the past the sidecar files are the truth for BIDS. We should overwrite the channel names on read if they are different so we can keep sharing the raw data really as raw without having to convert them

my 2c

Resolving patch:
diff --git a/mne_bids/read.py b/mne_bids/read.py
--- a/mne_bids/read.py
+++ b/mne_bids/read.py
@@ -346,37 +346,14 @@ def _get_bads_from_tsv_data(tsv_data):
     return bads
 
 
-def _handle_channels_reading(channels_fname, bids_fname, raw):
+def _handle_channels_reading(channels_fname, raw):
     """Read associated channels.tsv and populate raw.
 
     Updates status (bad) and types of channels.
     """
     logger.info('Reading channel info from {}.'.format(channels_fname))
     channels_dict = _from_tsv(channels_fname)
-
-    # First, make sure that ordering of names in channels.tsv matches the
-    # ordering of names in the raw data. The "name" column is mandatory in BIDS
-    ch_names_raw = list(raw.ch_names)
     ch_names_tsv = channels_dict['name']
-    if ch_names_raw != ch_names_tsv:
-
-        msg = ('Channels do not correspond between raw data and the '
-               'channels.tsv file. For MNE-BIDS, the channel names in the '
-               'tsv MUST be equal and in the same order as the channels in '
-               'the raw data.\n\n'
-               '{} channels in tsv file: "{}"\n\n --> {}\n\n'
-               '{} channels in raw file: "{}"\n\n --> {}\n\n'
-               .format(len(ch_names_tsv), channels_fname, ch_names_tsv,
-                       len(ch_names_raw), bids_fname, ch_names_raw)
-               )
-
-        # XXX: this could be due to MNE inserting a 'STI 014' channel as the
-        # last channel: In that case, we can work. --> Can be removed soon,
-        # because MNE will stop the synthesis of stim channels in the near
-        # future
-        if not (ch_names_raw[-1] == 'STI 014' and
-                ch_names_raw[:-1] == ch_names_tsv):
-            raise RuntimeError(msg)
 
     # Now we can do some work.
     # The "type" column is mandatory in BIDS. We can use it to set channel
@@ -407,6 +384,11 @@ def _handle_channels_reading(channels_fname, bids_fname, raw):
         if updated_ch_type is not None:
             channel_type_dict[ch_name] = updated_ch_type
 
+    # Rename channels in loaded Raw to match those read from the BIDS sidecar
+    for bids_ch_name, raw_ch_name in zip(ch_names_tsv, raw.ch_names.copy()):
+        if bids_ch_name != raw_ch_name:
+            raw.rename_channels({raw_ch_name: bids_ch_name})
+
     # Set the channel types in the raw data according to channels.tsv
     raw.set_channel_types(channel_type_dict)
 
@@ -514,7 +496,7 @@ def read_raw_bids(bids_path, extra_params=None, verbose=True):
                                             extension='.tsv',
                                             on_error='warn')
     if channels_fname is not None:
-        raw = _handle_channels_reading(channels_fname, bids_fname, raw)
+        raw = _handle_channels_reading(channels_fname, raw)
 
     # Try to find an associated electrodes.tsv and coordsystem.json
     # to get information about the status and type of present channels


Test patch:
diff --git a/mne_bids/tests/test_write.py b/mne_bids/tests/test_write.py
--- a/mne_bids/tests/test_write.py
+++ b/mne_bids/tests/test_write.py
@@ -1000,9 +1000,9 @@ def test_eegieeg(dir_name, fname, reader, _bids_validate):
     del raw2, events2
 
     # alter some channels manually
-    raw.rename_channels({raw.info['ch_names'][0]: 'EOGtest'})
+    raw.rename_channels({raw.ch_names[0]: 'EOGtest'})
     raw.info['chs'][0]['coil_type'] = FIFF.FIFFV_COIL_EEG_BIPOLAR
-    raw.rename_channels({raw.info['ch_names'][1]: 'EMG'})
+    raw.rename_channels({raw.ch_names[1]: 'EMG'})
     raw.set_channel_types({'EMG': 'emg'})
 
     # Test we can overwrite dataset_description.json
@@ -1045,15 +1045,12 @@ def test_eegieeg(dir_name, fname, reader, _bids_validate):
             ["Please cite MNE-BIDS in your publication before removing this "
              "(citations in README)"]
 
-    # Reading the file back should raise an error, because we renamed channels
-    # in `raw` and used that information to write a channels.tsv. Yet, we
-    # saved the unchanged `raw` in the BIDS folder, so channels in the TSV and
-    # in raw clash
-    # Note: only needed for data files that store channel names
-    # alongside the data
-    if dir_name == 'EDF':
-        with pytest.raises(RuntimeError, match='Channels do not correspond'):
-            read_raw_bids(bids_path=bids_path)
+    # Reading the file back should still work, even though we've renamed
+    # some channels (there's now a mismatch between BIDS and Raw channel
+    # names, and BIDS should take precedence)
+    raw_read = read_raw_bids(bids_path=bids_path)
+    assert raw_read.ch_names[0] == 'EOGtest'
+    assert raw_read.ch_names[1] == 'EMG'
 
     with pytest.raises(TypeError, match="unexpected keyword argument 'foo'"):
         read_raw_bids(bids_path=bids_path, extra_params=dict(foo='bar'))