Repository: mne-tools/mne-bids

Issue:
Supporting CNTs (Neuroscan) files
#### Describe the problem

Hi mne-bids team. I was wondering if there was any reason for not supporting CNTs. I have run into some problems when using ``write_raw_bids`` and I noticed it was not on the list of eeg extensions:

https://github.com/mne-tools/mne-bids/blob/4c0755c6eb567c7840d70acf06f22201c5e75c65/mne_bids/config.py#L49-L55

Is there a reason for not supporting it beyond not being included on this dictionary?


Discussion:
Hello! 👋 Thanks for opening your first issue here! ❤️ We will try to get back to you soon. 🚴🏽‍♂️

.cnt is not a format parf of the BIDS standard

https://bids-specification.readthedocs.io/en/bep-009/04-modality-specific-files/03-electroencephalography.html

Thanks @agramfort , I meant as an input format (that is, I want to convert from cnt to any of the accepted bids formats).

Right now I think it is giving an error when it extracts the .cnt extesion and is not able to pass the ALLOWED_INPUT_EXTENSIONS check:

https://github.com/mne-tools/mne-bids/blob/4c0755c6eb567c7840d70acf06f22201c5e75c65/mne_bids/write.py#L1365-L1383

Maybe I'm doing something wrong, Im just opening the file with the ``read_raw`` mne function and passing the Raw object to the  ``write_raw_bids`` function .
Here is a colab with the replication of the error I get:

https://colab.research.google.com/drive/1cKaSnQglJwdLL-isNBlN6z5p_LixvkJN?usp=sharing
@sappelhoff WDYT?
Hi @yjmantilla :)

I think if we are supporting Nihon Kohden and Persyst, we might as well support CNT ... as long as all of these non-supported file formats get auto-converted into a BIDS supported format. Could be an added feature in a PR?
sounds like a plan !

>

@sappelhoff Thanks!

I could test what happens if one naively just adds the .cnt format to the MANUFACTURERS dictionary, and start from there what would be required to accept support cnt .

Resolving patch:
diff --git a/mne_bids/config.py b/mne_bids/config.py
--- a/mne_bids/config.py
+++ b/mne_bids/config.py
@@ -51,7 +51,8 @@
                      '.BDF': 'Biosemi',
                      '.set': 'n/a', '.fdt': 'n/a',
                      '.lay': 'Persyst', '.dat': 'Persyst',
-                     '.EEG': 'Nihon Kohden'}
+                     '.EEG': 'Nihon Kohden',
+                     '.cnt': 'Neuroscan', '.CNT': 'Neuroscan'}
 
 ieeg_manufacturers = {'.vhdr': 'BrainProducts', '.eeg': 'BrainProducts',
                       '.edf': 'n/a', '.EDF': 'n/a', '.set': 'n/a',
@@ -66,7 +67,8 @@
           '.edf': io.read_raw_edf, '.EDF': io.read_raw_edf,
           '.bdf': io.read_raw_bdf,
           '.set': io.read_raw_eeglab, '.lay': io.read_raw_persyst,
-          '.EEG': io.read_raw_nihon}
+          '.EEG': io.read_raw_nihon,
+          '.cnt': io.read_raw_cnt, '.CNT': io.read_raw_cnt}
 
 
 # Merge the manufacturer dictionaries in a python2 / python3 compatible way
@@ -109,7 +111,7 @@
 # recommended formats
 ALLOWED_INPUT_EXTENSIONS = \
     allowed_extensions_meg + allowed_extensions_eeg + \
-    allowed_extensions_ieeg + ['.lay', '.EEG']
+    allowed_extensions_ieeg + ['.lay', '.EEG', '.cnt', '.CNT']
 
 # allowed suffixes (i.e. last "_" delimiter in the BIDS filenames before
 # the extension)
diff --git a/mne_bids/inspect.py b/mne_bids/inspect.py
--- a/mne_bids/inspect.py
+++ b/mne_bids/inspect.py
@@ -13,7 +13,7 @@
 from mne.utils import logger, verbose
 from mne.fixes import _compare_version
 
-if _compare_version(mne.__version__, '<', '1.0.dev0'):
+if _compare_version(mne.__version__, '<', '1.0.dev0'):  # pragma: no cover
     from mne.preprocessing import annotate_flat
     _annotate_flat_func = annotate_flat
 else:
@@ -142,7 +142,7 @@ def _inspect_raw(*, bids_path, l_freq, h_freq, find_flat, show_annotations):
                 min_duration=0.05,
                 bad_percent=5
             )
-        else:  # annotate_flat
+        else:  # pragma: no cover
             flat_annot, flat_chans = annotate_flat(
                 raw=raw,
                 min_duration=0.05


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
@@ -77,7 +77,12 @@
     edfblocks='ignore:.*EDF format requires equal-length data '
               'blocks:RuntimeWarning:mne',
     brainvision_unit='ignore:Encountered unsupported '
-                     'non-voltage units*.:UserWarning'
+                     'non-voltage units*.:UserWarning',
+    cnt_warning1='ignore:.*Could not parse meas date from the header. '
+                 'Setting to None.',
+    cnt_warning2='ignore:.*Could not define the number of bytes automatically.'
+                 ' Defaulting to 2.',
+    no_hand='ignore:.*Not setting subject handedness.:RuntimeWarning:mne'
 )
 
 
@@ -99,12 +104,14 @@ def fn(fname, *args, **kwargs):
 _read_raw_brainvision = _wrap_read_raw(mne.io.read_raw_brainvision)
 _read_raw_persyst = _wrap_read_raw(mne.io.read_raw_persyst)
 _read_raw_nihon = _wrap_read_raw(mne.io.read_raw_nihon)
+_read_raw_cnt = _wrap_read_raw(mne.io.read_raw_cnt)
 
 # parametrized directory, filename and reader for EEG/iEEG data formats
 test_eegieeg_data = [
     ('EDF', 'test_reduced.edf', _read_raw_edf),
     ('Persyst', 'sub-pt1_ses-02_task-monitor_acq-ecog_run-01_clip2.lay', _read_raw_persyst),  # noqa
-    ('NihonKohden', 'MB0400FU.EEG', _read_raw_nihon)
+    ('NihonKohden', 'MB0400FU.EEG', _read_raw_nihon),
+    ('CNT', 'scan41_short.cnt', _read_raw_cnt),
 ]
 test_convert_data = test_eegieeg_data.copy()
 test_convert_data.append(('CTF', 'testdata_ctf.ds', _read_raw_ctf))
@@ -119,8 +126,10 @@ def fn(fname, *args, **kwargs):
 test_converteeg_data = [
     ('Persyst', 'BrainVision', 'sub-pt1_ses-02_task-monitor_acq-ecog_run-01_clip2.lay', _read_raw_persyst),  # noqa
     ('NihonKohden', 'BrainVision', 'MB0400FU.EEG', _read_raw_nihon),
+    ('CNT', 'BrainVision', 'scan41_short.cnt', _read_raw_cnt),
     ('Persyst', 'EDF', 'sub-pt1_ses-02_task-monitor_acq-ecog_run-01_clip2.lay', _read_raw_persyst),  # noqa
     ('NihonKohden', 'EDF', 'MB0400FU.EEG', _read_raw_nihon),
+    ('CNT', 'EDF', 'scan41_short.cnt', _read_raw_cnt)
 ]
 
 
@@ -1161,7 +1170,10 @@ def test_vhdr(_bids_validate, tmp_path):
 @pytest.mark.filterwarnings(
     warning_str['nasion_not_found'],
     warning_str['brainvision_unit'],
-    warning_str['channel_unit_changed']
+    warning_str['channel_unit_changed'],
+    warning_str['cnt_warning1'],
+    warning_str['cnt_warning2'],
+    warning_str['no_hand'],
 )
 def test_eegieeg(dir_name, fname, reader, _bids_validate, tmp_path):
     """Test write_raw_bids conversion for EEG/iEEG data formats."""
@@ -1181,6 +1193,11 @@ def test_eegieeg(dir_name, fname, reader, _bids_validate, tmp_path):
         with pytest.warns(RuntimeWarning,
                           match='Encountered data in "short" format'):
             bids_output_path = write_raw_bids(**kwargs)
+    elif dir_name == 'CNT':
+        with pytest.warns(RuntimeWarning,
+                          match='Encountered data in "int" format. '
+                          'Converting to float32.'):
+            bids_output_path = write_raw_bids(**kwargs)
     else:
         with pytest.warns(RuntimeWarning,
                           match='Encountered data in "double" format'):
@@ -1224,6 +1241,11 @@ def test_eegieeg(dir_name, fname, reader, _bids_validate, tmp_path):
         with pytest.warns(RuntimeWarning,
                           match='Encountered data in "short" format'):
             write_raw_bids(**kwargs)
+    elif dir_name == 'CNT':
+        with pytest.warns(RuntimeWarning,
+                          match='Encountered data in "int" format. '
+                          'Converting to float32.'):
+            write_raw_bids(**kwargs)
     else:
         with pytest.warns(RuntimeWarning,
                           match='Encountered data in "double" format'):
@@ -1245,6 +1267,11 @@ def test_eegieeg(dir_name, fname, reader, _bids_validate, tmp_path):
         with pytest.warns(RuntimeWarning,
                           match='Encountered data in "short" format'):
             write_raw_bids(**kwargs)
+    elif dir_name == 'CNT':
+        with pytest.warns(RuntimeWarning,
+                          match='Encountered data in "int" format. '
+                          'Converting to float32.'):
+            write_raw_bids(**kwargs)
     else:
         with pytest.warns(RuntimeWarning,
                           match='Encountered data in "double" format'):
@@ -1306,6 +1333,11 @@ def test_eegieeg(dir_name, fname, reader, _bids_validate, tmp_path):
         with pytest.warns(RuntimeWarning,
                           match='Encountered data in "short" format'):
             write_raw_bids(**kwargs)
+    elif dir_name == 'CNT':
+        with pytest.warns(RuntimeWarning,
+                          match='Encountered data in "int" format. '
+                          'Converting to float32.'):
+            write_raw_bids(**kwargs)
     else:
         with pytest.warns(RuntimeWarning,
                           match='Encountered data in "double" format'):
@@ -1335,8 +1367,12 @@ def test_eegieeg(dir_name, fname, reader, _bids_validate, tmp_path):
 
     # check that scans list is properly converted to brainvision
     if check_version('pybv', '0.6') or dir_name == 'EDF':
-        daysback_min, daysback_max = _get_anonymization_daysback(raw)
-        daysback = (daysback_min + daysback_max) // 2
+        if raw.info['meas_date'] is not None:
+            daysback_min, daysback_max = _get_anonymization_daysback(raw)
+            daysback = (daysback_min + daysback_max) // 2
+        else:
+            # just pass back any arbitrary number if no measurement date
+            daysback = 3300
 
         kwargs = dict(raw=raw, bids_path=bids_path,
                       anonymize=dict(daysback=daysback), overwrite=True)
@@ -1348,6 +1384,11 @@ def test_eegieeg(dir_name, fname, reader, _bids_validate, tmp_path):
             with pytest.warns(RuntimeWarning,
                               match='Encountered data in "double" format'):
                 write_raw_bids(**kwargs)
+        elif dir_name == 'CNT':
+            with pytest.warns(RuntimeWarning,
+                              match='Encountered data in "int" format. '
+                              'Converting to float32.'):
+                write_raw_bids(**kwargs)
         else:
             with pytest.warns(RuntimeWarning,
                               match='Encountered data in "short" format'):
@@ -1380,6 +1421,11 @@ def test_eegieeg(dir_name, fname, reader, _bids_validate, tmp_path):
         with pytest.warns(RuntimeWarning,
                           match='Encountered data in "short" format'):
             write_raw_bids(**kwargs)
+    elif dir_name == 'CNT':
+        with pytest.warns(RuntimeWarning,
+                          match='Encountered data in "int" format. '
+                          'Converting to float32.'):
+            write_raw_bids(**kwargs)
     else:
         with pytest.warns(RuntimeWarning,
                           match='Encountered data in "double" format'):
@@ -1417,6 +1463,11 @@ def test_eegieeg(dir_name, fname, reader, _bids_validate, tmp_path):
         with pytest.warns(RuntimeWarning,
                           match='Encountered data in "short" format'):
             write_raw_bids(**kwargs)
+    elif dir_name == 'CNT':
+        with pytest.warns(RuntimeWarning,
+                          match='Encountered data in "int" format. '
+                          'Converting to float32.'):
+            write_raw_bids(**kwargs)
     else:
         with pytest.warns(RuntimeWarning,
                           match='Encountered data in "double" format'):
@@ -1465,6 +1516,11 @@ def test_eegieeg(dir_name, fname, reader, _bids_validate, tmp_path):
         with pytest.warns(RuntimeWarning,
                           match='Encountered data in "short" format'):
             write_raw_bids(**kwargs)
+    elif dir_name == 'CNT':
+        with pytest.warns(RuntimeWarning,
+                          match='Encountered data in "int" format. '
+                          'Converting to float32.'):
+            write_raw_bids(**kwargs)
     else:
         with pytest.warns(RuntimeWarning,
                           match='Encountered data in "double" format'):
@@ -1517,11 +1573,17 @@ def test_eegieeg(dir_name, fname, reader, _bids_validate, tmp_path):
             with pytest.warns(RuntimeWarning, match=match):
                 write_raw_bids(**kwargs)  # Just copies.
                 output_path = _test_anonymize(tmp_path / 'b', raw, bids_path)
+        elif dir_name == 'CNT':
+            with pytest.warns(RuntimeWarning,
+                              match='Encountered data in "int" format. '
+                              'Converting to float32.'):
+                write_raw_bids(**kwargs)
+                output_path = _test_anonymize(tmp_path / 'c', raw, bids_path)
         else:
             with pytest.warns(RuntimeWarning,
                               match='Encountered data in "double" format'):
                 write_raw_bids(**kwargs)  # Converts.
-                output_path = _test_anonymize(tmp_path / 'c', raw, bids_path)
+                output_path = _test_anonymize(tmp_path / 'd', raw, bids_path)
         _bids_validate(output_path)
 
 
@@ -2806,7 +2868,12 @@ def test_sidecar_encoding(_bids_validate, tmp_path):
 @pytest.mark.parametrize(
     'dir_name, format, fname, reader', test_converteeg_data)
 @pytest.mark.filterwarnings(
-    warning_str['channel_unit_changed'], warning_str['edfblocks'])
+    warning_str['channel_unit_changed'],
+    warning_str['edfblocks'],
+    warning_str['cnt_warning1'],
+    warning_str['cnt_warning2'],
+    warning_str['no_hand'],
+)
 def test_convert_eeg_formats(dir_name, format, fname, reader, tmp_path):
     """Test conversion of EEG/iEEG manufacturer fmt to BrainVision/EDF."""
     bids_root = tmp_path / format
@@ -2828,6 +2895,11 @@ def test_convert_eeg_formats(dir_name, format, fname, reader, tmp_path):
             with pytest.warns(RuntimeWarning,
                               match='Encountered data in "short" format'):
                 bids_output_path = write_raw_bids(**kwargs)
+        elif dir_name == 'CNT':
+            with pytest.warns(RuntimeWarning,
+                              match='Encountered data in "int" format. '
+                              'Converting to float32.'):
+                bids_output_path = write_raw_bids(**kwargs)
         else:
             with pytest.warns(RuntimeWarning,
                               match='Encountered data in "double" format'):
@@ -2872,7 +2944,12 @@ def test_convert_eeg_formats(dir_name, format, fname, reader, tmp_path):
 @pytest.mark.parametrize(
     'dir_name, format, fname, reader', test_converteeg_data)
 @pytest.mark.filterwarnings(
-    warning_str['channel_unit_changed'], warning_str['edfblocks'])
+    warning_str['channel_unit_changed'],
+    warning_str['edfblocks'],
+    warning_str['cnt_warning1'],
+    warning_str['cnt_warning2'],
+    warning_str['no_hand'],
+)
 def test_format_conversion_overwrite(dir_name, format, fname, reader,
                                      tmp_path):
     """Test that overwrite works when format is passed to write_raw_bids."""
@@ -2902,7 +2979,12 @@ def test_format_conversion_overwrite(dir_name, format, fname, reader,
 @requires_version('mne', '0.22')
 @pytest.mark.parametrize(
     'dir_name, format, fname, reader', test_converteeg_data)
-@pytest.mark.filterwarnings(warning_str['channel_unit_changed'])
+@pytest.mark.filterwarnings(
+    warning_str['channel_unit_changed'],
+    warning_str['cnt_warning1'],
+    warning_str['cnt_warning2'],
+    warning_str['no_hand'],
+)
 def test_error_write_meg_as_eeg(dir_name, format, fname, reader, tmp_path):
     """Test error writing as BrainVision EEG data for MEG."""
     bids_root = tmp_path / 'bids1'
@@ -2957,7 +3039,12 @@ def test_convert_meg_formats(dir_name, format, fname, reader, tmp_path):
 
 
 @pytest.mark.parametrize('dir_name, fname, reader', test_convert_data)
-@pytest.mark.filterwarnings(warning_str['channel_unit_changed'])
+@pytest.mark.filterwarnings(
+    warning_str['channel_unit_changed'],
+    warning_str['cnt_warning1'],
+    warning_str['cnt_warning2'],
+    warning_str['no_hand'],
+)
 def test_convert_raw_errors(dir_name, fname, reader, tmp_path):
     """Test errors when converting raw file formats."""
     bids_root = tmp_path / 'bids_1'