Repository: mne-tools/mne-bids

Issue:
Don't silently apply the BIDS datatype's channel type to channels for which we don't have a proper BIDS -> MNE mapping
_Originally posted by @hoechenberger in https://github.com/mne-tools/mne-bids/issues/1046#issuecomment-1210231693_

We currently use the following channel type mapping from BIDS to MNE:
https://github.com/mne-tools/mne-bids/blob/16a7b0c1a4348256f6781f539cd82c0acf444de3/mne_bids/utils.py#L77-L83

Apparently, we silently apply the BIDS datatype's channel type to channels for which we don't have a proper BIDS -> MNE mapping defined here. I don't think we should do this. Instead, we should default to `misc`.


Discussion:
agreed

Resolving patch:
diff --git a/mne_bids/read.py b/mne_bids/read.py
--- a/mne_bids/read.py
+++ b/mne_bids/read.py
@@ -507,12 +507,19 @@ def _handle_channels_reading(channels_fname, raw):
     # Now we can do some work.
     # The "type" column is mandatory in BIDS. We can use it to set channel
     # types in the raw data using a mapping between channel types
-    channel_type_dict = dict()
+    channel_type_bids_mne_map = dict()
 
     # Get the best mapping we currently have from BIDS to MNE nomenclature
     bids_to_mne_ch_types = _get_ch_type_mapping(fro='bids', to='mne')
     ch_types_json = channels_dict['type']
     for ch_name, ch_type in zip(ch_names_tsv, ch_types_json):
+        # We don't map MEG channels for now, as there's no clear 1:1 mapping
+        # from BIDS to MNE coil types.
+        if ch_type.upper() in (
+            'MEGGRADAXIAL', 'MEGMAG', 'MEGREFGRADAXIAL', 'MEGGRADPLANAR',
+            'MEGREFMAG', 'MEGOTHER'
+        ):
+            continue
 
         # Try to map from BIDS nomenclature to MNE, leave channel type
         # untouched if we are uncertain
@@ -530,8 +537,16 @@ def _handle_channels_reading(channels_fname, raw):
                        'will raise an error in the future.')
                 warn(msg)
 
-        if updated_ch_type is not None:
-            channel_type_dict[ch_name] = updated_ch_type
+        if updated_ch_type is None:
+            # We don't have an appropriate mapping, so make it a "misc" channel
+            channel_type_bids_mne_map[ch_name] = 'misc'
+            warn(
+                f'No BIDS -> MNE mapping found for channel type "{ch_type}". '
+                f'Type of channel "{ch_name}" will be set to "misc".'
+            )
+        else:
+            # We found a mapping, so use it
+            channel_type_bids_mne_map[ch_name] = updated_ch_type
 
     # Special handling for (synthesized) stimulus channel
     synthesized_stim_ch_name = 'STI 014'
@@ -556,16 +571,19 @@ def _handle_channels_reading(channels_fname, raw):
                 raw.rename_channels({raw_ch_name: bids_ch_name})
 
     # Set the channel types in the raw data according to channels.tsv
-    ch_type_map_avail = {
+    channel_type_bids_mne_map_available_channels = {
         ch_name: ch_type
-        for ch_name, ch_type in channel_type_dict.items()
+        for ch_name, ch_type in channel_type_bids_mne_map.items()
         if ch_name in raw.ch_names
     }
-    ch_diff = set(channel_type_dict.keys()) - set(ch_type_map_avail.keys())
+    ch_diff = (
+        set(channel_type_bids_mne_map.keys()) -
+        set(channel_type_bids_mne_map_available_channels.keys())
+    )
     if ch_diff:
         warn(f'Cannot set channel type for the following channels, as they '
              f'are missing in the raw data: {", ".join(sorted(ch_diff))}')
-    raw.set_channel_types(ch_type_map_avail)
+    raw.set_channel_types(channel_type_bids_mne_map_available_channels)
 
     # Set bad channels based on _channels.tsv sidecar
     if 'status' in channels_dict:
diff --git a/mne_bids/utils.py b/mne_bids/utils.py
--- a/mne_bids/utils.py
+++ b/mne_bids/utils.py
@@ -67,6 +67,7 @@ def _get_ch_type_mapping(fro='mne', to='bids'):
         mapping = dict(eeg='EEG', misc='MISC', stim='TRIG', emg='EMG',
                        ecog='ECOG', seeg='SEEG', eog='EOG', ecg='ECG',
                        resp='RESP', bio='MISC', dbs='DBS',
+                       # NIRS
                        fnirs_cw_amplitude='NIRSCWAMPLITUDE',
                        # MEG channels
                        meggradaxial='MEGGRADAXIAL', megmag='MEGMAG',
@@ -77,8 +78,11 @@ def _get_ch_type_mapping(fro='mne', to='bids'):
     elif fro == 'bids' and to == 'mne':
         mapping = dict(EEG='eeg', MISC='misc', TRIG='stim', EMG='emg',
                        ECOG='ecog', SEEG='seeg', EOG='eog', ECG='ecg',
-                       RESP='resp', NIRS='fnirs_cw_amplitude',
-                       # No MEG channels for now
+                       RESP='resp',
+                       # NIRS
+                       NIRSCWAMPLITUDE='fnirs_cw_amplitude',
+                       NIRS='fnirs_cw_amplitude',
+                       # No MEG channels for now (see Notes above)
                        # Many to one mapping
                        VEOG='eog', HEOG='eog', DBS='dbs')
     else:


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
@@ -1069,6 +1069,36 @@ def test_handle_channel_type_casing(tmp_path):
         read_raw_bids(bids_path)
 
 
+@pytest.mark.filterwarnings(warning_str['channel_unit_changed'])
+def test_handle_non_mne_channel_type(tmp_path):
+    """Test that channel types not known to MNE will be read as 'misc'."""
+    bids_path = _bids_path.copy().update(root=tmp_path)
+    raw = _read_raw_fif(raw_fname, verbose=False)
+
+    write_raw_bids(raw, bids_path, overwrite=True,
+                   verbose=False)
+
+    channels_tsv_path = bids_path.copy().update(
+        root=tmp_path,
+        datatype='meg',
+        suffix='channels',
+        extension='.tsv'
+    ).fpath
+
+    channels_data = _from_tsv(channels_tsv_path)
+    # Violates BIDS, but ensures we won't have an appropriate
+    # BIDS -> MNE mapping.
+    ch_idx = -1
+    channels_data['type'][ch_idx] = 'FOOBAR'
+    _to_tsv(data=channels_data, fname=channels_tsv_path)
+
+    with pytest.warns(RuntimeWarning, match='will be set to \"misc\"'):
+        raw = read_raw_bids(bids_path)
+
+    # Should be a 'misc' channel.
+    assert raw.get_channel_types([channels_data['name'][ch_idx]]) == ['misc']
+
+
 @pytest.mark.filterwarnings(warning_str['channel_unit_changed'])
 def test_bads_reading(tmp_path):
     bids_path = _bids_path.copy().update(root=tmp_path, datatype='meg')