Skip to content

Commit 027bae8

Browse files
authored
Merge branch 'main' into main
2 parents 7bfab3e + 2ccfdf1 commit 027bae8

File tree

13 files changed

+183
-38
lines changed

13 files changed

+183
-38
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
repos:
22
- repo: https://github.com/astral-sh/ruff-pre-commit
3-
rev: v0.12.12
3+
rev: v0.13.1
44
hooks:
55
- id: ruff
66
name: ruff mne_bids/
@@ -33,7 +33,7 @@ repos:
3333
- id: check-docstring-first
3434

3535
- repo: https://github.com/pappasam/toml-sort
36-
rev: v0.24.2
36+
rev: v0.24.3
3737
hooks:
3838
- id: toml-sort-fix
3939
files: pyproject.toml

CITATION.cff

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@ authors:
214214
family-names: Ritz
215215
affiliation: 'Princeton Neuroscience Institute, Princeton University, Princeton, USA'
216216
orcid: 'https://orcid.org/0009-0003-1477-4912'
217+
- given-names: Alex
218+
family-names: Lopez Marquez
219+
affiliation: 'Institut Guttmann, Barcelona, Spain'
220+
orcid: 'https://orcid.org/0000-0002-3353-280X'
217221
- given-names: Nathan
218222
family-names: Azrak
219223
orcid: 'https://orcid.org/0000-0001-7695-4634'

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,4 @@ Please also cite one of the following papers to credit BIDS, depending on which
5858
- [EEG-BIDS](https://doi.org/10.1038/s41597-019-0104-8)
5959
- [iEEG-BIDS](https://doi.org/10.1038/s41597-019-0105-7)
6060
- [NIRS-BIDS](https://doi.org/10.31219/osf.io/7nmcp)
61+
- [Motion-BIDS](https://doi.org/10.1038/s41597-024-03559-8)

doc/authors.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.. _Aaron Earle-Richardson: https://github.com/Aaronearlerichardson
22
.. _Adam Li: https://github.com/adam2392
3+
.. _Alex Lopez Marquez: https://github.com/alm180
34
.. _Alex Rockhill: https://github.com/alexrockhill
45
.. _Alexandre Gramfort: http://alexandre.gramfort.net
56
.. _Amaia Benitez: https://github.com/AmaiaBA
@@ -58,3 +59,4 @@
5859
.. _waldie11: https://github.com/waldie11
5960
.. _William Turner: https://bootstrapbill.github.io/
6061
.. _Yorguin Mantilla: https://github.com/yjmantilla
62+
.. _Julius Welzel: https://github.com/JuliusWelzel

doc/whats_new.rst

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ Version 0.18 (unreleased)
1919

2020
The following authors contributed for the first time. Thank you so much! 🤩
2121

22-
* TBD
22+
* `Julius Welzel`_
23+
* `Alex Lopez Marquez`_
24+
2325

2426
The following authors had contributed before. Thank you for sticking around! 🤘
2527

26-
* TBD
28+
* `Stefan Appelhoff`_
2729

2830

2931
Detailed list of changes
@@ -32,13 +34,13 @@ Detailed list of changes
3234
🚀 Enhancements
3335
^^^^^^^^^^^^^^^
3436

35-
- None yet
36-
37+
- :func:`mne_bids.write_raw_bids()` has a new parameter `electrodes_tsv_task` which allows adding the `task` entity to the `electrodes.tsv` filepath, by `Alex Lopez Marquez`_ (:gh:`1424`)
38+
- Extended the configuration to recognise `motion` as a valid BIDS datatype by `Julius Welzel`_ (:gh:`1430`)
3739

3840
🧐 API and behavior changes
3941
^^^^^^^^^^^^^^^^^^^^^^^^^^^
4042

41-
- None yet
43+
- `tracksys` accepted as argument in :class:`mne_bids.BIDSPath()` by `Julius Welzel`_ (:gh:`1430`)
4244

4345
🛠 Requirements
4446
^^^^^^^^^^^^^^^
@@ -48,11 +50,11 @@ Detailed list of changes
4850
🪲 Bug fixes
4951
^^^^^^^^^^^^
5052

51-
- None yet
53+
- Fixed a bug that modified the name and help message of some of the available commands, by `Alex Lopez Marquez`_ (:gh:`1441`)
5254

5355
⚕️ Code health
5456
^^^^^^^^^^^^^^
5557

56-
- None yet
58+
- Made :func:`mne_bids.copyfiles.copyfile_brainvision` output more meaningful error messages when encountering problematic files, by `Stefan Appelhoff`_ (:gh:`1444`)
5759

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

mne_bids/commands/run.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
mne_bin_dir = Path(mne_bids.__file__).parent
1313
valid_command_paths = sorted((mne_bin_dir / "commands").glob("mne_bids_*.py"))
14-
valid_commands = [cmd.stem.lstrip("mne_bids_") for cmd in valid_command_paths]
14+
valid_commands = [cmd.stem.removeprefix("mne_bids_") for cmd in valid_command_paths]
1515

1616

1717
def print_help():

mne_bids/config.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from mne import io
77
from mne.io.constants import FIFF
88

9-
BIDS_VERSION = "1.7.0"
9+
BIDS_VERSION = "1.9.0"
1010

1111
PYBV_VERSION = "0.7.3"
1212
EEGLABIO_VERSION = "0.0.2"
@@ -15,17 +15,19 @@
1515

1616
EPHY_ALLOWED_DATATYPES = ["meg", "eeg", "ieeg", "nirs"]
1717

18-
ALLOWED_DATATYPES = EPHY_ALLOWED_DATATYPES + ["anat", "beh"]
18+
ALLOWED_DATATYPES = EPHY_ALLOWED_DATATYPES + ["anat", "beh", "motion"]
1919

2020
MEG_CONVERT_FORMATS = ["FIF", "auto"]
2121
EEG_CONVERT_FORMATS = ["BrainVision", "auto"]
2222
IEEG_CONVERT_FORMATS = ["BrainVision", "auto"]
2323
NIRS_CONVERT_FORMATS = ["auto"]
24+
MOTION_CONVERT_FORMATS = ["tsv", "auto"]
2425
CONVERT_FORMATS = {
2526
"meg": MEG_CONVERT_FORMATS,
2627
"eeg": EEG_CONVERT_FORMATS,
2728
"ieeg": IEEG_CONVERT_FORMATS,
2829
"nirs": NIRS_CONVERT_FORMATS,
30+
"motion": MOTION_CONVERT_FORMATS,
2931
}
3032

3133
# Orientation of the coordinate system dependent on manufacturer
@@ -147,12 +149,17 @@
147149
".snirf", # SNIRF
148150
]
149151

152+
allowed_extensions_motion = [
153+
".tsv", # Tab-separated values
154+
]
155+
150156
# allowed extensions (data formats) in BIDS spec
151157
ALLOWED_DATATYPE_EXTENSIONS = {
152158
"meg": allowed_extensions_meg,
153159
"eeg": allowed_extensions_eeg,
154160
"ieeg": allowed_extensions_ieeg,
155161
"nirs": allowed_extensions_nirs,
162+
"motion": allowed_extensions_motion,
156163
}
157164

158165
# allow additional extensions that are not BIDS
@@ -190,6 +197,7 @@
190197
"physio",
191198
"stim", # behavioral
192199
"nirs",
200+
"motion", # motion
193201
]
194202

195203
# converts suffix to known path modalities
@@ -227,6 +235,7 @@
227235
"description",
228236
"suffix",
229237
"extension",
238+
"tracking_system",
230239
)
231240
ALLOWED_PATH_ENTITIES_SHORT = {
232241
"sub": "subject",
@@ -239,6 +248,7 @@
239248
"recording": "recording",
240249
"split": "split",
241250
"desc": "description",
251+
"tracksys": "tracking_system",
242252
}
243253

244254
# Annotations to never remove during reading or writing
@@ -316,6 +326,7 @@
316326
ALLOWED_SPACES["ieeg"] = BIDS_SHARED_COORDINATE_FRAMES + BIDS_IEEG_COORDINATE_FRAMES
317327
ALLOWED_SPACES["anat"] = None
318328
ALLOWED_SPACES["beh"] = None
329+
ALLOWED_SPACES["motion"] = None
319330

320331
# See: https://bids-specification.readthedocs.io/en/latest/appendices/entity-table.html#encephalography-eeg-ieeg-and-meg # noqa: E501
321332
ENTITY_VALUE_TYPE = {
@@ -331,6 +342,7 @@
331342
"description": "label",
332343
"suffix": "label",
333344
"extension": "label",
345+
"tracking_system": "label",
334346
}
335347

336348
# mapping from supported BIDs coordinate frames -> MNE

mne_bids/copyfiles.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -108,17 +108,24 @@ def _get_brainvision_paths(vhdr_path):
108108
vmrk_file = vmrk_file_match.groups()[0]
109109

110110
# Make sure we are dealing with file names as is customary, not paths
111-
# Paths are problematic when copying the files to another system. Instead,
112-
# always use the file name and keep the file triplet in the same directory
113-
assert os.sep not in eeg_file
114-
assert os.sep not in vmrk_file
111+
for fi in [eeg_file, vmrk_file]:
112+
if os.sep in fi:
113+
raise RuntimeError(
114+
f"Detected a path separator in a file link: {fi}.\n\n"
115+
"Paths are problematic when copying the files to another system. "
116+
"Instead, always use the file name and keep the "
117+
"BrainVision file triplet (eeg/dat, vhdr, vmrk) in the same directory."
118+
)
115119

116120
# Assert the paths exist
117121
head, tail = op.split(vhdr_path)
118122
eeg_file_path = op.join(head, eeg_file)
119123
vmrk_file_path = op.join(head, vmrk_file)
120-
assert op.exists(eeg_file_path)
121-
assert op.exists(vmrk_file_path)
124+
for fpath in [eeg_file_path, vmrk_file_path]:
125+
if not Path(fpath).exists():
126+
raise FileNotFoundError(
127+
f"{fpath} referenced in {vhdr_path} but it does not exist."
128+
)
122129

123130
# Return the paths
124131
return (eeg_file_path, vmrk_file_path)
@@ -355,14 +362,24 @@ def copyfile_brainvision(vhdr_src, vhdr_dest, anonymize=None, verbose=None):
355362
# Write new header and marker files, fixing the file pointer links
356363
# For that, we need to replace an old "basename" with a new one
357364
# assuming that all .eeg/.dat, .vhdr, .vmrk share one basename
358-
__, basename_src = op.split(fname_src)
359-
assert op.split(eeg_file_path)[-1] in [basename_src + ".eeg", basename_src + ".dat"]
360-
assert basename_src + ".vmrk" == op.split(vmrk_file_path)[-1]
361-
__, basename_dest = op.split(fname_dest)
365+
basename_src = Path(fname_src).name
366+
eeg_expected = [f"{basename_src}.eeg", f"{basename_src}.dat"]
367+
vmrk_expected = [f"{basename_src}.vmrk"]
368+
if Path(eeg_file_path).name not in eeg_expected:
369+
raise RuntimeError(
370+
f"Unexpected path to data file in {vhdr_src}:\n "
371+
f"-->{Path(eeg_file_path).name}\nExpected one of {eeg_expected}."
372+
)
373+
if Path(vmrk_file_path).name not in vmrk_expected:
374+
raise RuntimeError(
375+
f"Unexpected path to marker file in {vhdr_src}:\n "
376+
f"-->{Path(vmrk_file_path).name}\nExpected one of {vmrk_expected}."
377+
)
378+
basename_dest = Path(fname_dest).name
362379
search_lines = [
363-
"DataFile=" + basename_src + ".eeg",
364-
"DataFile=" + basename_src + ".dat",
365-
"MarkerFile=" + basename_src + ".vmrk",
380+
f"DataFile={basename_src}.eeg",
381+
f"DataFile={basename_src}.dat",
382+
f"MarkerFile={basename_src}.vmrk",
366383
]
367384

368385
with open(vhdr_src, encoding=enc) as fin:

mne_bids/dig.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,14 @@ def _write_coordsystem_json(
385385
_write_json(fname, fid_json, overwrite=True)
386386

387387

388-
def _write_dig_bids(bids_path, raw, montage=None, acpc_aligned=False, overwrite=False):
388+
def _write_dig_bids(
389+
bids_path,
390+
raw,
391+
montage=None,
392+
acpc_aligned=False,
393+
electrodes_tsv_task=False,
394+
overwrite=False,
395+
):
389396
"""Write BIDS formatted DigMontage from Raw instance.
390397
391398
Handles coordsystem.json and electrodes.tsv writing
@@ -405,6 +412,9 @@ def _write_dig_bids(bids_path, raw, montage=None, acpc_aligned=False, overwrite=
405412
must be transformed from the "head" coordinate frame.
406413
acpc_aligned : bool
407414
Whether "mri" space is aligned to ACPC.
415+
electrodes_tsv_task : bool
416+
Whether to add the ``task-`` entity to the ``electrodes.tsv`` filename.
417+
Defaults to ``False``.
408418
overwrite : bool
409419
Whether to overwrite the existing file.
410420
Defaults to False.
@@ -500,12 +510,16 @@ def _write_dig_bids(bids_path, raw, montage=None, acpc_aligned=False, overwrite=
500510
"acquisition": bids_path.acquisition,
501511
"space": None if bids_path.datatype == "nirs" else coord_frame,
502512
}
513+
# add `task-` to the electrodes.tsv file if requested
514+
electrode_file_entities = coord_file_entities.copy()
515+
if electrodes_tsv_task and bids_path.task is not None:
516+
electrode_file_entities["task"] = bids_path.task
503517
channels_suffix = "optodes" if bids_path.datatype == "nirs" else "electrodes"
504518
_channels_fun = (
505519
_write_optodes_tsv if bids_path.datatype == "nirs" else _write_electrodes_tsv
506520
)
507521
channels_path = BIDSPath(
508-
**coord_file_entities, suffix=channels_suffix, extension=".tsv"
522+
**electrode_file_entities, suffix=channels_suffix, extension=".tsv"
509523
)
510524
coordsystem_path = BIDSPath(
511525
**coord_file_entities, suffix="coordsystem", extension=".json"

mne_bids/path.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ def __init__(
385385
space=None,
386386
split=None,
387387
description=None,
388+
tracking_system=None,
388389
root=None,
389390
suffix=None,
390391
extension=None,
@@ -421,6 +422,7 @@ def __init__(
421422
space=space,
422423
split=split,
423424
description=description,
425+
tracking_system=tracking_system,
424426
root=root,
425427
datatype=datatype,
426428
suffix=suffix,
@@ -442,6 +444,7 @@ def entities(self):
442444
"recording": self.recording,
443445
"split": self.split,
444446
"description": self.description,
447+
"tracking_system": self.tracking_system,
445448
}
446449

447450
@property
@@ -575,6 +578,15 @@ def description(self) -> str | None:
575578
def description(self, value):
576579
self.update(description=value)
577580

581+
@property
582+
def tracking_system(self) -> str | None:
583+
"""The tracking system entity."""
584+
return self._tracking_system
585+
586+
@tracking_system.setter
587+
def tracking_system(self, value):
588+
self.update(tracking_system=value)
589+
578590
@property
579591
def suffix(self) -> str | None:
580592
"""The filename suffix."""
@@ -881,7 +893,9 @@ def fpath(self):
881893
if self.suffix is None or self.suffix in ALLOWED_DATATYPES:
882894
# now only use valid datatype extension
883895
if self.extension is None:
884-
valid_exts = sum(ALLOWED_DATATYPE_EXTENSIONS.values(), [])
896+
valid_exts = ALLOWED_DATATYPE_EXTENSIONS.get(
897+
self.datatype, sum(ALLOWED_DATATYPE_EXTENSIONS.values(), [])
898+
)
885899
else:
886900
valid_exts = [self.extension]
887901
matching_paths = [
@@ -2325,6 +2339,7 @@ def _filter_fnames(
23252339
description=None,
23262340
suffix=None,
23272341
extension=None,
2342+
tracking_system=None,
23282343
):
23292344
"""Filter a list of BIDS filenames / paths based on BIDS entity values.
23302345
@@ -2351,6 +2366,7 @@ def _filter_fnames(
23512366
description = _ensure_tuple(description)
23522367
suffix = _ensure_tuple(suffix)
23532368
extension = _ensure_tuple(extension)
2369+
tracking_system = _ensure_tuple(tracking_system)
23542370

23552371
leading_path_str = r".*\/?" # nothing or something ending with a `/`
23562372
sub_str = r"sub-(" + "|".join(subject) + ")" if subject else r"sub-([^_]+)"
@@ -2375,6 +2391,11 @@ def _filter_fnames(
23752391
)
23762392
suffix_str = r"_(" + "|".join(suffix) + ")" if suffix else r"_([^_]+)"
23772393
ext_str = r"(" + "|".join(extension) + ")$" if extension else r"\.([^_]+)"
2394+
tracksys_str = (
2395+
r"tracksys-(" + "|".join(tracking_system) + ")"
2396+
if tracking_system
2397+
else r"(|tracksys-([^_]+))"
2398+
)
23782399

23792400
regexp = (
23802401
leading_path_str
@@ -2390,6 +2411,7 @@ def _filter_fnames(
23902411
+ desc_str
23912412
+ suffix_str
23922413
+ ext_str
2414+
+ tracksys_str
23932415
)
23942416

23952417
# Convert to str so we can apply the regexp ...

0 commit comments

Comments
 (0)