Skip to content

Commit f53a4e2

Browse files
committed
Merge branch 'main' into add_emg_support
2 parents 31ee317 + 8e4e13d commit f53a4e2

File tree

14 files changed

+170
-76
lines changed

14 files changed

+170
-76
lines changed

.github/workflows/unit_tests.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jobs:
5151
fail-fast: false
5252
matrix:
5353
os: [ubuntu-latest]
54-
python-version: ["3.10", "3.13"] # Oldest and newest supported versions
54+
python-version: ["3.11", "3.13"] # Oldest and newest supported versions
5555
steps:
5656
- name: Set up Python ${{ matrix.python-version }}
5757
uses: actions/setup-python@v6
@@ -129,22 +129,22 @@ jobs:
129129
#
130130
# 8 checks:
131131
# for each machine type (ubuntu, macos, windows):
132-
# 3.10 / mne-stable / full / validator-stable
132+
# 3.11 / mne-stable / full / validator-stable
133133
# 3.13 / mne-stable / full / validator-stable
134134
#
135135
# 4 additional checks with alternative MNE-Python and BIDS validator versions:
136136
# ubuntu / 3.13 / mne-main / full / validator-main --> to test cutting edge of everything
137-
# ubuntu / 3.10 / mne-prev / full / validator-stable --> to test last supported of everything
137+
# ubuntu / 3.11 / mne-prev / full / validator-stable --> to test last supported of everything
138138
# ubuntu / 3.13 / mne-stable / minimal / validator-stable --> to test a minimal installation
139-
# ubuntu / 3.12 / mne-stable / full / validator-main-schema --> to test next-gen BIDS validator
139+
# ubuntu / 3.13 / mne-stable / full / validator-main-schema --> to test next-gen BIDS validator
140140
# macos-15-intel / 3.13 / mne-stable / full / validator-stable --> to test macos x86_64
141141
timeout-minutes: 60
142142
runs-on: ${{ matrix.os }}
143143
strategy:
144144
fail-fast: false
145145
matrix:
146146
os: [ubuntu-latest, macos-latest, windows-latest]
147-
python-version: ["3.10", "3.13"] # Oldest and newest supported versions
147+
python-version: ["3.11", "3.13"] # Oldest and newest supported versions
148148
mne-version: [mne-stable]
149149
mne-bids-install: [full]
150150
bids-validator-version: [validator-stable]
@@ -159,7 +159,7 @@ jobs:
159159
bids-validator-version: validator-main
160160
# Test previous MNE stable version
161161
- os: ubuntu-latest
162-
python-version: "3.10"
162+
python-version: "3.11"
163163
mne-version: mne-prev-stable
164164
mne-bids-install: full
165165
bids-validator-version: validator-stable
@@ -171,7 +171,7 @@ jobs:
171171
bids-validator-version: validator-stable
172172
# Test next gen BIDS schema validator
173173
- os: ubuntu-latest
174-
python-version: "3.12"
174+
python-version: "3.13"
175175
mne-version: mne-stable
176176
mne-bids-install: full
177177
bids-validator-version: validator-main-schema

doc/install.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@ 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

3132
We recommend installing ``mne-bids`` into an isolated Python environment,
3233
for example created via ``conda``
3334
(may be obtained through `miniconda <https://docs.conda.io/en/latest/miniconda.html>`_).
34-
We require that you **use Python 3.10 or higher**.
35+
We require that you **use Python 3.11 or higher**.
3536
You may choose to install ``mne-bids`` into your isolated Python environment
3637
`via pip <#installation-via-pip>`_ or
3738
`via conda <#installation-via-conda>`_.

doc/whats_new.rst

Lines changed: 7 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,11 +45,13 @@ 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
^^^^^^^^^^^^^^^
54+
- MNE-BIDS now requires Python 3.11 or higher.
5255
- Including ``filelock`` as a dependency to handle atomic file writing and parallel processing support by `Bruno Aristimunha`_ (:gh:`1451`)
5356

5457
- None yet
@@ -60,12 +63,15 @@ Detailed list of changes
6063
- 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`)
6164
- 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`)
6265
- 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`)
66+
- 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`)
67+
- Converting data to EDF format while also anonymizing it will now yield valid EDF files (with ``startdate`` set to 1985-01-01 00:00:00); the BIDS-compliant anonymized date will still be stored in ``scans.tsv`` file and restored to the in-memory Raw object when read using :func:`~mne_bids.read_raw_bids`, by `Daniel McCloy`_ (:gh:`1479`)
6368

6469
⚕️ Code health
6570
^^^^^^^^^^^^^^
6671

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

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

examples/convert_empty_room.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
# Let us first import mne_bids.
3030

3131
import shutil
32-
from datetime import datetime, timezone
32+
from datetime import UTC, datetime
3333

3434
import mne
3535
from mne.datasets import sample
@@ -98,7 +98,7 @@
9898
for date in dates:
9999
er_bids_path.update(session=date)
100100
er_meas_date = datetime.strptime(date, "%Y%m%d")
101-
er_raw.set_meas_date(er_meas_date.replace(tzinfo=timezone.utc))
101+
er_raw.set_meas_date(er_meas_date.replace(tzinfo=UTC))
102102
write_raw_bids(er_raw, er_bids_path, overwrite=True)
103103

104104
# %%

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: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import json
77
import os
88
import re
9-
from datetime import datetime, timedelta, timezone
9+
from datetime import UTC, datetime, timedelta
1010
from difflib import get_close_matches
1111
from pathlib import Path
1212

@@ -417,14 +417,14 @@ def _handle_scans_reading(scans_fname, raw, bids_path):
417417

418418
if acq_time_is_utc:
419419
# Enforce setting timezone to UTC without additonal conversion
420-
acq_time = acq_time.replace(tzinfo=timezone.utc)
420+
acq_time = acq_time.replace(tzinfo=UTC)
421421
else:
422422
# Convert time offset to UTC
423423
if acq_time.tzinfo is None:
424424
# Windows needs an explicit local tz for naive, pre-epoch times.
425-
local_tz = datetime.now().astimezone().tzinfo or timezone.utc
425+
local_tz = datetime.now().astimezone().tzinfo or UTC
426426
acq_time = acq_time.replace(tzinfo=local_tz)
427-
acq_time = acq_time.astimezone(timezone.utc)
427+
acq_time = acq_time.astimezone(UTC)
428428

429429
logger.debug(f"Loaded {scans_fname} scans file to set acq_time as {acq_time}.")
430430
# First set measurement date to None and then call call anonymize() to
@@ -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_path.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import shutil
99
import shutil as sh
1010
import timeit
11-
from datetime import datetime, timezone
11+
from datetime import UTC, datetime
1212
from pathlib import Path
1313

1414
import mne
@@ -1435,7 +1435,7 @@ def test_find_empty_room(return_bids_test_dir, tmp_path):
14351435
for date in dates:
14361436
er_bids_path.update(session=date)
14371437
er_meas_date = datetime.strptime(date, "%Y%m%d")
1438-
er_meas_date = er_meas_date.replace(tzinfo=timezone.utc)
1438+
er_meas_date = er_meas_date.replace(tzinfo=UTC)
14391439
er_raw.set_meas_date(er_meas_date)
14401440
write_raw_bids(er_raw, er_bids_path, verbose=False)
14411441

@@ -1460,8 +1460,8 @@ def test_find_empty_room(return_bids_test_dir, tmp_path):
14601460
bids_root = tmp_path / "associated-empty-room"
14611461
bids_root.mkdir()
14621462
raw = _read_raw_fif(raw_fname)
1463-
meas_date = datetime(year=2020, month=1, day=10, tzinfo=timezone.utc)
1464-
er_date = datetime(year=2010, month=1, day=1, tzinfo=timezone.utc)
1463+
meas_date = datetime(year=2020, month=1, day=10, tzinfo=UTC)
1464+
er_date = datetime(year=2010, month=1, day=1, tzinfo=UTC)
14651465
raw.set_meas_date(meas_date)
14661466

14671467
er_raw_matching_date = er_raw.copy().set_meas_date(meas_date)
@@ -1543,7 +1543,7 @@ def test_find_emptyroom_ties(tmp_path):
15431543
)
15441544
er_dir = er_dir_path.mkdir().directory
15451545

1546-
meas_date = datetime.strptime(session, "%Y%m%d").replace(tzinfo=timezone.utc)
1546+
meas_date = datetime.strptime(session, "%Y%m%d").replace(tzinfo=UTC)
15471547

15481548
raw = _read_raw_fif(raw_fname)
15491549

mne_bids/tests/test_read.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import shutil as sh
1414
from collections import OrderedDict
1515
from contextlib import nullcontext
16-
from datetime import date, datetime, timezone
16+
from datetime import UTC, date, datetime
1717
from pathlib import Path
1818

1919
import mne
@@ -108,7 +108,7 @@ def _make_parallel_raw(subject, *, seed=None):
108108
info = mne.create_info(["MEG0113"], 100, ch_types="mag")
109109
data = rng.standard_normal((1, 100)) * 1e-12
110110
raw = mne.io.RawArray(data, info)
111-
raw.set_meas_date(datetime(2020, 1, 1, tzinfo=timezone.utc))
111+
raw.set_meas_date(datetime(2020, 1, 1, tzinfo=UTC))
112112
raw.info["line_freq"] = 60
113113
raw.info["subject_info"] = {
114114
"his_id": subject,
@@ -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")
@@ -802,7 +802,7 @@ def test_handle_scans_reading(tmp_path):
802802
scans_tsv = _from_tsv(scans_path)
803803
acq_time_str = scans_tsv["acq_time"][0]
804804
acq_time = datetime.strptime(acq_time_str, "%Y-%m-%dT%H:%M:%S.%fZ")
805-
acq_time = acq_time.replace(tzinfo=timezone.utc)
805+
acq_time = acq_time.replace(tzinfo=UTC)
806806
new_acq_time = acq_time_str.split(".")[0] + "Z"
807807
assert acq_time == raw_01.info["meas_date"]
808808
scans_tsv["acq_time"][0] = new_acq_time
@@ -813,7 +813,7 @@ def test_handle_scans_reading(tmp_path):
813813
raw_02 = read_raw_bids(bids_path)
814814
new_acq_time = new_acq_time.replace("Z", ".0Z")
815815
new_acq_time = datetime.strptime(new_acq_time, "%Y-%m-%dT%H:%M:%S.%fZ")
816-
new_acq_time = new_acq_time.replace(tzinfo=timezone.utc)
816+
new_acq_time = new_acq_time.replace(tzinfo=UTC)
817817
assert raw_02.info["meas_date"] == new_acq_time
818818
assert new_acq_time != raw_01.info["meas_date"]
819819

@@ -833,7 +833,7 @@ def test_handle_scans_reading(tmp_path):
833833
# from the original date and the same as the newly altered date
834834
raw_03 = read_raw_bids(bids_path)
835835
new_acq_time = datetime.strptime(new_acq_time_str, date_format)
836-
assert raw_03.info["meas_date"] == new_acq_time.astimezone(timezone.utc)
836+
assert raw_03.info["meas_date"] == new_acq_time.astimezone(UTC)
837837

838838
# Regression for naive, pre-epoch acquisition times (Windows bug GH-1399)
839839
pre_epoch_str = "1950-06-15T13:45:30"
@@ -842,10 +842,8 @@ def test_handle_scans_reading(tmp_path):
842842

843843
raw_pre_epoch = read_raw_bids(bids_path)
844844
pre_epoch_naive = datetime.strptime(pre_epoch_str, "%Y-%m-%dT%H:%M:%S")
845-
local_tz = datetime.now().astimezone().tzinfo or timezone.utc
846-
expected_pre_epoch = pre_epoch_naive.replace(tzinfo=local_tz).astimezone(
847-
timezone.utc
848-
)
845+
local_tz = datetime.now().astimezone().tzinfo or UTC
846+
expected_pre_epoch = pre_epoch_naive.replace(tzinfo=local_tz).astimezone(UTC)
849847
assert raw_pre_epoch.info["meas_date"] == expected_pre_epoch
850848
if raw_pre_epoch.annotations.orig_time is not None:
851849
assert raw_pre_epoch.annotations.orig_time == expected_pre_epoch

0 commit comments

Comments
 (0)