Skip to content

Commit 56d36ab

Browse files
authored
add curryreader as full dep (mne-tools#1475)
* fixups from mne-tools#1430 * fix wrong orig_unit sometimes written to channels.tsv * fix doctest (bad syntax; add verbose; avoid np types) * add curryreader to full and test deps * importorskip
1 parent 1e0a96e commit 56d36ab

File tree

10 files changed

+68
-23
lines changed

10 files changed

+68
-23
lines changed

doc/install.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Optional:
2525
* ``matplotlib`` (>=3.6, for using the interactive data inspector)
2626
* ``pandas`` (>=1.3.2, for generating event statistics)
2727
* ``edfio`` (>=0.4.10, for writing EDF data)
28+
* ``curryreader`` (>=0.1.2, for reading Curry data)
2829
* ``defusedxml`` (for writing reading EGI MFF data and BrainVision montages)
2930
* ``filelock`` (for atomic file writing, and parallel processing support)
3031

doc/whats_new.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ The following authors contributed for the first time. Thank you so much! 🤩
2727
The following authors had contributed before. Thank you for sticking around! 🤘
2828

2929
* `Stefan Appelhoff`_
30+
* `Daniel McCloy`_
3031

3132

3233
Detailed list of changes
@@ -44,8 +45,9 @@ Detailed list of changes
4445
🧐 API and behavior changes
4546
^^^^^^^^^^^^^^^^^^^^^^^^^^^
4647

47-
- `tracksys` accepted as argument in :class:`mne_bids.BIDSPath()` by `Julius Welzel`_ (:gh:`1430`)
48+
- ``tracksys`` accepted as argument in :class:`mne_bids.BIDSPath()` by `Julius Welzel`_ (:gh:`1430`)
4849
- :func:`mne_bids.read_raw_bids()` has a new parameter ``on_ch_mismatch`` that controls behaviour when there is a mismatch between channel names in ``channels.tsv`` and the raw data; accepted values are ``'raise'`` (default), ``'reorder'``, and ``'rename'``, by `Kalle Mäkelä`_.
50+
- Column names read from ``.tsv`` are now converted from :class:`numpy.str_` class to built-in :class:`str` class, by `Daniel McCloy`_ (:gh:`1475`).
4951

5052
🛠 Requirements
5153
^^^^^^^^^^^^^^^
@@ -60,12 +62,14 @@ Detailed list of changes
6062
- Updated MEG/iEEG writers to satisfy the stricter checks in the latest BIDS validator releases: BTi/4D run folders now retain their ``.pdf`` suffix (falling back to the legacy naming when an older validator is detected), KIT marker files encode the run via the ``acq`` entity instead of ``run``, datasets lacking iEEG montages receive placeholder ``electrodes.tsv``/``coordsystem.json`` files, and the ``AssociatedEmptyRoom`` entry stores dataset-relative paths by `Bruno Aristimunha`_ (:gh:`1449`)
6163
- Made the lock helpers skip reference counting when the optional ``filelock`` dependency is missing, preventing spurious ``AttributeError`` crashes during reads, by `Bruno Aristimunha`_ (:gh:`1469`)
6264
- Fixed a bug in :func:`mne_bids.read_raw_bids` that caused it to fail when reading BIDS datasets where the acquisition time was specified in local time rather than UTC only in Windows, by `Bruno Aristimunha`_ (:gh:`1452`)
65+
- Fixed bug in :func:`~mne_bids.write_raw_bids` where incorrect unit was sometimes written into ``channels.tsv`` file when converting data to BrainVision, EDF, or EEGLAB formats, by `Daniel McCloy`_ (:gh:`1475`)
6366

6467
⚕️ Code health
6568
^^^^^^^^^^^^^^
6669

6770
- Made :func:`mne_bids.copyfiles.copyfile_brainvision` output more meaningful error messages when encountering problematic files, by `Stefan Appelhoff`_ (:gh:`1444`)
6871
- Raised the minimum ``edfio`` requirement to ``0.4.10``, eeglabio to ``0.1.0`` by `Bruno Aristimunha`_ (:gh:`1449`)
6972
- Relaxed EDF padding warnings in the test suite to accommodate upstream changes by `Bruno Aristimunha`_ (:gh:`1449`)
73+
- Adapt to upstream switch to new reader package for Neuroscan-Curry-format files, by `Daniel McCloy`_ (:gh:`1475`)
7074

7175
:doc:`Find out what was new in previous releases <whats_new_previous_releases>`

mne_bids/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
PYBV_VERSION = "0.7.3"
1212
EEGLABIO_VERSION = "0.0.2"
13+
CURRYREADER_VERSION = "0.1.2"
1314

1415
DOI = """https://doi.org/10.21105/joss.01896"""
1516

mne_bids/path.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1766,7 +1766,8 @@ def get_entities_from_fname(fname, on_error="raise", verbose=None):
17661766
'space': None, \
17671767
'recording': None, \
17681768
'split': None, \
1769-
'description': None}
1769+
'description': None, \
1770+
'tracking_system': None}
17701771
"""
17711772
if on_error not in ("warn", "raise", "ignore"):
17721773
raise ValueError(

mne_bids/read.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -541,14 +541,16 @@ def _handle_info_reading(sidecar_fname, raw):
541541
return raw
542542

543543

544-
def events_file_to_annotation_kwargs(events_fname: str | Path) -> dict:
544+
@verbose
545+
def events_file_to_annotation_kwargs(events_fname: str | Path, *, verbose=None) -> dict:
545546
r"""
546547
Read the ``events.tsv`` file and extract onset, duration, and description.
547548
548549
Parameters
549550
----------
550551
events_fname : str
551552
The file path to the ``events.tsv`` file.
553+
%(verbose)s
552554
553555
Returns
554556
-------
@@ -592,8 +594,8 @@ def events_file_to_annotation_kwargs(events_fname: str | Path) -> dict:
592594
... 'duration': [0.1, 0.1, 0.1],
593595
... 'trial_type': ['event1', 'event2', 'event1'],
594596
... 'value': [1, 2, 1],
595-
... 'sample': [10, 20, 30]
596-
'foo': ['a', 'b', 'c'],
597+
... 'sample': [10, 20, 30],
598+
... 'foo': ['a', 'b', 'c'],
597599
... }
598600
>>> df = pd.DataFrame(data)
599601
>>>
@@ -603,15 +605,11 @@ def events_file_to_annotation_kwargs(events_fname: str | Path) -> dict:
603605
>>> df.to_csv(events_file, sep='\t', index=False)
604606
>>>
605607
>>> # Read the events file using the function
606-
>>> events_dict = events_file_to_annotation_kwargs(events_file)
608+
>>> events_dict = events_file_to_annotation_kwargs(events_file, verbose=False)
607609
>>> events_dict
608-
{'onset': array([0.1, 0.2, 0.3]),
609-
'duration': array([0.1, 0.1, 0.1]),
610-
'description': array(['event1', 'event2', 'event1'], dtype='<U6'),
611-
'event_id': {'event1': 1, 'event2': 2},
612-
'extras': [{'foo': 'a'}, {'foo': 'b'}, {'foo': 'c'}]}
610+
{'onset': array([0.1, 0.2, 0.3]), 'duration': array([0.1, 0.1, 0.1]), 'description': array(['event1', 'event2', 'event1'], dtype='<U6'), 'event_id': {'event1': 1, 'event2': 2}, 'extras': [{'foo': 'a'}, {'foo': 'b'}, {'foo': 'c'}]}
613611
614-
"""
612+
""" # noqa E501
615613
logger.info(f"Reading events from {events_fname}.")
616614
events_dict = _from_tsv(events_fname)
617615

@@ -677,9 +675,11 @@ def events_file_to_annotation_kwargs(events_fname: str | Path) -> dict:
677675
culled_vals = culled_vals.astype(int)
678676
except ValueError: # numeric, but has some non-integer values
679677
pass
678+
# purge any np.str_, np.int_, or np.float_ types
679+
culled_vals = np.asarray(culled_vals).tolist()
680680
event_id = dict(zip(culled[trial_type_col_name], culled_vals))
681681
else:
682-
event_id = dict(zip(trial_types, np.arange(len(trial_types))))
682+
event_id = dict(zip(trial_types, list(range(len(trial_types)))))
683683
descrs = np.asarray(trial_types, dtype=str)
684684

685685
# convert onsets & durations to floats ("n/a" onsets were already dropped)

mne_bids/tests/test_read.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@ def test_handle_events_reading(tmp_path, with_extras):
635635
match=re.escape(
636636
"The version of MNE-Python you are using (<1.10) "
637637
"does not support the extras argument in mne.Annotations. "
638-
"The extra column(s) [np.str_('foo')] will be ignored."
638+
"The extra column(s) ['foo'] will be ignored."
639639
),
640640
)
641641
if with_extras and not check_version("mne", "1.10")

mne_bids/tests/test_write.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
)
4848
from mne_bids.config import (
4949
BIDS_COORD_FRAME_DESCRIPTIONS,
50+
CURRYREADER_VERSION,
5051
EEGLABIO_VERSION,
5152
PYBV_VERSION,
5253
REFERENCES,
@@ -3373,8 +3374,12 @@ def test_sidecar_encoding(_bids_validate, tmp_path):
33733374
@testing.requires_testing_data
33743375
def test_convert_eeg_formats(dir_name, fmt, fname, reader, tmp_path):
33753376
"""Test conversion of EEG/iEEG manufacturer fmt to BrainVision/EDF."""
3376-
pytest.importorskip("pybv", PYBV_VERSION)
3377-
pytest.importorskip("eeglabio", EEGLABIO_VERSION)
3377+
if dir_name == "BrainVision" or fmt == "BrainVision":
3378+
pytest.importorskip("pybv", PYBV_VERSION)
3379+
elif dir_name == "EEGLAB" or fmt == "EEGLAB":
3380+
pytest.importorskip("eeglabio", EEGLABIO_VERSION)
3381+
elif dir_name == "curry" or fmt == "curry":
3382+
pytest.importorskip("curryreader", CURRYREADER_VERSION)
33783383
bids_root = tmp_path / fmt
33793384
raw_fname = data_path / dir_name / fname
33803385

@@ -3431,7 +3436,7 @@ def test_convert_eeg_formats(dir_name, fmt, fname, reader, tmp_path):
34313436
# load channels.tsv; the unit should be Volts
34323437
channels_fname = bids_output_path.copy().update(suffix="channels", extension=".tsv")
34333438
channels_tsv = _from_tsv(channels_fname)
3434-
assert channels_tsv["units"][0] == "V"
3439+
assert channels_tsv["units"][0] == "µV"
34353440

34363441
if fmt == "BrainVision":
34373442
assert Path(raw2.filenames[0]).suffix == ".eeg"

mne_bids/tsv_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def _from_tsv(fname, dtypes=None):
158158
# If data is 1-dimensional (only header), make it 2D
159159
data = np.atleast_2d(data)
160160

161-
column_names = data[0, :]
161+
column_names = data[0, :].tolist() # cast to list to avoid `np.str_()` keys in dict
162162
info = data[1:, :]
163163
data_dict = OrderedDict()
164164
if dtypes is None:

mne_bids/write.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def _should_use_bti_pdf_suffix() -> bool:
136136
return use_pdf_suffix
137137

138138

139-
def _channels_tsv(raw, fname, overwrite=False):
139+
def _channels_tsv(raw, fname, *, convert_fmt, overwrite=False):
140140
"""Create a channels.tsv file and save it.
141141
142142
Parameters
@@ -145,6 +145,10 @@ def _channels_tsv(raw, fname, overwrite=False):
145145
The data as MNE-Python Raw object.
146146
fname : str | mne_bids.BIDSPath
147147
Filename to save the channels.tsv to.
148+
convert_fmt : str | None
149+
Which format the data are being converted to (determines what gets written in
150+
the "unit" column). If ``None`` then we assume no conversion is happening (the
151+
raw data files are being copied from the source tree into the BIDS folder tree).
148152
overwrite : bool
149153
Whether to overwrite the existing file.
150154
Defaults to False.
@@ -193,11 +197,32 @@ def _channels_tsv(raw, fname, overwrite=False):
193197
ch_type.append(map_chs[_channel_type])
194198
description.append(map_desc[_channel_type])
195199
low_cutoff, high_cutoff = (raw.info["highpass"], raw.info["lowpass"])
196-
if raw._orig_units:
200+
# If data are being *converted*, unit is determined by destination format:
201+
# - `eeglabio.raw.export_set` always converts V to µV, cf:
202+
# https://github.com/jackz314/eeglabio/blob/3961bb29daf082767ea44e7c7d9da2df10971c37/eeglabio/raw.py#L57
203+
#
204+
# - `mne.export._edf_bdf._export_raw_edf_bdf` always converts V to µV, cf:
205+
# https://github.com/mne-tools/mne-python/blob/1b921f4af5154bad40202d87428a2583ef896a00/mne/export/_edf_bdf.py#L61-L63
206+
#
207+
# - `pybv.write_brainvision` converts V to µV by default (and we don't alter that)
208+
# https://github.com/bids-standard/pybv/blob/2832c80ee00d12990a8c79f12c843c0d4ddc825b/pybv/io.py#L40
209+
# https://github.com/mne-tools/mne-bids/blob/1e0a96e132fc904ba856d42beaa9ddddb985f1ed/mne_bids/write.py#L1279-L1280
210+
if convert_fmt:
211+
volt_like = "µV" if convert_fmt in ("BrainVision", "EDF", "EEGLAB") else "V"
212+
units = [
213+
volt_like
214+
if ch_i["unit"] == FIFF.FIFF_UNIT_V
215+
else _unit2human.get(ch_i["unit"], "n/a")
216+
for ch_i in raw.info["chs"]
217+
]
218+
# if raw data is merely copied, check `raw._orig_units`
219+
elif raw._orig_units:
197220
units = [raw._orig_units.get(ch, "n/a") for ch in raw.ch_names]
221+
# If `raw._orig_units` is missing, assume SI units
198222
else:
199223
units = [_unit2human.get(ch_i["unit"], "n/a") for ch_i in raw.info["chs"]]
200-
units = [u if u not in ["NA"] else "n/a" for u in units]
224+
# fixup "NA" (from `_unit2human`) → "n/a"
225+
units = [u if u not in ["NA"] else "n/a" for u in units]
201226

202227
# Translate from MNE to BIDS unit naming
203228
for idx, mne_unit in enumerate(units):
@@ -2229,7 +2254,6 @@ def write_raw_bids(
22292254
emptyroom_fname=associated_er_path,
22302255
overwrite=overwrite,
22312256
)
2232-
_channels_tsv(raw, channels_path.fpath, overwrite)
22332257

22342258
# create parent directories if needed
22352259
_mkdir_p(os.path.dirname(data_path))
@@ -2288,6 +2312,14 @@ def write_raw_bids(
22882312
f"for {datatype} datatype."
22892313
)
22902314

2315+
# this can't happen until after value of `convert` has been determined
2316+
_channels_tsv(
2317+
raw,
2318+
channels_path.fpath,
2319+
convert_fmt=format if convert else None,
2320+
overwrite=overwrite,
2321+
)
2322+
22912323
# raise error when trying to copy files (copyfile_*) into same location
22922324
# (src == dest, see https://github.com/mne-tools/mne-bids/issues/867)
22932325
if (

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ doc = [
6262
]
6363
# Dependencies for using all mne_bids features
6464
full = [
65+
"curryreader >= 0.1.2",
6566
"defusedxml", # For reading EGI MFF data and BrainVision montages
6667
"edfio >= 0.4.10",
6768
"eeglabio >= 0.1.0",
@@ -74,7 +75,7 @@ full = [
7475
"pymatreader",
7576
]
7677
# Dependencies for running the test infrastructure
77-
test = ["mne_bids[full]", "pytest", "pytest-cov", "pytest-rerunfailures", "pytest-sugar", "ruff"]
78+
test = ["curryreader", "mne_bids[full]", "pytest", "pytest-cov", "pytest-rerunfailures", "pytest-sugar", "ruff"]
7879

7980
[project.urls]
8081
"Bug Tracker" = "https://github.com/mne-tools/mne-bids/issues/"

0 commit comments

Comments
 (0)