diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 317860aad..dd6e37a13 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -28,6 +28,7 @@ The following authors had contributed before. Thank you for sticking around! * `Laetitia Fesselier`_ * `Richard Höchenberger`_ * `Stefan Appelhoff`_ +* `Daniel McCloy`_ Detailed list of changes ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -46,6 +47,8 @@ Detailed list of changes For example, If ``run=1`` is passed to MNE-BIDS, it will no longer be silently auto-converted to ``run-01``, by `Alex Rockhill`_ (:gh:`1215`) - MNE-BIDS will no longer warn about missing leading punctuation marks for extensions passed :class:`~mne_bids.BIDSPath`. For example, MNE-BIDS will now silently auto-convert ``edf`` to ```.edf``, by `Alex Rockhill`_ (:gh:`1215`) +- MNE-BIDS will no longer error during `~mne_bids.write_raw_bids` if there are ``BAD_ACQ_SKIP`` annotations in the raw file and no corresponding key in ``event_id``. + Instead, it will automatically add the necessary key and assign an unused integer event code to it. By `Daniel McCloy`_ (:gh:`1258`) 🛠 Requirements ^^^^^^^^^^^^^^^ diff --git a/mne_bids/read.py b/mne_bids/read.py index 45089d8fe..cf91f43d7 100644 --- a/mne_bids/read.py +++ b/mne_bids/read.py @@ -151,9 +151,18 @@ def _read_events(events, event_id, raw, bids_path=None): 'To specify custom event codes, please pass "event_id".' ) else: + special_annots = {"BAD_ACQ_SKIP"} desc_without_id = sorted( set(raw.annotations.description) - set(event_id.keys()) ) + # auto-add entries to `event_id` for "special" annotation values + # (but only if they're needed) + if set(desc_without_id) & special_annots: + for annot in special_annots: + # use a value guaranteed to not be in use + event_id = {annot: max(event_id.values()) + 90000} | event_id + # remove the "special" annots from the list of problematic annots + desc_without_id = sorted(set(desc_without_id) - special_annots) if desc_without_id: raise ValueError( f"The provided raw data contains annotations, but " diff --git a/mne_bids/tests/test_read.py b/mne_bids/tests/test_read.py index b5830bc78..560f71ac2 100644 --- a/mne_bids/tests/test_read.py +++ b/mne_bids/tests/test_read.py @@ -46,6 +46,15 @@ acq = "01" task = "testing" +sample_data_event_id = { + "Auditory/Left": 1, + "Auditory/Right": 2, + "Visual/Left": 3, + "Visual/Right": 4, + "Smiley": 5, + "Button": 32, +} + _bids_path = BIDSPath( subject=subject_id, session=session_id, run=run, acquisition=acq, task=task ) @@ -214,14 +223,6 @@ def test_get_head_mri_trans(tmp_path): """Test getting a trans object from BIDS data.""" nib = pytest.importorskip("nibabel") - event_id = { - "Auditory/Left": 1, - "Auditory/Right": 2, - "Visual/Left": 3, - "Visual/Right": 4, - "Smiley": 5, - "Button": 32, - } events_fname = op.join( data_path, "MEG", "sample", "sample_audvis_trunc_raw-eve.fif" ) @@ -234,7 +235,9 @@ def test_get_head_mri_trans(tmp_path): # Write it to BIDS raw = _read_raw_fif(raw_fname) bids_path = _bids_path.copy().update(root=tmp_path, datatype="meg", suffix="meg") - write_raw_bids(raw, bids_path, events=events, event_id=event_id, overwrite=False) + write_raw_bids( + raw, bids_path, events=events, event_id=sample_data_event_id, overwrite=False + ) # We cannot recover trans if no MRI has yet been written with pytest.raises(FileNotFoundError, match="Did not find"): @@ -300,7 +303,7 @@ def test_get_head_mri_trans(tmp_path): # sidecar, and also accept "nasion" instead of just "NAS" raw = _read_raw_fif(raw_fname) write_raw_bids( - raw, bids_path, events=events, event_id=event_id, overwrite=True + raw, bids_path, events=events, event_id=sample_data_event_id, overwrite=True ) # overwrite with new acq t1w_bids_path = write_anat( t1w_mgh, bids_path=t1w_bids_path, landmarks=landmarks, overwrite=True @@ -578,6 +581,27 @@ def test_keep_essential_annotations(tmp_path): assert raw_read.annotations[0]["description"] == raw.annotations[0]["description"] +@testing.requires_testing_data +def test_adding_essential_annotations_to_dict(tmp_path): + """Test that essential Annotations are auto-added to the `event_id` dictionary.""" + raw = _read_raw_fif(raw_fname) + annotations = mne.Annotations( + onset=[raw.times[0]], duration=[1], description=["BAD_ACQ_SKIP"] + ) + raw.set_annotations(annotations) + events = mne.find_events(raw) + event_id = sample_data_event_id.copy() + obj_id = id(event_id) + + # see that no error is raised for missing event_id key for BAD_ACQ_SKIP + bids_path = BIDSPath(subject="01", task="task", datatype="meg", root=tmp_path) + with pytest.warns(RuntimeWarning, match="Acquisition skips detected"): + write_raw_bids(raw, bids_path, overwrite=True, events=events, event_id=event_id) + # make sure we didn't modify the user-passed dict + assert event_id == sample_data_event_id + assert obj_id == id(event_id) + + @pytest.mark.filterwarnings(warning_str["channel_unit_changed"]) @testing.requires_testing_data def test_handle_scans_reading(tmp_path):