From 18999ca3d374b7f2338e60dd13658695649950d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Sun, 21 Aug 2022 11:11:51 +0200 Subject: [PATCH] Deprecate write_raw_bids()'s events_data parameter in favor of "events" (#1054) * Deprecate write_raw_bids()'s events_data parameter in favor of "events" For more consistency with MNE-Python. This is WIP. WDYT? * Fix a few tests * Fix examples * Update changelog * Additional changelog entry * Fix CLI * Better backward-compat * Fix CLI * Add test * Fix test * Raise ValueError instead of RuntimeError * Schedule removal for MNE-BIDS 0.14 --- doc/whats_new.rst | 6 + examples/anonymize_dataset.py | 2 +- examples/convert_empty_room.py | 3 +- examples/convert_mne_sample.py | 6 +- examples/convert_mri_and_trans.py | 4 +- examples/mark_bad_channels.py | 2 +- mne_bids/commands/mne_bids_raw_to_bids.py | 7 +- mne_bids/commands/tests/test_cli.py | 4 +- mne_bids/inspect.py | 2 +- mne_bids/read.py | 26 ++--- .../tiny_bids/code/make_tiny_bids_dataset.py | 2 +- mne_bids/tests/test_inspect.py | 2 +- mne_bids/tests/test_path.py | 2 +- mne_bids/tests/test_read.py | 4 +- mne_bids/tests/test_stats.py | 4 +- mne_bids/tests/test_update.py | 2 +- mne_bids/tests/test_write.py | 97 +++++++++++------ mne_bids/write.py | 103 ++++++++++++------ 18 files changed, 174 insertions(+), 104 deletions(-) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index afefb24b0..69cdf94fe 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -54,6 +54,10 @@ Detailed list of changes 🧐 API and behavior changes ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +- :func:`~mne_bids.write_raw_bids` now expects all but the first four parameters to be passed as keyword arguments, by `Richard Höchenberger`_ (:gh:`1054`) + +- The ``events_data`` parameter of :func:`~mne_bids.write_raw_bids` has been deprecated in favor of a new parameter named ``events``. This ensures more consistency between the MNE-BIDS and MNE-Python APIs. You may continue using the ``events_data`` parameter for now, but a ``FutureWarning`` will be raised. ``events_data`` will be removed in MNE-BIDS 0.14, by `Richard Höchenberger`_ (:gh:`1054`) + - In many places, we used to infer the ``datatype`` of a :class:`~mne_bids.BIDSPath` from the ``suffix``, if not explicitly provided. However, this has lead to trouble in certain edge cases. In an effort to reduce the amount of implicit behavior in MNE-BIDS, we now require users to explicitly specify a ``datatype`` whenever the invoked functions or methods expect one, by `Richard Höchenberger`_ (:gh:`1030`) - :func:`mne_bids.make_dataset_description` now accepts keyword arguments only, and can now also write the following metadata: ``HEDVersion``, ``EthicsApprovals``, ``GeneratedBy``, and ``SourceDatasets``, by `Stefan Appelhoff`_ (:gh:`406`) @@ -62,6 +66,8 @@ Detailed list of changes - :func:`mne_bids.print_dir_tree` now raises a :py:class:`FileNotFoundError` instead of a :py:class:`ValueError` if the directory does not exist, by `Richard Höchenberger`_ (:gh:`1013`) +- Passing only one of ``events`` and ``event_id`` to :func:`~mne_bids.write_raw_bids` now raises a ``ValueError`` instead of a ``RuntimeError``, by `Richard Höchenberger`_ (:gh:`1054`) + 🛠 Requirements ^^^^^^^^^^^^^^^ diff --git a/examples/anonymize_dataset.py b/examples/anonymize_dataset.py index 30a300e95..814589116 100644 --- a/examples/anonymize_dataset.py +++ b/examples/anonymize_dataset.py @@ -75,7 +75,7 @@ # Write experimental MEG data, fine-calibration and crosstalk files write_raw_bids( - raw=raw, bids_path=bids_path, events_data=events_path, event_id=event_id, + raw=raw, bids_path=bids_path, events=events_path, event_id=event_id, empty_room=bids_path_er, verbose=False ) write_meg_calibration(cal_path, bids_path=bids_path, verbose=False) diff --git a/examples/convert_empty_room.py b/examples/convert_empty_room.py index 85c0f3329..0b95802e9 100644 --- a/examples/convert_empty_room.py +++ b/examples/convert_empty_room.py @@ -59,8 +59,7 @@ shutil.rmtree(bids_root) # %% -# Specify the raw_file and events_data and run the BIDS conversion, and write -# the BIDS data. +# Specify the raw file and write the BIDS data. raw = mne.io.read_raw_fif(raw_fname) raw.info['line_freq'] = 60 # specify power line frequency as required by BIDS diff --git a/examples/convert_mne_sample.py b/examples/convert_mne_sample.py index 2bcbeea26..3bc1045bf 100644 --- a/examples/convert_mne_sample.py +++ b/examples/convert_mne_sample.py @@ -44,7 +44,7 @@ # Now we can read the MNE sample data. We define an `event_id` based on our # knowledge of the data, to give meaning to events in the data. # -# With `raw_fname` and `events_data`, we determine where to get the sample data +# With `raw_fname` and `events`, we determine where to get the sample data # from. `output_path` determines where we will write the BIDS conversion to. data_path = sample.data_path() @@ -53,7 +53,7 @@ raw_fname = op.join(data_path, 'MEG', 'sample', 'sample_audvis_raw.fif') er_fname = op.join(data_path, 'MEG', 'sample', 'ernoise_raw.fif') # empty room -events_data = op.join(data_path, 'MEG', 'sample', 'sample_audvis_raw-eve.fif') +events_fname = op.join(data_path, 'MEG', 'sample', 'sample_audvis_raw-eve.fif') output_path = op.join(data_path, '..', 'MNE-sample-data-bids') # %% @@ -99,7 +99,7 @@ write_raw_bids( raw=raw, bids_path=bids_path, - events_data=events_data, + events=events_fname, event_id=event_id, empty_room=raw_er, overwrite=True diff --git a/examples/convert_mri_and_trans.py b/examples/convert_mri_and_trans.py index 0038e4504..10bd21d36 100644 --- a/examples/convert_mri_and_trans.py +++ b/examples/convert_mri_and_trans.py @@ -61,7 +61,7 @@ event_id = {'Auditory/Left': 1, 'Auditory/Right': 2, 'Visual/Left': 3, 'Visual/Right': 4, 'Smiley': 5, 'Button': 32} raw_fname = op.join(data_path, 'MEG', 'sample', 'sample_audvis_raw.fif') -events_data = op.join(data_path, 'MEG', 'sample', 'sample_audvis_raw-eve.fif') +events_fname = op.join(data_path, 'MEG', 'sample', 'sample_audvis_raw-eve.fif') output_path = op.abspath(op.join(data_path, '..', 'MNE-sample-data-bids')) fs_subjects_dir = op.join(data_path, 'subjects') # FreeSurfer subjects dir @@ -87,7 +87,7 @@ run = '01' bids_path = BIDSPath(subject=sub, session=ses, task=task, run=run, root=output_path) -write_raw_bids(raw, bids_path, events_data=events_data, +write_raw_bids(raw, bids_path, events=events_fname, event_id=event_id, overwrite=True) # %% diff --git a/examples/mark_bad_channels.py b/examples/mark_bad_channels.py index 02860d0a5..24ece4cfa 100644 --- a/examples/mark_bad_channels.py +++ b/examples/mark_bad_channels.py @@ -57,7 +57,7 @@ raw = mne.io.read_raw_fif(raw_fname, verbose=False) raw.info['line_freq'] = 60 # Specify power line frequency as required by BIDS. -write_raw_bids(raw, bids_path=bids_path, events_data=events_fname, +write_raw_bids(raw, bids_path=bids_path, events=events_fname, event_id=event_id, overwrite=True, verbose=False) # %% diff --git a/mne_bids/commands/mne_bids_raw_to_bids.py b/mne_bids/commands/mne_bids_raw_to_bids.py index 32daa1f41..fdd83c115 100644 --- a/mne_bids/commands/mne_bids_raw_to_bids.py +++ b/mne_bids/commands/mne_bids_raw_to_bids.py @@ -37,6 +37,8 @@ def run(): parser.add_option('--acq', dest='acq', help='acquisition parameter for this dataset') parser.add_option('--events_data', dest='events_data', + help='Deprecated. Pass --events instead.') + parser.add_option('--events', dest='events', help='events file (events.tsv)') parser.add_option('--event_id', dest='event_id', help='event id dict', metavar='eid') @@ -80,9 +82,10 @@ def run(): if opt.line_freq is not None: line_freq = None if opt.line_freq == "None" else opt.line_freq raw.info['line_freq'] = line_freq + write_raw_bids(raw, bids_path, event_id=opt.event_id, - events_data=opt.events_data, overwrite=opt.overwrite, - verbose=True) + events=opt.events, overwrite=opt.overwrite, + events_data=opt.events_data, verbose=True) if __name__ == '__main__': diff --git a/mne_bids/commands/tests/test_cli.py b/mne_bids/commands/tests/test_cli.py index 3e60c4763..ee16b6e90 100644 --- a/mne_bids/commands/tests/test_cli.py +++ b/mne_bids/commands/tests/test_cli.py @@ -257,8 +257,8 @@ def test_count_events(tmp_path): 'visual/right': 4, 'face': 5, 'button': 32} bids_path = BIDSPath(subject='01', root=output_path, task='foo') - write_raw_bids(raw, bids_path, events, event_id, overwrite=True, - verbose=False) + write_raw_bids(raw, bids_path, events=events, event_id=event_id, + overwrite=True, verbose=False) with ArgvSetter(('--bids_root', output_path)): mne_bids_count_events.run() diff --git a/mne_bids/inspect.py b/mne_bids/inspect.py index 0c6fec6b7..6ef2a8d9c 100644 --- a/mne_bids/inspect.py +++ b/mne_bids/inspect.py @@ -239,7 +239,7 @@ def _save_annotations(*, annotations, bids_path): raw = read_raw_bids(bids_path=bids_path, extra_params=extra_params, verbose='warning') raw.set_annotations(annotations) - events, durs, descrs = _read_events(events_data=None, event_id=None, + events, durs, descrs = _read_events(events=None, event_id=None, bids_path=bids_path, raw=raw) # Write sidecar – or remove it if no events are left. diff --git a/mne_bids/read.py b/mne_bids/read.py index ab55b2399..1192781dd 100644 --- a/mne_bids/read.py +++ b/mne_bids/read.py @@ -74,12 +74,12 @@ def _read_raw(raw_path, electrode=None, hsp=None, hpi=None, return raw -def _read_events(events_data, event_id, raw, bids_path=None): +def _read_events(events, event_id, raw, bids_path=None): """Retrieve events (for use in *_events.tsv) from FIFF/array & Annotations. Parameters ---------- - events_data : path-like | np.ndarray | None + events : path-like | np.ndarray | None If a string, a path to an events file. If an array, an MNE events array (shape n_events, 3). If None, events will be generated from ``raw.annotations``. @@ -107,19 +107,19 @@ def _read_events(events_data, event_id, raw, bids_path=None): the values to the event IDs. """ - # get events from events_data - if isinstance(events_data, np.ndarray): - if events_data.ndim != 2: + # retrieve events + if isinstance(events, np.ndarray): + if events.ndim != 2: raise ValueError('Events must have two dimensions, ' - f'found {events_data.ndim}') - if events_data.shape[1] != 3: + f'found {events.ndim}') + if events.shape[1] != 3: raise ValueError('Events must have second dimension of length 3, ' - f'found {events_data.shape[1]}') - events = events_data - elif events_data is None: + f'found {events.shape[1]}') + events = events + elif events is None: events = np.empty(shape=(0, 3), dtype=int) else: - events = read_events(events_data).astype(int) + events = read_events(events).astype(int) if events.size > 0: # Only keep events for which we have an ID <> description mapping. @@ -129,7 +129,7 @@ def _read_events(events_data, event_id, raw, bids_path=None): f'No description was specified for the following event(s): ' f'{", ".join([str(x) for x in sorted(ids_without_desc)])}. ' f'Please add them to the event_id dictionary, or drop them ' - f'from the events_data array.' + f'from the events array.' ) del ids_without_desc mask = [e in list(event_id.values()) for e in events[:, 2]] @@ -175,7 +175,7 @@ def _read_events(events_data, event_id, raw, bids_path=None): ) ): warn('No events found or provided. Please add annotations to the raw ' - 'data, or provide the events_data and event_id parameters. For ' + 'data, or provide the events and event_id parameters. For ' 'resting state data, BIDS recommends naming the task using ' 'labels beginning with "rest".') diff --git a/mne_bids/tests/data/tiny_bids/code/make_tiny_bids_dataset.py b/mne_bids/tests/data/tiny_bids/code/make_tiny_bids_dataset.py index f16b1f752..90a25a060 100644 --- a/mne_bids/tests/data/tiny_bids/code/make_tiny_bids_dataset.py +++ b/mne_bids/tests/data/tiny_bids/code/make_tiny_bids_dataset.py @@ -46,7 +46,7 @@ # %% write_raw_bids( - raw, bids_path, events_data=events, event_id=event_id, overwrite=True + raw, bids_path, events=events, event_id=event_id, overwrite=True ) # %% diff --git a/mne_bids/tests/test_inspect.py b/mne_bids/tests/test_inspect.py index 484e09cee..4e094d29a 100644 --- a/mne_bids/tests/test_inspect.py +++ b/mne_bids/tests/test_inspect.py @@ -47,7 +47,7 @@ def setup_bids_test_dir(bids_root): events = events[events[:, 2] != 0] bids_path = _bids_path.copy().update(root=bids_root) - write_raw_bids(raw, bids_path=bids_path, events_data=events, + write_raw_bids(raw, bids_path=bids_path, events=events, event_id=event_id, overwrite=True) write_meg_calibration(cal_fname, bids_path=bids_path) write_meg_crosstalk(crosstalk_fname, bids_path=bids_path) diff --git a/mne_bids/tests/test_path.py b/mne_bids/tests/test_path.py index e4d0bda37..5a63c153a 100644 --- a/mne_bids/tests/test_path.py +++ b/mne_bids/tests/test_path.py @@ -64,7 +64,7 @@ def return_bids_test_dir(tmp_path_factory): # Write multiple runs for test_purposes for run_idx in [run, '02']: name = bids_path.copy().update(run=run_idx) - write_raw_bids(raw, name, events_data=events, + write_raw_bids(raw, name, events=events, event_id=event_id, overwrite=True) write_meg_calibration(cal_fname, bids_path=bids_path) diff --git a/mne_bids/tests/test_read.py b/mne_bids/tests/test_read.py index ea29968fc..3867733cb 100644 --- a/mne_bids/tests/test_read.py +++ b/mne_bids/tests/test_read.py @@ -229,7 +229,7 @@ def test_get_head_mri_trans(tmp_path): bids_path = _bids_path.copy().update( root=tmp_path, datatype='meg', suffix='meg' ) - write_raw_bids(raw, bids_path, events_data=events, event_id=event_id, + write_raw_bids(raw, bids_path, events=events, event_id=event_id, overwrite=False) # We cannot recover trans if no MRI has yet been written @@ -293,7 +293,7 @@ def test_get_head_mri_trans(tmp_path): # test we are permissive for different casings of landmark names in the # sidecar, and also accept "nasion" instead of just "NAS" raw = _read_raw_fif(raw_fname) - write_raw_bids(raw, bids_path, events_data=events, event_id=event_id, + write_raw_bids(raw, bids_path, events=events, event_id=event_id, overwrite=True) # overwrite with new acq t1w_bids_path = write_anat( t1w_mgh, bids_path=t1w_bids_path, landmarks=landmarks, overwrite=True diff --git a/mne_bids/tests/test_stats.py b/mne_bids/tests/test_stats.py index c65324187..d6a3dc577 100644 --- a/mne_bids/tests/test_stats.py +++ b/mne_bids/tests/test_stats.py @@ -36,8 +36,8 @@ def _make_dataset(root, subjects, tasks=(None,), runs=(None,), bids_path = BIDSPath( subject=subject, session=session, run=run, task=task, root=root, ) - write_raw_bids(raw, bids_path, events, event_id, overwrite=True, - verbose=False) + write_raw_bids(raw, bids_path, events=events, event_id=event_id, + overwrite=True, verbose=False) return root, events, event_id diff --git a/mne_bids/tests/test_update.py b/mne_bids/tests/test_update.py index 59f78cfd6..5e424c34b 100644 --- a/mne_bids/tests/test_update.py +++ b/mne_bids/tests/test_update.py @@ -58,7 +58,7 @@ def _get_bids_test_dir(tmp_path_factory): # Write multiple runs for test_purposes for run_idx in [run, '02']: name = bids_path.copy().update(run=run_idx) - write_raw_bids(raw, name, events_data=events, + write_raw_bids(raw, name, events=events, event_id=event_id, overwrite=True) write_meg_calibration(cal_fname, bids_path=bids_path) diff --git a/mne_bids/tests/test_write.py b/mne_bids/tests/test_write.py index 48837f725..48ee0880e 100644 --- a/mne_bids/tests/test_write.py +++ b/mne_bids/tests/test_write.py @@ -148,7 +148,7 @@ def _test_anonymize(root, raw, bids_path, events_fname=None, event_id=None): else: # just pass back any arbitrary number if no measurement date daysback = 3300 - write_raw_bids(raw, bids_path, events_data=events_fname, + write_raw_bids(raw, bids_path, events=events_fname, event_id=event_id, anonymize=dict(daysback=daysback), overwrite=False) scans_tsv = BIDSPath( @@ -482,7 +482,7 @@ def test_fif(_bids_validate, tmp_path): events = events[events[:, 2] != 0] raw = _read_raw_fif(raw_fname) - write_raw_bids(raw, bids_path, events_data=events, event_id=event_id, + write_raw_bids(raw, bids_path, events=events, event_id=event_id, overwrite=False) # Read the file back in to check that the data has come through cleanly. @@ -527,7 +527,7 @@ def test_fif(_bids_validate, tmp_path): with pytest.warns(RuntimeWarning, match='Converting data files to BrainVision format'): write_raw_bids(raw2, bids_path, - events_data=events, event_id=event_id, + events=events, event_id=event_id, verbose=True, overwrite=False) bids_dir = op.join(bids_root, 'sub-%s' % subject_id, 'ses-%s' % session_id, 'eeg') @@ -581,13 +581,13 @@ def test_fif(_bids_validate, tmp_path): # give the raw object some fake participant data (potentially overwriting) raw = _read_raw_fif(raw_fname) bids_path_meg = bids_path.copy().update(datatype='meg') - write_raw_bids(raw, bids_path_meg, events_data=events, + write_raw_bids(raw, bids_path_meg, events=events, event_id=event_id, overwrite=True) # try and write preloaded data raw = _read_raw_fif(raw_fname, preload=True) with pytest.raises(ValueError, match='allow_preload'): - write_raw_bids(raw, bids_path_meg, events_data=events, + write_raw_bids(raw, bids_path_meg, events=events, event_id=event_id, allow_preload=False, overwrite=False) # test anonymize @@ -606,7 +606,7 @@ def test_fif(_bids_validate, tmp_path): bids_path2 = bids_path_meg.copy().update(subject=subject_id2) raw = _read_raw_fif(raw_fname2) bids_output_path = write_raw_bids(raw, bids_path2, - events_data=events, + events=events, event_id=event_id, overwrite=False) # check that the overwrite parameters work correctly for the participant @@ -617,7 +617,7 @@ def test_fif(_bids_validate, tmp_path): 'birthday': (1994, 1, 26), 'sex': 2, 'hand': 1} with pytest.raises(FileExistsError, match="already exists"): # noqa: F821 write_raw_bids(raw, bids_path2, - events_data=events, event_id=event_id, overwrite=False) + events=events, event_id=event_id, overwrite=False) # assert README has references in it with open(readme, 'r', encoding='utf-8-sig') as fid: @@ -629,7 +629,7 @@ def test_fif(_bids_validate, tmp_path): assert REFERENCES['ieeg'] not in text # now force the overwrite - write_raw_bids(raw, bids_path2, events_data=events, event_id=event_id, + write_raw_bids(raw, bids_path2, events=events, event_id=event_id, overwrite=True) with open(readme, 'r', encoding='utf-8-sig') as fid: @@ -701,7 +701,7 @@ def test_fif(_bids_validate, tmp_path): with raw_no_extra_points.info._unlock(): raw_no_extra_points.info['dig'] = new_dig - write_raw_bids(raw_no_extra_points, bids_path, events_data=events, + write_raw_bids(raw_no_extra_points, bids_path, events=events, event_id=event_id, overwrite=True) meg_json_path = Path( @@ -816,27 +816,27 @@ def test_fif_anonymize(_bids_validate, tmp_path): # test keyword mne-bids anonymize raw = _read_raw_fif(raw_fname) with pytest.raises(ValueError, match='`daysback` argument required'): - write_raw_bids(raw, bids_path, events_data=events, event_id=event_id, + write_raw_bids(raw, bids_path, events=events, event_id=event_id, anonymize=dict(), overwrite=True) bids_root = tmp_path / 'bids2' bids_path.update(root=bids_root) raw = _read_raw_fif(raw_fname) with pytest.warns(RuntimeWarning, match='daysback` is too small'): - write_raw_bids(raw, bids_path, events_data=events, event_id=event_id, + write_raw_bids(raw, bids_path, events=events, event_id=event_id, anonymize=dict(daysback=400), overwrite=False) bids_root = tmp_path / 'bids3' bids_path.update(root=bids_root) raw = _read_raw_fif(raw_fname) with pytest.raises(ValueError, match='`daysback` exceeds maximum value'): - write_raw_bids(raw, bids_path, events_data=events, event_id=event_id, + write_raw_bids(raw, bids_path, events=events, event_id=event_id, anonymize=dict(daysback=40000), overwrite=False) bids_root = tmp_path / 'bids4' bids_path.update(root=bids_root) raw = _read_raw_fif(raw_fname) - write_raw_bids(raw, bids_path, events_data=events, event_id=event_id, + write_raw_bids(raw, bids_path, events=events, event_id=event_id, anonymize=dict(daysback=30000, keep_his=True), overwrite=False) scans_tsv = BIDSPath( @@ -907,7 +907,7 @@ def test_kit(_bids_validate, tmp_path): raw_fname, mrk=hpi_fname, elp=electrode_fname, hsp=headshape_fname) write_raw_bids(raw, kit_bids_path, - events_data=events_fname, + events=events_fname, event_id=event_id, overwrite=False) _bids_validate(bids_root) @@ -944,19 +944,19 @@ def test_kit(_bids_validate, tmp_path): other_output_path = tmp_path / 'tmp2' bids_path = _bids_path.copy().update(root=other_output_path) with pytest.raises(ValueError, match='two dimensions'): - write_raw_bids(raw, bids_path, events_data=event_data_3d, + write_raw_bids(raw, bids_path, events=event_data_3d, event_id=event_id, overwrite=True) # remove 3rd column event_data = event_data[:, :2] with pytest.raises(ValueError, match='second dimension'): - write_raw_bids(raw, bids_path, events_data=event_data, + write_raw_bids(raw, bids_path, events=event_data, event_id=event_id, overwrite=True) # test correct naming of marker files raw = _read_raw_kit( raw_fname, mrk=[hpi_pre_fname, hpi_post_fname], elp=electrode_fname, hsp=headshape_fname) kit_bids_path.update(subject=subject_id2) - write_raw_bids(raw, kit_bids_path, events_data=events_fname, + write_raw_bids(raw, kit_bids_path, events=events_fname, event_id=event_id, overwrite=False) _bids_validate(bids_root) @@ -976,7 +976,7 @@ def test_kit(_bids_validate, tmp_path): hsp=headshape_fname) with pytest.raises(ValueError, match='Markers'): write_raw_bids(raw, kit_bids_path.update(subject=subject_id2), - events_data=events_fname, event_id=event_id, + events=events_fname, event_id=event_id, overwrite=True) # check that everything works with MRK markers, and CON files @@ -1249,12 +1249,12 @@ def test_eegieeg(dir_name, fname, reader, _bids_validate, tmp_path): event_id = {'Auditory/Left': 1, 'Auditory/Right': 2, 'Visual/Left': 3, 'Visual/Right': 4, 'Smiley': 5, 'Button': 32} - with pytest.raises(RuntimeError, - match='You passed events_data, but no event_id '): - write_raw_bids(raw, bids_path, events_data=events) + with pytest.raises(ValueError, + match='You passed events, but no event_id '): + write_raw_bids(raw, bids_path, events=events) - with pytest.raises(RuntimeError, - match='You passed event_id, but no events_data'): + with pytest.raises(ValueError, + match='You passed event_id, but no events'): write_raw_bids(raw, bids_path, event_id=event_id) # check events.tsv is written @@ -2033,7 +2033,7 @@ def test_write_anat(_bids_validate, tmp_path): raw = _read_raw_fif(raw_fname) bids_path = _bids_path.copy().update(root=bids_root) - write_raw_bids(raw, bids_path, events_data=events, event_id=event_id, + write_raw_bids(raw, bids_path, events=events, event_id=event_id, overwrite=False) # define some keyword arguments to simplify testing @@ -2179,7 +2179,7 @@ def test_write_raw_pathlike(tmp_path): 'sample_audvis_trunc_raw-eve.fif') bids_path = _bids_path.copy().update(root=bids_root) bids_path_ = write_raw_bids(raw=raw, bids_path=bids_path, - events_data=events_fname, + events=events_fname, event_id=event_id, overwrite=False) # write_raw_bids() should return a string. @@ -2254,7 +2254,7 @@ def test_write_does_not_alter_events_inplace(tmp_path): bids_path = _bids_path.copy().update(root=tmp_path) write_raw_bids(raw=raw, bids_path=bids_path, - events_data=events, event_id=event_id, overwrite=True) + events=events, event_id=event_id, overwrite=True) assert np.array_equal(events, events_orig) @@ -2313,7 +2313,7 @@ def test_mark_channels(_bids_validate, raw = _read_raw_fif(raw_fname, verbose=False) raw.info['bads'] = [] - write_raw_bids(raw, bids_path=bids_path, events_data=events, + write_raw_bids(raw, bids_path=bids_path, events=events, event_id=event_id, verbose=False) channels_fname = _find_matching_sidecar(bids_path, suffix='channels', @@ -2405,7 +2405,7 @@ def test_mark_channel_roundtrip(tmp_path): events = events[events[:, 2] != 0] raw = _read_raw_fif(raw_fname, verbose=False) - write_raw_bids(raw, bids_path=bids_path, events_data=events, + write_raw_bids(raw, bids_path=bids_path, events=events, event_id=event_id, verbose=False) channels_fname = _find_matching_sidecar(bids_path, suffix='channels', extension='.tsv') @@ -2451,7 +2451,7 @@ def test_error_mark_channels(tmp_path): events = events[events[:, 2] != 0] raw = _read_raw_fif(raw_fname, verbose=False) - write_raw_bids(raw, bids_path=bids_path, events_data=events, + write_raw_bids(raw, bids_path=bids_path, events=events, event_id=event_id, verbose=False) ch_names = raw.ch_names @@ -2632,7 +2632,7 @@ def test_annotations(_bids_validate, bad_segments, tmp_path): del bad_annots raw.set_annotations(annotations) - write_raw_bids(raw, bids_path, events_data=None, event_id=None, + write_raw_bids(raw, bids_path, events=None, event_id=None, overwrite=False) annotations_read = read_raw_bids(bids_path=bids_path).annotations @@ -2670,7 +2670,7 @@ def test_undescribed_events(_bids_validate, drop_undescribed_events, tmp_path): raw = _read_raw_fif(raw_fname) raw.set_annotations(None) # Make sure it's clean. - kwargs = dict(raw=raw, bids_path=bids_path, events_data=events, + kwargs = dict(raw=raw, bids_path=bids_path, events=events, event_id=event_id, overwrite=False) if not drop_undescribed_events: @@ -2713,7 +2713,7 @@ def test_event_storage(tmp_path): 'Visual/Right': 4, 'Smiley': 5, 'Button': 32} raw = _read_raw_fif(raw_fname) - write_raw_bids(raw=raw, bids_path=bids_path, events_data=events, + write_raw_bids(raw=raw, bids_path=bids_path, events=events, event_id=event_id, overwrite=False) events_tsv = _from_tsv(events_tsv_fname) @@ -3497,7 +3497,7 @@ def test_anonymize_dataset(_bids_validate, tmpdir): write_raw_bids(raw_er, bids_path=bids_path_er) write_raw_bids( raw, bids_path=bids_path, empty_room=bids_path_er, - events_data=events_path, event_id=event_id, verbose=False + events=events_path, event_id=event_id, verbose=False ) write_meg_crosstalk( fname=crosstalk_path, bids_path=bids_path, verbose=False @@ -3776,3 +3776,34 @@ def test_repeat_write_location(tmpdir): # Re-writing with src == dest should error with pytest.raises(FileExistsError, match='Desired output BIDSPath'): write_raw_bids(raw, bids_path, overwrite=True, verbose=False) + + +def test_events_data_deprecation(tmp_path): + """Test that passing events_data raises a FutureWarning.""" + bids_root = tmp_path / 'bids' + bids_path = _bids_path.copy().update(root=bids_root) + data_path = testing.data_path() + raw_path = data_path / 'MEG' / 'sample' / 'sample_audvis_trunc_raw.fif' + events_path = (data_path / 'MEG' / 'sample' / + 'sample_audvis_trunc_raw-eve.fif') + event_id = {'Auditory/Left': 1, 'Auditory/Right': 2, 'Visual/Left': 3, + 'Visual/Right': 4, 'Smiley': 5, 'Button': 32} + + # Drop unknown events. + events = mne.read_events(events_path) + events = events[events[:, 2] != 0] + + raw = _read_raw_fif(raw_path) + with pytest.warns(FutureWarning, match='will be removed'): + write_raw_bids( + raw=raw, bids_path=bids_path, events_data=events, event_id=event_id + ) + + with pytest.raises( + ValueError, + match='Only one of events and events_data can be passed' + ): + write_raw_bids( + raw=raw, bids_path=bids_path, events=events, events_data=events, + event_id=event_id + ) diff --git a/mne_bids/write.py b/mne_bids/write.py index 7aab6c7bd..255de3dd3 100644 --- a/mne_bids/write.py +++ b/mne_bids/write.py @@ -1244,11 +1244,23 @@ def make_dataset_description(*, path, name, hed_version=None, @verbose -def write_raw_bids(raw, bids_path, events_data=None, event_id=None, - anonymize=None, format='auto', symlink=False, - empty_room=None, allow_preload=False, - montage=None, acpc_aligned=False, - overwrite=False, verbose=None): +def write_raw_bids( + raw, + bids_path, + events=None, + event_id=None, + *, + anonymize=None, + format='auto', + symlink=False, + empty_room=None, + allow_preload=False, + montage=None, + acpc_aligned=False, + overwrite=False, + events_data=None, + verbose=None +): """Save raw data to a BIDS-compliant folder structure. .. warning:: * The original file is simply copied over if the original @@ -1288,7 +1300,7 @@ def write_raw_bids(raw, bids_path, events_data=None, event_id=None, sub-01_ses-01_task-testing_acq-01_run-01_channels.tsv sub-01_ses-01_acq-01_coordsystem.json - and the following one if ``events_data`` is not ``None``:: + and the following one if ``events`` is not ``None``:: sub-01_ses-01_task-testing_acq-01_run-01_events.tsv @@ -1299,37 +1311,37 @@ def write_raw_bids(raw, bids_path, events_data=None, event_id=None, Note that the extension is automatically inferred from the raw object. - events_data : path-like | np.ndarray | None + events : path-like | np.ndarray | None Use this parameter to specify events to write to the ``*_events.tsv`` - sidecar file, additionally to the object's `mne.Annotations` (which - are always written). - If a path, specifies the location of an MNE events file. + sidecar file, additionally to the object's :class:`~mne.Annotations` + (which are always written). + If ``path-like``, specifies the location of an MNE events file. If an array, the MNE events array (shape: ``(n_events, 3)``). If a path or an array and ``raw.annotations`` exist, the union of - ``event_data`` and ``raw.annotations`` will be written. - Corresponding descriptions for all event IDs (listed in the third + ``events`` and ``raw.annotations`` will be written. + Corresponding descriptions for all event codes (listed in the third column of the MNE events array) must be specified via the ``event_id`` parameter; otherwise, an exception is raised. - If ``None``, events will only be inferred from the the raw object's - `mne.Annotations`. + If ``None``, events will only be inferred from the raw object's + :class:`~mne.Annotations`. .. note:: - If ``not None``, writes the union of ``events_data`` and + If ``not None``, writes the union of ``events`` and ``raw.annotations``. If you wish to **only** write - ``raw.annotations``, pass ``events_data=None``. If you want to + ``raw.annotations``, pass ``events=None``. If you want to **exclude** the events in ``raw.annotations`` from being written, call ``raw.set_annotations(None)`` before invoking this function. .. note:: - Descriptions of all event IDs must be specified via the ``event_id`` - parameter. + Descriptions of all event codes must be specified via the + ``event_id`` parameter. event_id : dict | None - Descriptions of all event IDs, if you passed ``events_data``. - The descriptions will be written to the ``trial_type`` column in - ``*_events.tsv``. The dictionary keys correspond to the event - descriptions and the values to the event IDs. You must specify a - description for all event IDs in ``events_data``. + Descriptions or names describing the event codes, if you passed + ``events``. The descriptions will be written to the ``trial_type`` + column in ``*_events.tsv``. The dictionary keys correspond to the event + description,s and the values to the event codes. You must specify a + description for all event codes appearing in ``events``. anonymize : dict | None If `None` (default), no anonymization is performed. If a dictionary, data will be anonymized depending on the dictionary @@ -1424,6 +1436,10 @@ def write_raw_bids(raw, bids_path, events_data=None, event_id=None, and ``participants.tsv`` by a user will be retained. If ``False``, no existing data will be overwritten or replaced. + events_data + .. deprecated:: 0.11 + Use ``events`` instead. + %(verbose)s Returns @@ -1445,7 +1461,7 @@ def write_raw_bids(raw, bids_path, events_data=None, event_id=None, This function will convert existing `mne.Annotations` from ``raw.annotations`` to events. Additionally, any events supplied via - ``events_data`` will be written too. To avoid writing of annotations, + ``events`` will be written too. To avoid writing of annotations, remove them from the raw file via ``raw.set_annotations(None)`` before invoking ``write_raw_bids``. @@ -1454,7 +1470,7 @@ def write_raw_bids(raw, bids_path, events_data=None, event_id=None, .. events = mne.find_events(raw, min_duration=0.002) - write_raw_bids(..., events_data=events) + write_raw_bids(..., events=events) See the documentation of :func:`mne.find_events` for more information on event extraction from ``STIM`` channels. @@ -1483,6 +1499,21 @@ def write_raw_bids(raw, bids_path, events_data=None, event_id=None, mne.events_from_annotations """ + if events_data is not None and events is not None: + raise ValueError('Only one of events and events_data can be passed.') + + if events_data is not None: + warn( + message='The events_data parameter has been deprecated in favor ' + 'the new events parameter, to ensure better consistency ' + 'with MNE-Python. The events_data parameter will be ' + 'removed in MNE-BIDS 0.14. Please use the events ' + 'parameter instead.', + category=FutureWarning + ) + events = events_data + del events_data + if not isinstance(raw, BaseRaw): raise ValueError('raw_file must be an instance of BaseRaw, ' 'got %s' % type(raw)) @@ -1495,8 +1526,8 @@ def write_raw_bids(raw, bids_path, events_data=None, event_id=None, raise RuntimeError('"bids_path" must be a BIDSPath object. Please ' 'instantiate using mne_bids.BIDSPath().') - _validate_type(events_data, types=('path-like', np.ndarray, None), - item_name='events_data', + _validate_type(events, types=('path-like', np.ndarray, None), + item_name='events', type_name='path-like, NumPy array, or None') if symlink and sys.platform in ('win32', 'cygwin'): @@ -1524,13 +1555,13 @@ def write_raw_bids(raw, bids_path, events_data=None, event_id=None, '"bids_path.task = "' ) - if events_data is not None and event_id is None: - raise RuntimeError('You passed events_data, but no event_id ' - 'dictionary. You need to pass both, or neither.') + if events is not None and event_id is None: + raise ValueError('You passed events, but no event_id ' + 'dictionary. You need to pass both, or neither.') - if event_id is not None and events_data is None: - raise RuntimeError('You passed event_id, but no events_data NumPy ' - 'array. You need to pass both, or neither.') + if event_id is not None and events is None: + raise ValueError('You passed event_id, but no events. ' + 'You need to pass both, or neither.') _validate_type(item=empty_room, item_name='empty_room', types=(mne.io.BaseRaw, BIDSPath, None)) @@ -1648,7 +1679,7 @@ def write_raw_bids(raw, bids_path, events_data=None, event_id=None, write_raw_bids( raw=empty_room, bids_path=er_bids_path, - events_data=None, + events=None, event_id=None, anonymize=anonymize, format=format, @@ -1784,13 +1815,13 @@ def write_raw_bids(raw, bids_path, events_data=None, event_id=None, # Write events. if not data_is_emptyroom: events_array, event_dur, event_desc_id_map = _read_events( - events_data, event_id, raw, bids_path=bids_path) + events, event_id, raw, bids_path=bids_path) if events_array.size != 0: _events_tsv(events=events_array, durations=event_dur, raw=raw, fname=events_path.fpath, trial_type=event_desc_id_map, overwrite=overwrite) # Kepp events_array around for BrainVision writing below. - del event_desc_id_map, events_data, event_id, event_dur + del event_desc_id_map, events, event_id, event_dur # make dataset description and add template data if it does not # already exist. Always set overwrite to False here. If users