Skip to content

Commit 31ee317

Browse files
committed
coverage + test refactor
1 parent b89c159 commit 31ee317

File tree

4 files changed

+83
-36
lines changed

4 files changed

+83
-36
lines changed

mne_bids/tests/test_utils.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ def test_handle_datatype():
8484
assert _handle_datatype(raw, datatype) == datatype
8585
# datatype is not given, will be inferred if possible
8686
datatype = None
87+
# check basic inference (only one ch_type)
88+
info = mne.create_info(n_channels, sampling_rate, ch_types="emg")
89+
raw = mne.io.RawArray(random((2, sampling_rate)), info)
90+
assert _handle_datatype(raw, datatype) == "emg"
8791
# check if datatype is correctly inferred (combined EEG and iEEG/MEG data)
8892
channel_types = [["grad", "eeg"], ["eeg", "mag"], ["eeg", "seeg"], ["ecog", "eeg"]]
8993
expected_modalities = ["meg", "meg", "ieeg", "ieeg"]
@@ -108,7 +112,7 @@ def test_handle_datatype():
108112
_handle_datatype(raw, datatype)
109113
# if proper channel type (iEEG, EEG or MEG) is not found, raise ValueError
110114
ch_type = ["misc"]
111-
with pytest.raises(ValueError, match="No MEG, EEG, iEEG, or EMG channels found"):
115+
with pytest.raises(ValueError, match="No MEG, EEG, iEEG, EMG, or fNIRS channels"):
112116
info = mne.create_info(n_channels, sampling_rate, ch_types=ch_type * 2)
113117
raw = mne.io.RawArray(data, info)
114118
_handle_datatype(raw, datatype)

mne_bids/tests/test_write.py

Lines changed: 66 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,12 @@
9898
no_montage=r"ignore:Not setting position of.*channel found in "
9999
r"montage.*:RuntimeWarning:mne",
100100
emg_coords_missing=r"ignore:No electrode location info found.*:RuntimeWarning",
101-
converting_to_edf=r"ignore:Converting data files to EDF format:RuntimeWarning",
101+
converting_to_edf=r"ignore:Converting data files to [BE]DF format:RuntimeWarning",
102102
channel_mismatch=(
103103
"ignore:Channel mismatch between .*channels\\.tsv and the raw data file "
104104
"detected\\.:RuntimeWarning:mne"
105105
),
106+
edf_date="ignore:.*limits dates to after 1985-01-01:RuntimeWarning",
106107
)
107108

108109

@@ -3173,6 +3174,7 @@ def test_coordsystem_json_compliance(
31733174
"sub-pt1_ses-02_task-monitor_acq-ecog_run-01_clip2.lay",
31743175
_read_raw_persyst,
31753176
),
3177+
("01", "Brainvision", "Analyzer_nV_Export.vhdr", _read_raw_brainvision),
31763178
("03", "NihonKohden", "MB0400FU.EEG", _read_raw_nihon),
31773179
("emptyroom", "MEG/sample", "sample_audvis_trunc_raw.fif", _read_raw_fif),
31783180
],
@@ -3182,10 +3184,14 @@ def test_coordsystem_json_compliance(
31823184
warning_str["channel_unit_changed"],
31833185
warning_str["edf_warning"],
31843186
warning_str["brainvision_unit"],
3187+
warning_str["converting_to_edf"],
3188+
warning_str["edfblocks"],
3189+
warning_str["emg_coords_missing"],
3190+
warning_str["edf_date"],
31853191
)
31863192
@testing.requires_testing_data
31873193
def test_anonymize(subject, dir_name, fname, reader, tmp_path, _bids_validate):
3188-
"""Test writing anonymized EDF data."""
3194+
"""Test writing anonymized data."""
31893195
raw_fname = op.join(data_path, dir_name, fname)
31903196

31913197
bids_root = tmp_path / "bids1"
@@ -3197,21 +3203,21 @@ def test_anonymize(subject, dir_name, fname, reader, tmp_path, _bids_validate):
31973203
# handle different edge cases
31983204
if subject == "emptyroom":
31993205
bids_path.update(task="noise", session=raw_date, suffix="meg", datatype="meg")
3200-
erm = None
3206+
write_kw = dict(empty_room=None)
3207+
elif dir_name == "Brainvision": # pretend it's EMG data
3208+
raw.set_channel_types({ch: "emg" for ch in raw.ch_names})
3209+
raw.set_montage(None)
3210+
bids_path.update(task="task", suffix="emg", datatype="emg")
3211+
write_kw = dict(empty_room=None, emg_placement="Measured")
32013212
else:
32023213
bids_path.update(task="task", suffix="eeg", datatype="eeg")
32033214
# make sure anonymization works when also writing empty room file
3204-
erm = raw.copy()
3215+
write_kw = dict(empty_room=raw.copy())
32053216
daysback_min, daysback_max = get_anonymization_daysback(raw)
32063217
anonymize = dict(daysback=daysback_min + 1)
32073218
orig_bids_path = bids_path.copy()
32083219
bids_path = write_raw_bids(
3209-
raw,
3210-
bids_path,
3211-
overwrite=True,
3212-
anonymize=anonymize,
3213-
verbose=False,
3214-
empty_room=erm,
3220+
raw, bids_path, overwrite=True, anonymize=anonymize, verbose=False, **write_kw
32153221
)
32163222
# emptyroom recordings' session should match the recording date
32173223
if subject == "emptyroom":
@@ -3225,7 +3231,8 @@ def test_anonymize(subject, dir_name, fname, reader, tmp_path, _bids_validate):
32253231
assert _raw.info["meas_date"].year == 1985
32263232
assert _raw.info["meas_date"].month == 1
32273233
assert _raw.info["meas_date"].day == 1
3228-
assert raw2.info["meas_date"].year < 1925
3234+
year = 1986 if dir_name == "Brainvision" else 1925
3235+
assert raw2.info["meas_date"].year < year
32293236

32303237
# write without source
32313238
scans_fname = BIDSPath(
@@ -3237,7 +3244,12 @@ def test_anonymize(subject, dir_name, fname, reader, tmp_path, _bids_validate):
32373244
)
32383245
anonymize["keep_source"] = False
32393246
bids_path = write_raw_bids(
3240-
raw, orig_bids_path, overwrite=True, anonymize=anonymize, verbose=False
3247+
raw,
3248+
orig_bids_path,
3249+
overwrite=True,
3250+
anonymize=anonymize,
3251+
verbose=False,
3252+
**write_kw,
32413253
)
32423254
scans_tsv = _from_tsv(scans_fname)
32433255
assert "source" not in scans_tsv.keys()
@@ -3249,6 +3261,7 @@ def test_anonymize(subject, dir_name, fname, reader, tmp_path, _bids_validate):
32493261
overwrite=True,
32503262
anonymize=dict(daysback=daysback_min, keep_source=True),
32513263
verbose=False,
3264+
**write_kw,
32523265
)
32533266
scans_fname = BIDSPath(
32543267
subject=bids_path.subject,
@@ -3259,7 +3272,8 @@ def test_anonymize(subject, dir_name, fname, reader, tmp_path, _bids_validate):
32593272
)
32603273
scans_tsv = _from_tsv(scans_fname)
32613274
assert scans_tsv["source"] == [Path(f).name for f in raw.filenames]
3262-
_bids_validate(bids_path.root)
3275+
if dir_name != "Brainvision": # EMG not yet supported by validator
3276+
_bids_validate(bids_path.root)
32633277

32643278
# update the scans sidecar JSON with information
32653279
scans_json_fpath = scans_fname.copy().update(extension=".json")
@@ -3275,6 +3289,7 @@ def test_anonymize(subject, dir_name, fname, reader, tmp_path, _bids_validate):
32753289
overwrite=True,
32763290
anonymize=dict(daysback=daysback_min, keep_source=True),
32773291
verbose=False,
3292+
**write_kw,
32783293
)
32793294
with open(scans_json_fpath) as fin:
32803295
scans_json = json.load(fin)
@@ -3371,16 +3386,19 @@ def test_sidecar_encoding(_bids_validate, tmp_path):
33713386
@testing.requires_testing_data
33723387
def test_emg_errors_and_warnings(tmp_path):
33733388
"""Test EMG-specific error/warning raising."""
3374-
bids_root = tmp_path / "EDF"
3375-
raw_fname = data_path / "EDF" / "test_generator_2.edf"
3389+
bids_root = tmp_path / "EMG_errors"
3390+
raw_fname = data_path / "Brainvision" / "test_NO.vhdr"
33763391
bids_path = _bids_path.copy().update(root=bids_root, datatype="emg")
3377-
raw = _read_raw_edf(raw_fname)
3392+
raw = _read_raw_brainvision(raw_fname)
33783393
raw.set_channel_types({ch: "emg" for ch in raw.ch_names}) # HACK eeg → emg
3379-
raw = raw.pick(["emg"]) # drop misc
3380-
good_kwargs = dict(raw=raw, bids_path=bids_path, verbose=False)
3394+
good_kwargs = dict(raw=raw, bids_path=bids_path, verbose=False, overwrite=True)
33813395
with pytest.raises(ValueError, match="`emg_placement` must be one of"):
33823396
write_raw_bids(**good_kwargs, emg_placement="Foo")
3383-
with pytest.warns(RuntimeWarning, match="add `coordsystem.json` file manually"):
3397+
with (
3398+
pytest.warns(RuntimeWarning, match="BDF format requires equal-length data"),
3399+
pytest.warns(RuntimeWarning, match="Converting data files to BDF format"),
3400+
pytest.warns(RuntimeWarning, match="add `coordsystem.json` file manually"),
3401+
):
33843402
write_raw_bids(**good_kwargs, emg_placement="Other")
33853403

33863404

@@ -3399,7 +3417,6 @@ def test_convert_emg_formats(tmp_path, dir_name, fmt, fname, reader):
33993417
bids_path = _bids_path.copy().update(root=bids_root, datatype="emg")
34003418
raw = reader(raw_fname)
34013419
raw.set_channel_types({ch: "emg" for ch in raw.ch_names}) # HACK eeg → emg
3402-
raw = raw.pick(["emg"]) # drop misc
34033420
# test anonymization in one case too, for coverage
34043421
if dir_name == "Brainvision":
34053422
raw.anonymize()
@@ -3803,8 +3820,8 @@ def test_write_associated_emptyroom(_bids_validate, tmp_path, empty_room_dtype):
38033820
assert meg_json_data["AssociatedEmptyRoom"] == expected_rel
38043821

38053822

3806-
def test_preload(_bids_validate, tmp_path):
3807-
"""Test writing custom preloaded raw objects."""
3823+
def test_preload_errors(tmp_path):
3824+
"""Test allow_preload error handling."""
38083825
bids_root = tmp_path / "bids"
38093826
bids_path = _bids_path.copy().update(root=bids_root)
38103827
sfreq, n_points = 1024.0, int(1e6)
@@ -3814,25 +3831,45 @@ def test_preload(_bids_validate, tmp_path):
38143831
raw.orig_format = "single"
38153832
raw.info["line_freq"] = 60
38163833

3834+
shared_kwargs = dict(raw=raw, bids_path=bids_path, verbose=False, overwrite=True)
38173835
# reject preloaded by default
38183836
with pytest.raises(ValueError, match="allow_preload"):
3819-
write_raw_bids(raw, bids_path, verbose=False, overwrite=True)
3837+
write_raw_bids(**shared_kwargs)
38203838

38213839
# preloaded raw must specify format
38223840
with pytest.raises(ValueError, match="format"):
3823-
write_raw_bids(
3824-
raw, bids_path, allow_preload=True, verbose=False, overwrite=True
3825-
)
3841+
write_raw_bids(**shared_kwargs, allow_preload=True)
3842+
38263843

3844+
@pytest.mark.filterwarnings(
3845+
warning_str["edfblocks"],
3846+
warning_str["emg_coords_missing"],
3847+
warning_str["converting_to_edf"],
3848+
)
3849+
@pytest.mark.parametrize(
3850+
"format,ch_type", (("BrainVision", "eeg"), ("BDF", "emg"), ("EDF", "seeg"))
3851+
)
3852+
def test_preload(_bids_validate, tmp_path, format, ch_type):
3853+
"""Test writing custom preloaded raw objects."""
3854+
bids_root = tmp_path / "bids"
3855+
bids_path = _bids_path.copy().update(root=bids_root)
3856+
sfreq = 1024.0
3857+
info = mne.create_info(["ch1", "ch2"], sfreq, ch_type)
3858+
raw = mne.io.RawArray(np.empty((2, 100), dtype=np.float32), info)
3859+
raw.orig_format = "single"
3860+
raw.info["line_freq"] = 60
3861+
kw = dict(emg_placement="Measured") if ch_type == "emg" else dict()
38273862
write_raw_bids(
38283863
raw,
38293864
bids_path,
3830-
allow_preload=True,
3831-
format="BrainVision",
38323865
verbose=False,
38333866
overwrite=True,
3867+
allow_preload=True,
3868+
format=format,
3869+
**kw,
38343870
)
3835-
_bids_validate(bids_root)
3871+
if ch_type != "emg": # TODO validator support for EMG not available yet
3872+
_bids_validate(bids_root)
38363873

38373874

38383875
@pytest.mark.parametrize("dir_name", ("tsv_test", "json_test"))

mne_bids/utils.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,15 +135,15 @@ def _handle_datatype(raw, datatype):
135135
raw : mne.io.Raw
136136
Raw object.
137137
datatype : str | None
138-
Can be one of either ``'meg'``, ``'eeg'``, or ``'ieeg'``. If ``None``,
138+
Can be one of either ``'meg'``, ``'eeg'``, ``'emg'`` or ``'ieeg'``. If ``None``,
139139
`mne.utils._handle_datatype()` will attempt to infer the datatype from
140140
the ``raw`` object. In case of multiple data types in the ``raw``
141141
object, ``datatype`` must not be ``None``.
142142
143143
Returns
144144
-------
145145
datatype : str
146-
One of either ``'meg'``, ``'eeg'``, or ``'ieeg'``.
146+
One of either ``'meg'``, ``'eeg'``, ``'emg'``, or ``'ieeg'``.
147147
"""
148148
if datatype is not None:
149149
_check_datatype(raw, datatype)
@@ -173,7 +173,7 @@ def _handle_datatype(raw, datatype):
173173
datatypes.append("nirs")
174174
if len(datatypes) == 0:
175175
raise ValueError(
176-
"No MEG, EEG, iEEG, or EMG channels found in data. "
176+
"No MEG, EEG, iEEG, EMG, or fNIRS channels found in data. "
177177
"Please use raw.set_channel_types to set the "
178178
"channel types in the data."
179179
)
@@ -182,8 +182,6 @@ def _handle_datatype(raw, datatype):
182182
datatype = "meg"
183183
elif "ieeg" in datatypes and "meg" not in datatypes:
184184
datatype = "ieeg"
185-
elif "emg" in datatypes:
186-
datatype = "emg"
187185
else:
188186
raise ValueError(
189187
f"Multiple data types (``{datatypes}``) were "

mne_bids/write.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1351,7 +1351,15 @@ def _write_raw_edf_bdf(raw, bids_fname, overwrite):
13511351
overwrite : bool
13521352
Whether to overwrite an existing file or not.
13531353
"""
1354-
assert bids_fname.suffix in (".edf", ".bdf")
1354+
ext = bids_fname.suffix[1:].upper()
1355+
assert ext in ("EDF", "BDF")
1356+
if raw.info["meas_date"].year < 1985:
1357+
warn(
1358+
f"Attempting to write a {ext} file with a meas_date of "
1359+
f"{raw.info['meas_date']}. This is not supported; {ext} limits dates to "
1360+
"after 1985-01-01. Setting raw.info['meas_date'] to 1985-01-01."
1361+
)
1362+
raw.set_meas_date(raw.info["meas_date"].replace(year=1985, month=1, day=1))
13551363
raw.export(bids_fname, overwrite=overwrite)
13561364

13571365

0 commit comments

Comments
 (0)