Skip to content

FIX: annotation onsets usable directly with get_data and plot#13893

Closed
SanskaarUndale21 wants to merge 3 commits intomne-tools:mainfrom
SanskaarUndale21:fix/annotation-onset-time-reference
Closed

FIX: annotation onsets usable directly with get_data and plot#13893
SanskaarUndale21 wants to merge 3 commits intomne-tools:mainfrom
SanskaarUndale21:fix/annotation-onset-time-reference

Conversation

@SanskaarUndale21
Copy link
Copy Markdown

What the problem is

When you read back raw.annotations[0]["onset"], the value you get is not directly usable with raw.get_data(tmin=...) or raw.plot(start=...). For files where first_samp != 0 (basically all FIFF files), the onset comes back with a first_time offset baked in, but get_data and plot both expect time in raw.times reference which starts at 0.

So if you do something like:

annot = mne.Annotations(onset=3, duration=1, description="test")
raw.set_annotations(annot)

ann = raw.annotations[0]
data = raw.get_data(tmin=ann["onset"], tmax=ann["onset"] + ann["duration"])

...you get data from completely the wrong part of the recording. For the sample dataset, ann["onset"] comes back as ~46 instead of 3, so get_data fetches from 46 seconds in rather than 3.

Fixes #13890.

Why it happens

set_annotations internally adds _first_time (= first_samp / sfreq) to stored onsets when orig_time is None, so annotations line up with meas_date. The problem is __getitem__ hands that raw stored value straight back to the user, with no adjustment. Meanwhile everything that uses annotations internally (_sync_onset, _annotations_starts_stops, the viz code) all already compensate for this offset. The user never gets a chance to.

Worth noting that crop_by_annotations already had a manual annot["onset"] - self.first_time correction in it, confirming this was a known mismatch.

What this PR does

Tags the stored Annotations object with _raw_first_time inside set_annotations, then subtracts it in Annotations.__getitem__ before returning to the caller. This means ann["onset"] comes back in raw.times reference, which is what every user-facing API expects.

  • raw.annotations[i]["onset"] now returns the onset in raw.times reference
  • Iterating over annotations gives the same corrected values (since __iter__ goes through __getitem__)
  • Sliced annotations propagate the tag so they stay consistent
  • Removed the now-redundant - self.first_time correction from crop_by_annotations
  • Fixed the chunk_duration branch of events_from_annotations which was building onsets from annot["onset"] and then passing them to time_as_index with the wrong origin
  • Fixed _mne_annots2pybv_events in BrainVision export which was manually subtracting first_time from annot["onset"] (would double-subtract with the new behavior)
  • Fixed EpochAnnotationsMixin.get_annotations_per_epoch to use the raw onset array directly when computing annotation position relative to epoch t_zero (since that calculation requires both sides in the same internal reference frame)

The internal annotations.onset array is untouched, so all the internal machinery keeps working exactly as before.

Testing

Ran the full annotation test suite (mne/tests/test_annotations.py) and the raw I/O test suite (mne/io/tests/test_raw.py) locally. Verified both test_chunk_duration cases with first_samp=10000 pass, and test_crop_by_annotations passes for all parameter combinations.

Also verified the exact reproduction case from the issue:

raw = mne.io.read_raw_fif(fname)  # first_samp=25800, first_time~=42.96s
annot = mne.Annotations(onset=3, duration=1, description="test")
raw.set_annotations(annot)

ann = raw.annotations[0]
print(ann["onset"])   # 3.0  (was ~45.96 before this fix)

data, times = raw.get_data(tmin=ann["onset"], tmax=ann["onset"] + ann["duration"], return_times=True)
print(times[0])       # ~3.0

Workaround until this lands

tmin = ann["onset"] - raw.first_time
data = raw.get_data(tmin=tmin, tmax=tmin + ann["duration"])

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 mne-tools#13890
- 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__)
- 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
Copilot AI review requested due to automatic review settings May 8, 2026 11:07
@welcome
Copy link
Copy Markdown

welcome Bot commented May 8, 2026

Hello! 👋 Thanks for opening your first pull request here! ❤️ We will try to get back to you soon. 🚴

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a long-standing time-reference mismatch where raw.annotations[i]["onset"] could not be used directly with user-facing APIs like raw.get_data(tmin=...) and raw.plot(start=...) when first_samp != 0, by returning onsets in raw.times (0-based-from-first_samp) reference.

Changes:

  • Tag Raw-attached Annotations with _raw_first_time in BaseRaw.set_annotations() and use it in Annotations.__getitem__ to return ann["onset"] in raw.times reference.
  • Remove now-redundant manual first_time corrections in callers (e.g., crop_by_annotations, BrainVision export) and adjust events_from_annotations (chunked events) and epoch-annotation alignment logic to use consistent internal vs public reference frames.
  • Add a changelog entry and contributor name.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
mne/io/base.py Tags annotations with _raw_first_time; updates crop_by_annotations onset handling.
mne/export/_brainvision.py Stops subtracting raw.first_time from per-annotation onset before time_as_index.
mne/annotations.py Adjusts Annotations.__getitem__ to return raw-times onsets; updates epoch-annotation and chunked-events computations to avoid double offsets.
doc/changes/names.inc Adds contributor reference for changelog attribution.
doc/changes/dev/13892.bugfix.rst Documents the bugfix in the development changelog.
Comments suppressed due to low confidence (1)

mne/annotations.py:524

  • Annotations.__getitem__ now subtracts _raw_first_time from the returned onset but still returns orig_time=self.orig_time. This makes the per-item contract inconsistent: the returned onset is no longer “seconds after orig_time” (per the class docstring), so users doing annot['orig_time'] + annot['onset'] will get the wrong absolute time. Consider also adjusting the returned orig_time (e.g., shift by _raw_first_time when not None) or otherwise ensure the onset/orig_time pair stays consistent.
            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] - _raw_ft,
                self.duration[key],
                self.description[key],
                self.orig_time,
            )

Comment thread mne/io/base.py
@SanskaarUndale21 SanskaarUndale21 deleted the fix/annotation-onset-time-reference branch May 8, 2026 11:38
@SanskaarUndale21 SanskaarUndale21 restored the fix/annotation-onset-time-reference branch May 8, 2026 11:38
@SanskaarUndale21 SanskaarUndale21 deleted the fix/annotation-onset-time-reference branch May 8, 2026 11:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Annotation times cannot be used to get corresponding data

2 participants