@@ -527,89 +527,76 @@ def _handle_info_reading(sidecar_fname, raw):
527527
528528
529529def _handle_events_reading (events_fname , raw ):
530- """Read associated events.tsv and populate raw.
531-
532- Handle onset, duration, and description of each event.
533- """
530+ """Read associated events.tsv and convert valid events to annotations on Raw."""
534531 logger .info (f"Reading events from { events_fname } ." )
535532 events_dict = _from_tsv (events_fname )
536533
537- # Get the descriptions of the events
534+ # drop events where onset is n/a
535+ events_dict = _drop (events_dict , "n/a" , "onset" )
536+
537+ # Get event descriptions. Use `trial_type` column if available.
538538 if "trial_type" in events_dict :
539539 trial_type_col_name = "trial_type"
540- elif "stim_type" in events_dict : # Backward-compat with old datasets.
540+ # allow `stim_type` for backward-compat with old datasets.
541+ elif "stim_type" in events_dict :
541542 trial_type_col_name = "stim_type"
542543 warn (
543- f'The events file, { events_fname } , contains a "stim_type" '
544- f'column. This column should be renamed to "trial_type" for '
545- f"BIDS compatibility."
544+ f'The events file, { events_fname } , contains a "stim_type" column. This '
545+ 'column should be renamed to "trial_type" for BIDS compatibility.'
546546 )
547+ # If we lack proper event descriptions, perhaps we have at least an event value?
548+ elif "value" in events_dict :
549+ trial_type_col_name = "value"
550+ # Worst case: all events will become `n/a` and all values will be `1`
547551 else :
548552 trial_type_col_name = None
549553
550554 if trial_type_col_name is not None :
551555 # Drop events unrelated to a trial type
552556 events_dict = _drop (events_dict , "n/a" , trial_type_col_name )
553-
557+ trial_types = events_dict [trial_type_col_name ]
558+ # handle event values (if provided); ensure pairings are 1 value per description
554559 if "value" in events_dict :
555- # Check whether the `trial_type` <> `value` mapping is unique.
556- trial_types = events_dict [trial_type_col_name ]
557560 values = np .asarray (events_dict ["value" ], dtype = str )
558561 for trial_type in np .unique (trial_types ):
559562 idx = np .where (trial_type == np .atleast_1d (trial_types ))[0 ]
560563 matching_values = values [idx ]
561-
562564 if len (np .unique (matching_values )) > 1 :
563- # Event type descriptors are ambiguous; create hierarchical
564- # event descriptors.
565+ # Event type descriptors are ambiguous; create hierarchical event
566+ # descriptors (to ensure trial_type -> integerID is 1:1)
565567 logger .info (
566- f'The event "{ trial_type } " refers to multiple event '
567- f"values. Creating hierarchical event names."
568+ f'The event "{ trial_type } " refers to multiple event values. '
569+ " Creating hierarchical event names."
568570 )
569571 for ii in idx :
570572 value = values [ii ]
571573 value = "na" if value == "n/a" else value
572574 new_name = f"{ trial_type } /{ value } "
573- logger .info (
574- f" Renaming event: { trial_type } -> " f"{ new_name } "
575- )
575+ logger .info (f" Renaming event: { trial_type } -> { new_name } " )
576576 trial_types [ii ] = new_name
577- descriptions = np .asarray (trial_types , dtype = str )
577+ # drop rows where `value` is `n/a` & convert remaining `value` to int (only
578+ # when making our `event_id` dict; `value = n/a` doesn't prevent annotation)
579+ culled = _drop (events_dict , "n/a" , "value" )
580+ event_id = dict (
581+ zip (culled [trial_type_col_name ], np .asarray (culled ["value" ], dtype = int ))
582+ )
578583 else :
579- descriptions = np .asarray (events_dict [trial_type_col_name ], dtype = str )
580- elif "value" in events_dict :
581- # If we don't have a proper description of the events, perhaps we have
582- # at least an event value?
583- # Drop events unrelated to value
584- events_dict = _drop (events_dict , "n/a" , "value" )
585- descriptions = np .asarray (events_dict ["value" ], dtype = str )
584+ event_id = dict (zip (trial_types , np .arange (len (trial_types ))))
585+ descrs = np .asarray (trial_types , dtype = str )
586586
587- # Worst case, we go with ' n/a' for all events
587+ # Worst case: all events become ` n/a` and all values become `1`
588588 else :
589- descriptions = np .array ([ "n/a" ] * len (events_dict ["onset" ]), dtype = str )
590-
589+ descrs = np .full ( len (events_dict ["onset" ]), "n/a" )
590+ event_id = { descrs [ 0 ]: 1 }
591591 # Deal with "n/a" strings before converting to float
592- onsets = np .array (
593- [np .nan if on == "n/a" else on for on in events_dict ["onset" ]], dtype = float
594- )
595- durations = np .array (
592+ ons = np .asarray (events_dict ["onset" ], dtype = float )
593+ durs = np .array (
596594 [0 if du == "n/a" else du for du in events_dict ["duration" ]], dtype = float
597595 )
598596
599- # Keep only events where onset is known
600- good_events_idx = ~ np .isnan (onsets )
601- onsets = onsets [good_events_idx ]
602- durations = durations [good_events_idx ]
603- descriptions = descriptions [good_events_idx ]
604- del good_events_idx
605-
606- # Add events as Annotations, but keep essential Annotations present in
607- # raw file
597+ # Add events as Annotations, but keep essential Annotations present in raw file
608598 annot_from_raw = raw .annotations .copy ()
609-
610- annot_from_events = mne .Annotations (
611- onset = onsets , duration = durations , description = descriptions
612- )
599+ annot_from_events = mne .Annotations (onset = ons , duration = durs , description = descrs )
613600 raw .set_annotations (annot_from_events )
614601
615602 annot_idx_to_keep = [
@@ -622,7 +609,7 @@ def _handle_events_reading(events_fname, raw):
622609 if len (annot_to_keep ):
623610 raw .set_annotations (raw .annotations + annot_to_keep )
624611
625- return raw
612+ return raw , event_id
626613
627614
628615def _get_bads_from_tsv_data (tsv_data ):
@@ -756,7 +743,9 @@ def _handle_channels_reading(channels_fname, raw):
756743
757744
758745@verbose
759- def read_raw_bids (bids_path , extra_params = None , verbose = None ):
746+ def read_raw_bids (
747+ bids_path , extra_params = None , * , return_event_dict = False , verbose = None
748+ ):
760749 """Read BIDS compatible data.
761750
762751 Will attempt to read associated events.tsv and channels.tsv files to
@@ -781,12 +770,21 @@ def read_raw_bids(bids_path, extra_params=None, verbose=None):
781770 Note that the ``exclude`` parameter, which is supported by some
782771 MNE-Python readers, is not supported; instead, you need to subset
783772 your channels **after** reading.
773+ return_event_dict : bool
774+ Whether to return a dictionary that maps annotation descriptions to integer
775+ event IDs, in addition to the :class:`~mne.io.Raw` object. If a ``value`` column
776+ is present in the ``*_events.tsv`` file, it will be used as the source of the
777+ integer event ID values (events with ``value="n/a"`` will be omitted).
784778 %(verbose)s
785779
786780 Returns
787781 -------
788782 raw : mne.io.Raw
789783 The data as MNE-Python Raw object.
784+ event_id : dict
785+ A mapping from event descriptions to integer event IDs, suitable for,
786+ e.g., passing to :func:`mne.events_from_annotations`. Only returned if
787+ ``return_event_dict=True``.
790788
791789 Raises
792790 ------
@@ -923,9 +921,8 @@ def read_raw_bids(bids_path, extra_params=None, verbose=None):
923921 events_fname = _find_matching_sidecar (
924922 bids_path , suffix = "events" , extension = ".tsv" , on_error = on_error
925923 )
926-
927924 if events_fname is not None :
928- raw = _handle_events_reading (events_fname , raw )
925+ raw , event_id = _handle_events_reading (events_fname , raw )
929926
930927 # Try to find an associated channels.tsv to get information about the
931928 # status and type of present channels
@@ -989,6 +986,8 @@ def read_raw_bids(bids_path, extra_params=None, verbose=None):
989986 raw .info ["subject_info" ] = dict ()
990987
991988 assert raw .annotations .orig_time == raw .info ["meas_date" ]
989+ if return_event_dict :
990+ return raw , event_id
992991 return raw
993992
994993
0 commit comments