Repository: mne-tools/mne-bids

Issue:
read_raw_bids() messes up the channel order and therefore loading raw fails
### Description of the problem

I can load my data using `mne.read_raw()` but not using `mne_bids.read_raw_bids()`. I get the following error:

```Python
ValueError: New channel names are not unique, renaming failed
```

My `electrodes.tsv` file seems correct. When inspecting the [code](https://github.com/mne-tools/mne-bids/blob/2de85360ff81e0e4287eeb52c583b1bcf443b59d/mne_bids/read.py#L645C1-L647C65), it turns out that the order of `ch_names_tsv` and `raw.ch_names` is messed up. Not sure why it only happens for one specific file.

If I change the lists using `sorted()`, it works fine. Should the code possibly be changed?

```Python
        for bids_ch_name, raw_ch_name in zip(sorted(ch_names_tsv),
                                             sorted(raw.ch_names.copy())):
            if bids_ch_name != raw_ch_name:
                raw.rename_channels({raw_ch_name: bids_ch_name})
```

Maybe it's the same issue as #1117 (I didn't look into the details)?

### Steps to reproduce

```Python
I can't share a replication since the patient data (even the metadata) are highly confidential.
```


### Expected results

I would expect `mne_bids.read_raw_bids()` to load the data.

### Actual results

```Python
Traceback (most recent call last):
  File "/Users/moritzgerster/Library/CloudStorage/Dropbox/Code/BIDS_LocalGlobal/scripts/_00_bidsify_sourcedata.py", line 600, in <module>
    bidsify_sourcedata()
  File "/Users/moritzgerster/Library/CloudStorage/Dropbox/Code/BIDS_LocalGlobal/scripts/_00_bidsify_sourcedata.py", line 28, in bidsify_sourcedata
    bidsify_sourcedata_neumann()
  File "/Users/moritzgerster/Library/CloudStorage/Dropbox/Code/BIDS_LocalGlobal/scripts/_00_bidsify_sourcedata.py", line 130, in bidsify_sourcedata_neumann
    raw = read_raw_bids(bids_path)
          ^^^^^^^^^^^^^^^^^^^^^^^^
  File "<decorator-gen-634>", line 12, in read_raw_bids
  File "/Users/moritzgerster/anaconda3/envs/localglobal/lib/python3.11/site-packages/mne_bids/read.py", line 780, in read_raw_bids
    raw = _handle_channels_reading(channels_fname, raw)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/moritzgerster/anaconda3/envs/localglobal/lib/python3.11/site-packages/mne_bids/read.py", line 606, in _handle_channels_reading
    raw.rename_channels({raw_ch_name: bids_ch_name})
  File "<decorator-gen-48>", line 12, in rename_channels
  File "/Users/moritzgerster/anaconda3/envs/localglobal/lib/python3.11/site-packages/mne/channels/channels.py", line 502, in rename_channels
    rename_channels(self.info, mapping, allow_duplicates)
  File "<decorator-gen-58>", line 12, in rename_channels
  File "/Users/moritzgerster/anaconda3/envs/localglobal/lib/python3.11/site-packages/mne/channels/channels.py", line 1290, in rename_channels
    raise ValueError("New channel names are not unique, renaming failed")
ValueError: New channel names are not unique, renaming failed
```

### Additional information

```
Platform             macOS-13.5.2-arm64-arm-64bit
Python               3.11.3 | packaged by conda-forge | (main, Apr  6 2023, 08:58:31) [Clang 14.0.6 ]
Executable           /Users/moritzgerster/anaconda3/envs/localglobal/bin/python
CPU                  arm (10 cores)
Memory               16.0 GB

Core
├☑ mne               1.4.0
├☑ numpy             1.23.4 (OpenBLAS 0.3.20 with 10 threads)
├☑ scipy             1.10.1
├☑ matplotlib        3.7.1 (backend=MacOSX)
├☑ pooch             1.7.0
└☑ jinja2            3.1.2

Numerical (optional)
├☑ sklearn           1.2.2
├☑ nibabel           5.0.1
├☑ pandas            2.0.1
└☐ unavailable       numba, nilearn, dipy, openmeeg, cupy
```


Discussion:
The BIDS names and channel order should take precedence, so renaming channels is the correct move.  Channel orders in the sidecars and raw file also are allowed to differ IIRC ("should" be the same).

Maybe we should adopt a 2-step procedure to avoid name collisions:

- Assign new unique names to all channels in raw. I'm thinking of something like a hash or random string. It must not be present in raw or the BIDS sidecars.
- Only now rename the channels according to BIDS.

This should prevent the problem you're describing here. 
Just to clarify: this issue should only come up if the channel order in channels.tsv differs from the channel order in the raw file.
I don't understand why any random names should be needed. Why not construct the full mapping from old names to new names and then call `raw.rename_channels`, then once that's done call `raw.reorder_channels` using the new names? To me it's a bit weird that `rename_channels` is called *inside* a for loop instead of constructing the mapping dict and calling `rename_channels` once.
Ok sounds good 

I don't see why we would need to `reorder` though?
> I don't see why we would need to reorder though?

I don't fully understand the use case or what MNE-BIDS does in all cases, I just saw "reorder" in the title and "channel order" in your comment so figured it was relevant / I should include for completeness
Ah ok! :)

MNE-BIDS is only supposed to apply the channel names in the order they appear in `channels.tsv`. There's no explicit "reorder" operation _per se_ (even though it could be implemented by swapping the names of two channels)

Resolving patch:
diff --git a/mne_bids/read.py b/mne_bids/read.py
--- a/mne_bids/read.py
+++ b/mne_bids/read.py
@@ -642,9 +642,7 @@ def _handle_channels_reading(channels_fname, raw):
             f"set channel names."
         )
     else:
-        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})
+        raw.rename_channels(dict(zip(raw.ch_names, ch_names_tsv)))
 
     # Set the channel types in the raw data according to channels.tsv
     channel_type_bids_mne_map_available_channels = {


Test patch:
diff --git a/mne_bids/tests/test_read.py b/mne_bids/tests/test_read.py
--- a/mne_bids/tests/test_read.py
+++ b/mne_bids/tests/test_read.py
@@ -1258,7 +1258,7 @@ def test_ignore_exclude_param(tmp_path):
 @pytest.mark.filterwarnings(warning_str["channel_unit_changed"])
 @testing.requires_testing_data
 def test_channels_tsv_raw_mismatch(tmp_path):
-    """Test behavior when channels.tsv contains channels not found in raw."""
+    """Test mismatch between channels in channels.tsv and raw."""
     bids_path = _bids_path.copy().update(root=tmp_path, datatype="meg", task="rest")
 
     # Remove one channel from the raw data without updating channels.tsv
@@ -1311,6 +1311,19 @@ def test_channels_tsv_raw_mismatch(tmp_path):
     ):
         read_raw_bids(bids_path)
 
+    # Test mismatched channel ordering between channels.tsv and raw
+    raw = _read_raw_fif(raw_fname, verbose=False)
+    write_raw_bids(raw, bids_path=bids_path, overwrite=True, verbose=False)
+
+    ch_names_orig = raw.ch_names.copy()
+    ch_names_new = ch_names_orig.copy()
+    ch_names_new[1], ch_names_new[0] = ch_names_new[0], ch_names_new[1]
+    raw.reorder_channels(ch_names_new)
+    raw.save(raw_path, overwrite=True)
+
+    raw = read_raw_bids(bids_path)
+    assert raw.ch_names == ch_names_orig
+
 
 @testing.requires_testing_data
 def test_file_not_found(tmp_path):