Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Neuroscan Support #924

Merged
merged 6 commits into from
Jan 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion doc/authors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@
.. _Clemens Brunner: https://github.com/cbrnr
.. _Kambiz Tavabi: https://github.com/ktavabi
.. _Franziska von Albedyll: https://www.researchgate.net/profile/Franziska-Von-Albedyll
.. _Simon Kern: https://github.com/skjerns
.. _Simon Kern: https://github.com/skjerns
.. _Yorguin Mantilla: https://github.com/yjmantilla
3 changes: 2 additions & 1 deletion doc/whats_new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ People who contributed to this release (in alphabetical order):
* `Mainak Jas`_
* `Richard Höchenberger`_
* `Stefan Appelhoff`_
* `Yorguin Mantilla`_

Detailed list of changes
~~~~~~~~~~~~~~~~~~~~~~~~

Enhancements
^^^^^^^^^^^^

- ...
- Add support for CNT (Neuroscan) files in :func:`mne_bids.write_raw_bids`, by `Yorguin Mantilla`_ (:gh:`924`)

API and behavior changes
^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
8 changes: 5 additions & 3 deletions mne_bids/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions mne_bids/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
107 changes: 97 additions & 10 deletions mne_bids/tests/test_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
)


Expand All @@ -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))
Expand All @@ -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)
]


Expand Down Expand Up @@ -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."""
Expand All @@ -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'):
Expand Down Expand Up @@ -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'):
Expand All @@ -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'):
Expand Down Expand Up @@ -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'):
Expand Down Expand Up @@ -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)
Expand All @@ -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'):
Expand Down Expand Up @@ -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'):
Expand Down Expand Up @@ -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'):
Expand Down Expand Up @@ -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'):
Expand Down Expand Up @@ -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)


Expand Down Expand Up @@ -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
Expand All @@ -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'):
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down