Skip to content

Commit 0e3b9d6

Browse files
authored
Merge branch 'main' into main
2 parents a1f79d4 + 6ed57f5 commit 0e3b9d6

File tree

9 files changed

+95
-29
lines changed

9 files changed

+95
-29
lines changed

.github/workflows/cffconvert.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ on:
44
push:
55
paths:
66
- CITATION.cff
7+
pull_request:
8+
paths:
9+
- CITATION.cff
710

811
jobs:
912
validate:

doc/whats_new.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,14 @@ Detailed list of changes
8686

8787
- :func:`~mne_bids.read_raw_bids` doesn't populate ``raw.info['subject_info']`` with invalid values anymore, preventing users from writing the data to disk again, by `Richard Höchenberger`_ (:gh:`1031`)
8888

89+
- Writing EEGLAB files was sometimes broken when ``.set`` and ``.fdt`` pairs were supplied. This is now fixed in :func:`~mne_bids.copyfiles.copyfile_eeglab`, by `Stefan Appelhoff`_ (:gh:`1039`)
90+
8991
- Writing and copying CTF files now works on Windows when files already exist (``overwrite=True``), by `Stefan Appelhoff`_ (:gh:`1035`)
9092

9193
- Instead of deleting files and raising cryptic errors, an intentional error message is now sent when calling :func:`~mne_bids.write_raw_bids` with the source file identical to the destination file, unless ``format`` is specified, by `Adam Li`_ and `Stefan Appelhoff`_ (:gh:`889`)
9294

95+
- Internal helper function to :func:`~mne_bids.read_raw_bids` would reject BrainVision data if ``_scans.tsv`` listed a ``.eeg`` file instead of ``.vhdr``, by `Teon Brooks`_ (:gh:`1034`)
96+
9397
:doc:`Find out what was new in previous releases <whats_new_previous_releases>`
9498

9599
.. include:: authors.rst

examples/convert_nirs_to_bids.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
# %%
2626
# We are importing everything we need for this example:
2727
import os.path as op
28+
import pathlib
2829
import shutil
2930

3031
import mne
@@ -43,7 +44,7 @@
4344
# We will use the MNE-NIRS package which includes convenient functions to
4445
# download openly available datasets.
4546

46-
data_dir = mne_nirs.datasets.fnirs_motor_group.data_path()
47+
data_dir = pathlib.Path(mne_nirs.datasets.fnirs_motor_group.data_path())
4748

4849
# Let's see whether the data has been downloaded using a quick visualization
4950
# of the directory tree.

mne_bids/copyfiles.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
from mne_bids.path import BIDSPath, _parse_ext, _mkdir_p
3131
from mne_bids.utils import _get_mrk_meas_date, _check_anonymize
32+
import numpy as np
3233

3334

3435
def _copytree(src, dst, **kwargs):
@@ -495,12 +496,11 @@ def copyfile_edf(src, dest, anonymize=None):
495496

496497

497498
def copyfile_eeglab(src, dest):
498-
"""Copy a EEGLAB files to a new location and adjust pointer to '.fdt' file.
499+
"""Copy an EEGLAB file to a new location.
499500
500-
Some EEGLAB .set files come with a .fdt binary file that contains the data.
501-
When moving a .set file, we need to check for an associated .fdt file and
502-
move it to an appropriate location as well as update an internal pointer
503-
within the .set file.
501+
If the EEGLAB ``.set`` file comes with an accompanying ``.fdt`` binary file
502+
that contains the actual data, this function will copy this file, too, and
503+
update all internal pointers in the new ``.set`` file.
504504
505505
Parameters
506506
----------
@@ -529,27 +529,40 @@ def copyfile_eeglab(src, dest):
529529
f' but got {ext_src}, {ext_dest}')
530530

531531
# Load the EEG struct
532+
# NOTE: do *not* simplify cells, because this changes the underlying
533+
# structure and potentially breaks re-reading of the file
532534
uint16_codec = None
533-
eeg = loadmat(file_name=src, simplify_cells=True,
535+
eeg = loadmat(file_name=src, simplify_cells=False,
534536
appendmat=False, uint16_codec=uint16_codec)
535537
oldstyle = False
536538
if 'EEG' in eeg:
537539
eeg = eeg['EEG']
538540
oldstyle = True
539541

540-
if isinstance(eeg['data'], str):
542+
try:
541543
# If the data field is a string, it points to a .fdt file in src dir
542-
fdt_fname = eeg['data']
543-
assert fdt_fname.endswith('.fdt')
544-
head, tail = op.split(src)
544+
if isinstance(eeg['data'][0, 0][0], str):
545+
has_fdt_link = True
546+
except IndexError:
547+
has_fdt_link = False
548+
549+
if has_fdt_link:
550+
fdt_fname = eeg['data'][0, 0][0]
551+
552+
assert fdt_fname.endswith('.fdt'), f'Unexpected fdt name: {fdt_fname}'
553+
head, _ = op.split(src)
545554
fdt_path = op.join(head, fdt_fname)
546555

547556
# Copy the .fdt file and give it a new name
548-
sh.copyfile(fdt_path, fname_dest + '.fdt')
557+
fdt_name_new = fname_dest + '.fdt'
558+
sh.copyfile(fdt_path, fdt_name_new)
549559

550560
# Now adjust the pointer in the .set file
551-
head, tail = op.split(fname_dest + '.fdt')
552-
eeg['data'] = tail
561+
# NOTE: Clunky numpy code is to match MATLAB structure for "savemat"
562+
_, tail = op.split(fdt_name_new)
563+
new_value = np.empty((1, 1), dtype=object)
564+
new_value[0, 0] = np.atleast_1d(np.array(tail))
565+
eeg['data'] = new_value
553566

554567
# Save the EEG dictionary as a Matlab struct again
555568
mdict = dict(EEG=eeg) if oldstyle else eeg

mne_bids/read.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,13 +233,23 @@ def _handle_scans_reading(scans_fname, raw, bids_path):
233233
# get the row corresponding to the file
234234
# use string concatenation instead of os.path
235235
# to work nicely with windows
236-
data_fname = bids_path.datatype + '/' + fname
236+
data_fname = Path(bids_path.datatype) / fname
237237
fnames = scans_tsv['filename']
238+
fnames = [Path(fname) for fname in fnames]
238239
if 'acq_time' in scans_tsv:
239240
acq_times = scans_tsv['acq_time']
240241
else:
241242
acq_times = ['n/a'] * len(fnames)
242243

244+
# There are three possible extensions for BrainVision
245+
# First gather all the possible extensions
246+
acq_suffixes = set(fname.suffix for fname in fnames)
247+
# Add the filename extension for the bids folder
248+
acq_suffixes.add(Path(data_fname).suffix)
249+
250+
if all(suffix in ('.vhdr', '.eeg', '.vmrk') for suffix in acq_suffixes):
251+
ext = fnames[0].suffix
252+
data_fname = Path(data_fname).with_suffix(ext)
243253
row_ind = fnames.index(data_fname)
244254

245255
# check whether all split files have the same acq_time
@@ -250,7 +260,9 @@ def _handle_scans_reading(scans_fname, raw, bids_path):
250260
bids_path.basename[:split_idx] +
251261
r'split-\d+_' + bids_path.datatype +
252262
bids_path.fpath.suffix)
253-
split_fnames = list(filter(pattern.match, fnames))
263+
split_fnames = list(filter(
264+
lambda x: pattern.match(x.as_posix()), fnames
265+
))
254266
split_acq_times = []
255267
for split_f in split_fnames:
256268
split_acq_times.append(acq_times[fnames.index(split_f)])

mne_bids/tests/data/tiny_bids/code/make_tiny_bids_dataset.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import os
55
import os.path as op
6+
from pathlib import Path
67

78
import mne
89
import numpy as np
@@ -14,7 +15,7 @@
1415
vhdr_fname = op.join(data_path, "montage", "bv_dig_test.vhdr")
1516
captrak_path = op.join(data_path, "montage", "captrak_coords.bvct")
1617

17-
mne_bids_root = os.sep.join(mne_bids.__file__.split("/")[:-2])
18+
mne_bids_root = Path(mne_bids.__file__).parent.parent
1819
tiny_bids = op.join(mne_bids_root, "mne_bids", "tests", "data", "tiny_bids")
1920
os.makedirs(tiny_bids, exist_ok=True)
2021

mne_bids/tests/test_copyfiles.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import pytest
1212

1313
import mne
14-
from mne.fixes import _compare_version
1514
from mne.datasets import testing
1615
from mne_bids import BIDSPath
1716
from mne_bids.path import _parse_ext
@@ -198,16 +197,10 @@ def test_copyfile_edfbdf_uppercase(tmp_path):
198197
'test_raw_2021.set'))
199198
def test_copyfile_eeglab(tmp_path, fname):
200199
"""Test the copying of EEGlab set and fdt files."""
201-
if (
202-
fname == 'test_raw_chanloc.set' and
203-
_compare_version(testing.get_version(), '<', '0.112')
204-
):
205-
return
206-
207200
bids_root = str(tmp_path)
208201
data_path = op.join(testing.data_path(), 'EEGLAB')
209202
raw_fname = op.join(data_path, fname)
210-
new_name = op.join(bids_root, 'tested_conversion.set')
203+
new_name = op.join(bids_root, f'CONVERTED_{fname}.set')
211204

212205
# IO error testing
213206
with pytest.raises(ValueError, match="Need to move data with same ext"):

mne_bids/tests/test_read.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
import json
66
import os
77
import os.path as op
8-
import pathlib
8+
from pathlib import Path
99
from datetime import datetime, timezone
10+
from typing import OrderedDict
1011

1112
import pytest
1213
import shutil as sh
@@ -23,7 +24,7 @@
2324
from mne_bids.config import (MNE_STR_TO_FRAME, BIDS_SHARED_COORDINATE_FRAMES,
2425
BIDS_TO_MNE_FRAMES)
2526
from mne_bids.read import (read_raw_bids, _read_raw, get_head_mri_trans,
26-
_handle_events_reading)
27+
_handle_events_reading, _handle_scans_reading)
2728
from mne_bids.tsv_handler import _to_tsv, _from_tsv
2829
from mne_bids.utils import (_write_json)
2930
from mne_bids.sidecar_updates import _update_sidecar
@@ -56,6 +57,10 @@
5657
# Data with cHPI info
5758
raw_fname_chpi = op.join(data_path, 'SSS', 'test_move_anon_raw.fif')
5859

60+
# Tiny BIDS testing dataset
61+
mne_bids_root = Path(mne_bids.__file__).parent.parent
62+
tiny_bids = op.join(mne_bids_root, "mne_bids", "tests", "data", "tiny_bids")
63+
5964
warning_str = dict(
6065
channel_unit_changed='ignore:The unit for chann*.:RuntimeWarning:mne',
6166
meas_date_set_to_none="ignore:.*'meas_date' set to None:RuntimeWarning:"
@@ -567,6 +572,38 @@ def test_handle_scans_reading(tmp_path):
567572
assert new_acq_time != raw_01.info['meas_date']
568573

569574

575+
def test_handle_scans_reading_brainvision(tmp_path):
576+
"""Test stability of BrainVision's different file extensions"""
577+
test_scan_eeg = OrderedDict(
578+
[('filename', [Path('eeg/sub-01_ses-eeg_task-rest_eeg.eeg')]),
579+
('acq_time', ['2000-01-01T12:00:00.000000Z'])]
580+
)
581+
test_scan_vmrk = OrderedDict(
582+
[('filename', [Path('eeg/sub-01_ses-eeg_task-rest_eeg.vmrk')]),
583+
('acq_time', ['2000-01-01T12:00:00.000000Z'])]
584+
)
585+
test_scan_edf = OrderedDict(
586+
[('filename', [Path('eeg/sub-01_ses-eeg_task-rest_eeg.edf')]),
587+
('acq_time', ['2000-01-01T12:00:00.000000Z'])]
588+
)
589+
os.mkdir(tmp_path / 'eeg')
590+
for test_scan in [test_scan_eeg, test_scan_vmrk, test_scan_edf]:
591+
_to_tsv(test_scan, tmp_path / test_scan['filename'][0])
592+
593+
bids_path = BIDSPath(subject='01', session='eeg', task='rest',
594+
datatype='eeg', root=tiny_bids)
595+
with pytest.warns(RuntimeWarning, match='Not setting positions'):
596+
raw = read_raw_bids(bids_path)
597+
598+
for test_scan in [test_scan_eeg, test_scan_vmrk]:
599+
_handle_scans_reading(tmp_path / test_scan['filename'][0],
600+
raw, bids_path)
601+
602+
with pytest.raises(ValueError, match="is not in list"):
603+
_handle_scans_reading(tmp_path / test_scan_edf['filename'][0],
604+
raw, bids_path)
605+
606+
570607
@pytest.mark.filterwarnings(warning_str['channel_unit_changed'])
571608
def test_handle_info_reading(tmp_path):
572609
"""Test reading information from a BIDS sidecar JSON file."""
@@ -587,7 +624,7 @@ def test_handle_info_reading(tmp_path):
587624
bids_fname.update(datatype=suffix)
588625
sidecar_fname = _find_matching_sidecar(bids_fname, suffix=suffix,
589626
extension='.json')
590-
sidecar_fname = pathlib.Path(sidecar_fname)
627+
sidecar_fname = Path(sidecar_fname)
591628

592629
# assert that we get the same line frequency set
593630
raw = read_raw_bids(bids_path=bids_path)
@@ -1071,7 +1108,7 @@ def test_write_read_fif_split_file(tmp_path, monkeypatch):
10711108
n_times = int(2.5e6 / n_channels) # enough to produce a 10MB split
10721109
data = np.empty((n_channels, n_times), dtype=np.float32)
10731110
raw = mne.io.RawArray(data, raw.info)
1074-
big_fif_fname = pathlib.Path(tmp_dir) / 'test_raw.fif'
1111+
big_fif_fname = Path(tmp_dir) / 'test_raw.fif'
10751112

10761113
split_size = '10MB'
10771114
raw.save(big_fif_fname, split_size=split_size)

setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ filterwarnings =
8787
ignore:MEG ref channel RMSP did not.*:RuntimeWarning
8888
# Python 3.10+ and NumPy 1.22 (and maybe also newer NumPy versions?)
8989
ignore:.*distutils\.sysconfig module is deprecated.*:DeprecationWarning
90+
# numba with NumPy dev
91+
ignore:`np.MachAr` is deprecated.*:DeprecationWarning
9092

9193
[pydocstyle]
9294
convention = pep257

0 commit comments

Comments
 (0)