Repository: mne-tools/mne-bids

Issue:
ENH: fix manufacturer checking in _channels_tsv()
### Description of the problem

in `write_bids_raw`, `allow_preload=True` should allow writing EEG files from unsupported manufacturers / files formats (e.g. EGI .raw).
Yet, _channels_tsv() makes strong assumptions about extensions e.g. when looking up ignored channels.
Could it make sense to implement `IGNOREd_CHANNELS` as a default dict or only look that up if the extension is known?



### Steps to reproduce

```Python

import tempfile
import mne
from mne_bids import write_raw_bids, BIDSPath

fname = mne.datasets.testing.data_path() / 'montage' / 'egi_dig_test.raw'

raw = mne.io.read_raw_egi(fname)

with tempfile.TemporaryDirectory() as tmpdir:
    bp = BIDSPath(subject='1', root=tmpdir, datatype='eeg', task='foo')

    write_raw_bids(raw, bp, format='BrainVision', allow_preload=True)
```


### Expected results

converting to BrainVision from EGI .raw (once preloaded)

### Actual results

```
channels_tsv(raw, fname, overwrite)
    113 # get the manufacturer from the file in the Raw object
    114 _, ext = _parse_ext(raw.filenames[0])
--> 115 manufacturer = MANUFACTURERS[ext]
    117 ignored_channels = IGNORED_CHANNELS.get(manufacturer, list())
    119 status, ch_type, description = list(), list(), list()

KeyError: '.raw'
```

### Additional information


Platform:         Linux
Python:           3.8.13 | packaged by conda-forge | (default, Mar 25 2022, 06:04:10)  [GCC 10.3.0]
mne:              1.2.0
numpy:            1.23.4 {unknown linalg bindings (threadpoolctl module not found: No module named 'threadpoolctl')}
scipy:            1.9.1


Discussion:
I can actually add a reproducible example based on MNE testing files, will do in a bit 
We need to convert the format, can you try with passing the `format` parameter to `write_raw_bids()`?
@hoechenberger added reproducible example on top
Thanks!

Yeah we should clearly skip this check if the data has already been preloaded. That would be the solution to the entire problem, right?
That's also my understanding.
@dengemann In your example, you forgot to actually preload the data. Here's the "corrected" MWE (which still produces the same error):

```python

import tempfile
import mne
from mne_bids import write_raw_bids, BIDSPath

fname = mne.datasets.testing.data_path() / 'montage' / 'egi_dig_test.raw'

raw = mne.io.read_raw_egi(fname, preload=True)

with tempfile.TemporaryDirectory() as tmpdir:
    bp = BIDSPath(
        subject='1', task='foo', suffix='eeg', extension='.vhdr',
        datatype='eeg', root=tmpdir
    )
    write_raw_bids(raw, bp, format='BrainVision', allow_preload=True)
```

Produces
```python
Writing '/var/folders/2k/nkw3dkx97ssdv08xmdgfnjkc0000gn/T/tmprycdflf0/dataset_description.json'...
Writing '/var/folders/2k/nkw3dkx97ssdv08xmdgfnjkc0000gn/T/tmprycdflf0/sub-1/eeg/sub-1_task-foo_eeg.json'...
Traceback (most recent call last):
  File "/private/tmp/mwe-bids.py", line 15, in <module>
    write_raw_bids(raw, bp, format='BrainVision', allow_preload=True)
  File "<decorator-gen-620>", line 12, in write_raw_bids
  File "/Users/hoechenberger/Development/mne-bids/mne_bids/write.py", line 1861, in write_raw_bids
    _channels_tsv(raw, channels_path.fpath, overwrite)
  File "/Users/hoechenberger/Development/mne-bids/mne_bids/write.py", line 115, in _channels_tsv
    manufacturer = MANUFACTURERS[ext]
KeyError: '.raw'
```

We can deduce 2 things here:

1. The error message is not helpful **at all**
2. If the data has been preloaded, the original format shouldn't matter, but currently does



Resolving patch:
diff --git a/mne_bids/write.py b/mne_bids/write.py
--- a/mne_bids/write.py
+++ b/mne_bids/write.py
@@ -112,8 +112,7 @@ def _channels_tsv(raw, fname, overwrite=False):
 
     # get the manufacturer from the file in the Raw object
     _, ext = _parse_ext(raw.filenames[0])
-    manufacturer = MANUFACTURERS[ext]
-
+    manufacturer = MANUFACTURERS.get(ext, '')
     ignored_channels = IGNORED_CHANNELS.get(manufacturer, list())
 
     status, ch_type, description = list(), list(), list()
@@ -1542,8 +1541,10 @@ def write_raw_bids(
                          'got %s' % type(raw))
 
     if raw.preload is not False and not allow_preload:
-        raise ValueError('The data is already loaded from disk and may be '
-                         'altered. See warning for "allow_preload".')
+        raise ValueError(
+            'The data has already been loaded from disk. To write it to BIDS, '
+            'pass "allow_preload=True" and the "format" parameter.'
+        )
 
     if not isinstance(bids_path, BIDSPath):
         raise RuntimeError('"bids_path" must be a BIDSPath object. Please '
@@ -1609,7 +1610,12 @@ def write_raw_bids(
             ext = '.bdf'
 
         if ext not in ALLOWED_INPUT_EXTENSIONS:
-            raise ValueError(f'Unrecognized file format {ext}')
+            raise ValueError(
+                f'The input data is in a file format not supported by '
+                f'BIDS: "{ext}". You can try to preload the data and call '
+                f'write_raw_bids() with the "allow_preload=True" and the '
+                f'"format" parameters.'
+            )
 
         if symlink and ext != '.fif':
             raise NotImplementedError('Symlinks are currently only supported '


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
@@ -681,12 +681,6 @@ def test_fif(_bids_validate, tmp_path):
     for FILE in files:
         assert 'split' in FILE
 
-    # test unknown extension
-    raw = _read_raw_fif(raw_fname)
-    raw._filenames = (raw.filenames[0].replace('.fif', '.foo'),)
-    with pytest.raises(ValueError, match='Unrecognized file format'):
-        write_raw_bids(raw, bids_path)
-
     # test whether extra points in raw.info['dig'] are correctly used
     # to set DigitizedHeadShape in the JSON sidecar
     # unchanged sample data includes extra points
@@ -3948,3 +3942,26 @@ def test_events_data_deprecation(tmp_path):
             raw=raw, bids_path=bids_path, events=events, events_data=events,
             event_id=event_id
         )
+
+
+@testing.requires_testing_data
+def test_unknown_extension(_bids_validate, tmp_path):
+    """Write data with unknown extension to BIDS."""
+    bids_root = tmp_path / 'bids'
+    bids_path = _bids_path.copy().update(root=bids_root, datatype='meg')
+    raw_fname = data_path / 'MEG' / 'sample' / 'sample_audvis_trunc_raw.fif'
+
+    raw = _read_raw_fif(raw_fname)
+    raw._filenames = (raw.filenames[0].replace('.fif', '.foo'),)
+
+    # When data is not preloaded, we should raise an exception.
+    with pytest.raises(ValueError, match='file format not supported by BIDS'):
+        write_raw_bids(raw, bids_path)
+
+    # With preloaded data, writing should work.
+    raw._filenames = (raw.filenames[0].replace('.foo', '.fif'),)
+    raw.load_data()
+    raw._filenames = (raw.filenames[0].replace('.fif', '.foo'),)
+
+    write_raw_bids(raw, bids_path, allow_preload=True, format='FIF')
+    _bids_validate(bids_root)