From 19cf8f34d6ac980855dbcb1552f9b35894d3e184 Mon Sep 17 00:00:00 2001 From: SanskaarUndale21 Date: Fri, 8 May 2026 15:30:45 +0530 Subject: [PATCH 1/3] FIX: annotation onsets now usable directly with get_data/plot raw.annotations[i]["onset"] previously returned time in meas_date-relative reference (including first_samp offset), but get_data(tmin=...) and plot(start=...) expect time in raw.times reference (0-indexed from first_samp). For FIFF files with first_samp != 0 this caused misaligned data extraction. Fix: tag annotations with _raw_first_time in set_annotations, then subtract it in Annotations.__getitem__ so the returned onset is always in raw.times reference. Slice access propagates the tag. crop_by_annotations no longer needs its manual - self.first_time correction. Also fix events_from_annotations chunk_duration branch which iterated over annot["onset"] and passed it to time_as_index with the wrong origin. Closes #13890 --- mne/annotations.py | 17 ++++++++++++++--- mne/io/base.py | 8 +++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/mne/annotations.py b/mne/annotations.py index c03e0610f28..2a8183d39d7 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -512,8 +512,12 @@ def __getitem__(self, key, *, with_ch_names=None, with_extras=True): """Propagate indexing and slicing to the underlying numpy structure.""" if isinstance(key, int_like): out_keys = ("onset", "duration", "description", "orig_time") + # When attached to a Raw object, subtract _raw_first_time so that + # ann["onset"] is in raw.times reference (usable directly with + # raw.get_data(tmin=...) and raw.plot(start=...)). + _raw_ft = getattr(self, "_raw_first_time", 0.0) out_vals = ( - self.onset[key], + self.onset[key] - _raw_ft, self.duration[key], self.description[key], self.orig_time, @@ -527,7 +531,7 @@ def __getitem__(self, key, *, with_ch_names=None, with_extras=True): return OrderedDict(zip(out_keys, out_vals)) else: key = list(key) if isinstance(key, tuple) else key - return Annotations( + result = Annotations( onset=self.onset[key], duration=self.duration[key], description=self.description[key], @@ -535,6 +539,11 @@ def __getitem__(self, key, *, with_ch_names=None, with_extras=True): ch_names=self.ch_names[key], extras=[self.extras[i] for i in np.arange(len(self.extras))[key]], ) + # Propagate the raw-times reference tag so that sliced annotations + # still return onsets in raw.times reference. + if hasattr(self, "_raw_first_time"): + result._raw_first_time = self._raw_first_time + return result @fill_doc def append(self, onset, duration, description, ch_names=None, *, extras=None): @@ -2370,8 +2379,10 @@ def events_from_annotations( good_events = annot_offset - _onsets >= chunk_duration - tol if good_events.any(): _onsets = _onsets[good_events] + # annot["onset"] is in raw.times reference (origin=None), + # not meas_date reference, so use origin=None here. _inds = raw.time_as_index( - _onsets, use_rounding=use_rounding, origin=annotations.orig_time + _onsets, use_rounding=use_rounding, origin=None ) _inds += raw.first_samp inds = np.append(inds, _inds) diff --git a/mne/io/base.py b/mne/io/base.py index 617ae66d302..479b102adec 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -765,6 +765,10 @@ def set_annotations( meas_date - new_annotations.orig_time ).total_seconds() new_annotations._orig_time = meas_date + # Tag with _first_time so the public onset accessor can return values + # in raw.times reference (0-indexed from first_samp) instead of the + # internal meas_date-relative reference. + new_annotations._raw_first_time = self._first_time self._annotations = new_annotations @@ -1730,7 +1734,9 @@ def crop_by_annotations(self, annotations=None, *, verbose=None): raws = [] for annot in annotations: - onset = annot["onset"] - self.first_time + # annot["onset"] is already in raw.times reference (0-indexed from + # first_samp) when the annotation comes from raw.annotations. + onset = annot["onset"] # be careful about near-zero errors (crop is very picky about this, # e.g., -1e-8 is an error) if -self.info["sfreq"] / 2 < onset < 0: From 770730badc05df005316157244fa48c47ce1a86b Mon Sep 17 00:00:00 2001 From: SanskaarUndale21 Date: Fri, 8 May 2026 15:45:11 +0530 Subject: [PATCH 2/3] FIX: Address consistency issues in annotation onset handling - Remove double-correction in _brainvision.py: annot["onset"] now returns raw.times-relative value, so subtracting raw.first_time was applying the offset twice - Fix _raw_first_time comment in set_annotations to match the actual attribute name being set - Make Annotations.append() aware of _raw_first_time so callers can pass onsets in raw.times reference (consistent with __getitem__) --- mne/annotations.py | 3 ++- mne/export/_brainvision.py | 2 +- mne/io/base.py | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/mne/annotations.py b/mne/annotations.py index 2a8183d39d7..7e133201d31 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -582,7 +582,8 @@ def append(self, onset, duration, description, ch_names=None, *, extras=None): onset, duration, description, ch_names, extras = _check_o_d_s_c_e( onset, duration, description, ch_names, extras ) - self.onset = np.append(self.onset, onset) + _raw_ft = getattr(self, "_raw_first_time", 0.0) + self.onset = np.append(self.onset, onset + _raw_ft) self.duration = np.append(self.duration, duration) self.description = np.append(self.description, description) self.ch_names = np.append(self.ch_names, ch_names) diff --git a/mne/export/_brainvision.py b/mne/export/_brainvision.py index 6503c540f41..2fb2becc1b5 100644 --- a/mne/export/_brainvision.py +++ b/mne/export/_brainvision.py @@ -118,7 +118,7 @@ def _mne_annots2pybv_events(raw): for annot in raw.annotations: # handle onset and duration: seconds to sample, relative to # raw.first_samp / raw.first_time - onset = annot["onset"] - raw.first_time + onset = annot["onset"] onset = raw.time_as_index(onset).astype(int)[0] duration = int(annot["duration"] * raw.info["sfreq"]) diff --git a/mne/io/base.py b/mne/io/base.py index 479b102adec..0245f70093a 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -765,9 +765,9 @@ def set_annotations( meas_date - new_annotations.orig_time ).total_seconds() new_annotations._orig_time = meas_date - # Tag with _first_time so the public onset accessor can return values - # in raw.times reference (0-indexed from first_samp) instead of the - # internal meas_date-relative reference. + # Tag with _raw_first_time so the public onset accessor can return + # values in raw.times reference (0-indexed from first_samp) instead + # of the internal meas_date-relative reference. new_annotations._raw_first_time = self._first_time self._annotations = new_annotations From 5517870a406f0ec6f6923e5cac588daa472a81fb Mon Sep 17 00:00:00 2001 From: SanskaarUndale21 Date: Fri, 8 May 2026 16:36:57 +0530 Subject: [PATCH 3/3] FIX: Fix append() revert and guard get_annotations_per_epoch - Revert Annotations.append() change: the previous commit incorrectly added _raw_first_time to stored onsets, breaking tests that already pass meas_date-relative values to append() - Fix EpochAnnotationsMixin.get_annotations_per_epoch: it computed onset relative to epoch t_zero using annot["onset"] (now in raw.times reference) subtracted from epoch_tzeros (meas_date-relative), causing a _raw_first_time-sized offset. Use the raw onset array directly to keep both sides in the same reference frame. - Add towncrier changelog entry for this fix - Add Sanskaar Undale to contributor list --- doc/changes/dev/13892.bugfix.rst | 1 + doc/changes/names.inc | 1 + mne/annotations.py | 9 ++++++--- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 doc/changes/dev/13892.bugfix.rst diff --git a/doc/changes/dev/13892.bugfix.rst b/doc/changes/dev/13892.bugfix.rst new file mode 100644 index 00000000000..2d295837cb6 --- /dev/null +++ b/doc/changes/dev/13892.bugfix.rst @@ -0,0 +1 @@ +Fix :meth:`mne.Annotations.__getitem__` (i.e., ``raw.annotations[i]["onset"]``) to return onset times in ``raw.times`` reference (0-indexed from ``first_samp``) instead of ``meas_date``-relative time, making the value directly usable with :meth:`mne.io.BaseRaw.get_data` and :meth:`mne.io.BaseRaw.plot`, by `Sanskaar Undale`_. diff --git a/doc/changes/names.inc b/doc/changes/names.inc index e20fad63acb..aca1242c3bf 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -301,6 +301,7 @@ .. _Samuel Deslauriers-Gauthier: https://github.com/sdeslauriers .. _Samuel Louviot: https://github.com/Sam54000 .. _Samuel Powell: https://github.com/samuelpowell +.. _Sanskaar Undale: https://github.com/SanskaarUndale21 .. _Santeri Ruuskanen: https://github.com/ruuskas .. _Santi Martínez: https://github.com/szz-dvl .. _Sara Sommariva: https://github.com/sarasommariva diff --git a/mne/annotations.py b/mne/annotations.py index 7e133201d31..6a97bfff99e 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -582,8 +582,7 @@ def append(self, onset, duration, description, ch_names=None, *, extras=None): onset, duration, description, ch_names, extras = _check_o_d_s_c_e( onset, duration, description, ch_names, extras ) - _raw_ft = getattr(self, "_raw_first_time", 0.0) - self.onset = np.append(self.onset, onset + _raw_ft) + self.onset = np.append(self.onset, onset) self.duration = np.append(self.duration, duration) self.description = np.append(self.description, description) self.ch_names = np.append(self.ch_names, ch_names) @@ -1507,11 +1506,15 @@ def get_annotations_per_epoch(self, *, with_extras=False): # for each Epoch-Annotation overlap occurrence: for annot_ix, epo_ix in zip(*np.nonzero(all_cases)): + # Use the raw onset array (internal reference) so the relative + # onset calculation stays consistent with epoch_tzeros, which is + # also in the internal (meas_date-relative) reference frame. + this_onset = self._annotations.onset[annot_ix] this_annot = self._annotations[annot_ix] this_tzero = epoch_tzeros[epo_ix] # adjust annotation onset to be relative to epoch tzero... annot = ( - this_annot["onset"] - this_tzero, + this_onset - this_tzero, this_annot["duration"], this_annot["description"], )