From 0f70809e7388efeb2555eb356baefceabeb04e8c Mon Sep 17 00:00:00 2001 From: Famous Date: Tue, 24 Feb 2026 19:35:27 +0530 Subject: [PATCH 01/25] DOC: document onset, duration, description, ch_names attrs of Annotations --- doc/changes/dev/12379.other.rst | 1 + mne/annotations.py | 80 +++++++++++++++++++++++++++++++-- 2 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 doc/changes/dev/12379.other.rst diff --git a/doc/changes/dev/12379.other.rst b/doc/changes/dev/12379.other.rst new file mode 100644 index 00000000000..ce6a11f15a9 --- /dev/null +++ b/doc/changes/dev/12379.other.rst @@ -0,0 +1 @@ +Document :attr:`~mne.Annotations.onset`, :attr:`~mne.Annotations.duration`, :attr:`~mne.Annotations.description`, and :attr:`~mne.Annotations.ch_names` attributes of :class:`mne.Annotations`, by `Famous Raj Bhat`_. \ No newline at end of file diff --git a/mne/annotations.py b/mne/annotations.py index e298e80918c..678791b1347 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -405,15 +405,89 @@ def __init__( f"' '. Got: {orig_time}. Defaulting `orig_time` to None.", RuntimeWarning, ) - self.onset, self.duration, self.description, self.ch_names, self._extras = ( - _check_o_d_s_c_e(onset, duration, description, ch_names, extras) - ) + self._onset, self._duration, self._description, self._ch_names, self._extras = ( + _check_o_d_s_c_e(onset, duration, description, ch_names, extras) +) self._sort() # ensure we're sorted @property def orig_time(self): """The time base of the Annotations.""" return self._orig_time + + @property + def onset(self): + """Onset of each annotation (in seconds). + + Returns + ------- + onset : array of shape (n_annotations,) + The onset of each annotation in seconds from the start of + the recording. + + See Also + -------- + duration, description + """ + return self._onset + + @onset.setter + def onset(self, onset): + self._onset = onset + + @property + def duration(self): + """Duration of each annotation (in seconds). + + Returns + ------- + duration : array of shape (n_annotations,) + The duration of each annotation in seconds. + + See Also + -------- + onset, description + """ + return self._duration + + @duration.setter + def duration(self, duration): + self._duration = duration + + @property + def description(self): + """Description of each annotation. + + Returns + ------- + description : array of shape (n_annotations,) + A string description for each annotation (e.g., event + label or condition name). + + See Also + -------- + onset, duration + """ + return self._description + + @description.setter + def description(self, description): + self._description = description + + @property + def ch_names(self): + """Channel names associated with each annotation. + + Returns + ------- + ch_names : list of tuple + Channel names associated with each annotation. + """ + return self._ch_names + + @ch_names.setter + def ch_names(self, ch_names): + self._ch_names = ch_names @property def extras(self): From 31c138e42ed327b21124014222874fbd2584c231 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 14:11:11 +0000 Subject: [PATCH 02/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mne/annotations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mne/annotations.py b/mne/annotations.py index 678791b1347..9989bb5dd0d 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -406,15 +406,15 @@ def __init__( RuntimeWarning, ) self._onset, self._duration, self._description, self._ch_names, self._extras = ( - _check_o_d_s_c_e(onset, duration, description, ch_names, extras) -) + _check_o_d_s_c_e(onset, duration, description, ch_names, extras) + ) self._sort() # ensure we're sorted @property def orig_time(self): """The time base of the Annotations.""" return self._orig_time - + @property def onset(self): """Onset of each annotation (in seconds). From 6cef5a60d9755d829c4b0aab1e6f84c075f1fa57 Mon Sep 17 00:00:00 2001 From: Famous Date: Tue, 24 Feb 2026 23:43:26 +0530 Subject: [PATCH 03/25] DOC: trigger CircleCI build From 0de9d786971df146d8ade9d5fb08da523fa655f1 Mon Sep 17 00:00:00 2001 From: Famous Date: Wed, 25 Feb 2026 00:00:23 +0530 Subject: [PATCH 04/25] DOC: add validation to onset, duration, description setters --- mne/annotations.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/mne/annotations.py b/mne/annotations.py index 9989bb5dd0d..60c58876e5c 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -433,8 +433,48 @@ def onset(self): @onset.setter def onset(self, onset): + onset = np.atleast_1d(np.array(onset, dtype=float)) + if len(onset) != len(self._duration): + raise ValueError( + f"onset length ({len(onset)}) must match " + f"duration length ({len(self._duration)})." + ) self._onset = onset + @property + def duration(self): + """Duration of each annotation (in seconds). + ... + """ + return self._duration + + @duration.setter + def duration(self, duration): + duration = np.atleast_1d(np.array(duration, dtype=float)) + if len(duration) != len(self._onset): + raise ValueError( + f"duration length ({len(duration)}) must match " + f"onset length ({len(self._onset)})." + ) + self._duration = duration + + @property + def description(self): + """Description of each annotation. + ... + """ + return self._description + + @description.setter + def description(self, description): + description = np.atleast_1d(np.array(description, dtype=str)) + if len(description) != len(self._onset): + raise ValueError( + f"description length ({len(description)}) must match " + f"onset length ({len(self._onset)})." + ) + self._description = description + @property def duration(self): """Duration of each annotation (in seconds). From bd5e6ab12efcc34adb0a77f4f1c6b4f22828b6ec Mon Sep 17 00:00:00 2001 From: Famous Date: Wed, 25 Feb 2026 12:07:28 +0530 Subject: [PATCH 05/25] DOC: remove length validation from setters to avoid internal conflicts --- mne/annotations.py | 42 +----------------------------------------- 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/mne/annotations.py b/mne/annotations.py index 60c58876e5c..c014108d075 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -433,47 +433,7 @@ def onset(self): @onset.setter def onset(self, onset): - onset = np.atleast_1d(np.array(onset, dtype=float)) - if len(onset) != len(self._duration): - raise ValueError( - f"onset length ({len(onset)}) must match " - f"duration length ({len(self._duration)})." - ) - self._onset = onset - - @property - def duration(self): - """Duration of each annotation (in seconds). - ... - """ - return self._duration - - @duration.setter - def duration(self, duration): - duration = np.atleast_1d(np.array(duration, dtype=float)) - if len(duration) != len(self._onset): - raise ValueError( - f"duration length ({len(duration)}) must match " - f"onset length ({len(self._onset)})." - ) - self._duration = duration - - @property - def description(self): - """Description of each annotation. - ... - """ - return self._description - - @description.setter - def description(self, description): - description = np.atleast_1d(np.array(description, dtype=str)) - if len(description) != len(self._onset): - raise ValueError( - f"description length ({len(description)}) must match " - f"onset length ({len(self._onset)})." - ) - self._description = description + self._onset = np.atleast_1d(np.array(onset, dtype=float)) @property def duration(self): From 1edf047007fd27b77b1eba2edd58cd172cca8dfc Mon Sep 17 00:00:00 2001 From: Famous Date: Wed, 25 Feb 2026 13:24:23 +0530 Subject: [PATCH 06/25] DOC: fix contributor name format in changelog --- doc/changes/dev/12379.other.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/dev/12379.other.rst b/doc/changes/dev/12379.other.rst index ce6a11f15a9..eb16cb5e4a7 100644 --- a/doc/changes/dev/12379.other.rst +++ b/doc/changes/dev/12379.other.rst @@ -1 +1 @@ -Document :attr:`~mne.Annotations.onset`, :attr:`~mne.Annotations.duration`, :attr:`~mne.Annotations.description`, and :attr:`~mne.Annotations.ch_names` attributes of :class:`mne.Annotations`, by `Famous Raj Bhat`_. \ No newline at end of file +Document :attr:`~mne.Annotations.onset`, :attr:`~mne.Annotations.duration`, :attr:`~mne.Annotations.description`, and :attr:`~mne.Annotations.ch_names` attributes of :class:`mne.Annotations`, by `Famous077 `__. \ No newline at end of file From 5b59b5602a1a6b36316653cb907f11cb9328c5f4 Mon Sep 17 00:00:00 2001 From: Famous Date: Fri, 6 Mar 2026 19:47:14 +0530 Subject: [PATCH 07/25] DOC: fix changelog filename and add Famous077 to names.inc --- doc/changes/dev/{12379.other.rst => 13680.other.rst} | 2 +- doc/changes/names.inc | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) rename doc/changes/dev/{12379.other.rst => 13680.other.rst} (80%) diff --git a/doc/changes/dev/12379.other.rst b/doc/changes/dev/13680.other.rst similarity index 80% rename from doc/changes/dev/12379.other.rst rename to doc/changes/dev/13680.other.rst index eb16cb5e4a7..df680ad643d 100644 --- a/doc/changes/dev/12379.other.rst +++ b/doc/changes/dev/13680.other.rst @@ -1 +1 @@ -Document :attr:`~mne.Annotations.onset`, :attr:`~mne.Annotations.duration`, :attr:`~mne.Annotations.description`, and :attr:`~mne.Annotations.ch_names` attributes of :class:`mne.Annotations`, by `Famous077 `__. \ No newline at end of file +Document :attr:`~mne.Annotations.onset`, :attr:`~mne.Annotations.duration`, :attr:`~mne.Annotations.description`, and :attr:`~mne.Annotations.ch_names` attributes of :class:`mne.Annotations`, by `Famous077`_. diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 76b3e7534df..e1381489b94 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -102,6 +102,7 @@ .. _Federico Zamberlan: https://github.com/fzamberlan .. _Felix Klotzsche: https://github.com/eioe .. _Felix Raimundo: https://github.com/gamazeps +.. _Famous077: https://github.com/Famous077 .. _Florian Hofer: https://github.com/hofaflo .. _Florin Pop: https://github.com/florin-pop .. _Frederik Weber: https://github.com/Frederik-D-Weber From 430e40832202a1eafdae71701f0d3d30f6bc4648 Mon Sep 17 00:00:00 2001 From: Famous Date: Fri, 6 Mar 2026 19:49:51 +0530 Subject: [PATCH 08/25] DOC: remove duplicate Famous Raj Bhat entry from names.inc --- doc/changes/names.inc | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/changes/names.inc b/doc/changes/names.inc index e1381489b94..910ee6082af 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -96,7 +96,6 @@ .. _Ezequiel Mikulan: https://github.com/ezemikulan .. _Ezequiel Mikulan: https://github.com/ezemikulan .. _Fahimeh Mamashli: https://github.com/fmamashli -.. _Famous Raj Bhat: https://github.com/Famous077 .. _Farzin Negahbani: https://github.com/Farzin-Negahbani .. _Federico Raimondo: https://github.com/fraimondo .. _Federico Zamberlan: https://github.com/fzamberlan From 74f44f6d45dfd95278bbe63e4f58e4d3b8212c10 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:20:13 +0000 Subject: [PATCH 09/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- doc/changes/names.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 910ee6082af..2bcf3fd53ac 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -96,12 +96,12 @@ .. _Ezequiel Mikulan: https://github.com/ezemikulan .. _Ezequiel Mikulan: https://github.com/ezemikulan .. _Fahimeh Mamashli: https://github.com/fmamashli +.. _Famous077: https://github.com/Famous077 .. _Farzin Negahbani: https://github.com/Farzin-Negahbani .. _Federico Raimondo: https://github.com/fraimondo .. _Federico Zamberlan: https://github.com/fzamberlan .. _Felix Klotzsche: https://github.com/eioe .. _Felix Raimundo: https://github.com/gamazeps -.. _Famous077: https://github.com/Famous077 .. _Florian Hofer: https://github.com/hofaflo .. _Florin Pop: https://github.com/florin-pop .. _Frederik Weber: https://github.com/Frederik-D-Weber From 05e6f663bd2eb10fe483d7338734050da62f0196 Mon Sep 17 00:00:00 2001 From: Famous Date: Thu, 19 Mar 2026 13:48:23 +0530 Subject: [PATCH 10/25] DOC: fix setter validation for duration/description and fix contributor name --- doc/changes/dev/13680.other.rst | 2 +- doc/changes/names.inc | 2 +- mne/annotations.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/changes/dev/13680.other.rst b/doc/changes/dev/13680.other.rst index df680ad643d..fa8de9c10c6 100644 --- a/doc/changes/dev/13680.other.rst +++ b/doc/changes/dev/13680.other.rst @@ -1 +1 @@ -Document :attr:`~mne.Annotations.onset`, :attr:`~mne.Annotations.duration`, :attr:`~mne.Annotations.description`, and :attr:`~mne.Annotations.ch_names` attributes of :class:`mne.Annotations`, by `Famous077`_. +Document :attr:`~mne.Annotations.onset`, :attr:`~mne.Annotations.duration`, :attr:`~mne.Annotations.description`, and :attr:`~mne.Annotations.ch_names` attributes of :class:`mne.Annotations`, by :newcontrib:`Famous Raj Bhat`. \ No newline at end of file diff --git a/doc/changes/names.inc b/doc/changes/names.inc index ac53955a57f..4e6f21a1308 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -97,12 +97,12 @@ .. _Ezequiel Mikulan: https://github.com/ezemikulan .. _Ezequiel Mikulan: https://github.com/ezemikulan .. _Fahimeh Mamashli: https://github.com/fmamashli -.. _Famous077: https://github.com/Famous077 .. _Farzin Negahbani: https://github.com/Farzin-Negahbani .. _Federico Raimondo: https://github.com/fraimondo .. _Federico Zamberlan: https://github.com/fzamberlan .. _Felix Klotzsche: https://github.com/eioe .. _Felix Raimundo: https://github.com/gamazeps +.. _Famous Raj Bhat: https://github.com/Famous077 .. _Florian Hofer: https://github.com/hofaflo .. _Florin Pop: https://github.com/florin-pop .. _Frederik Weber: https://github.com/Frederik-D-Weber diff --git a/mne/annotations.py b/mne/annotations.py index 9c396dd2f5f..b826fcd5205 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -455,7 +455,7 @@ def duration(self): @duration.setter def duration(self, duration): - self._duration = duration + self._duration = np.atleast_1d(np.array(duration, dtype=float)) @property def description(self): @@ -475,7 +475,7 @@ def description(self): @description.setter def description(self, description): - self._description = description + self._description = np.atleast_1d(np.array(description, dtype=str)) @property def ch_names(self): From a014f91a066248c31df2c1ba55bd4d8f80e3bf5d Mon Sep 17 00:00:00 2001 From: Famous Date: Thu, 19 Mar 2026 14:09:42 +0530 Subject: [PATCH 11/25] DOC: revert contributor name to Famous077 for consistency --- doc/changes/dev/13680.other.rst | 2 +- doc/changes/names.inc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/changes/dev/13680.other.rst b/doc/changes/dev/13680.other.rst index fa8de9c10c6..affeff62c03 100644 --- a/doc/changes/dev/13680.other.rst +++ b/doc/changes/dev/13680.other.rst @@ -1 +1 @@ -Document :attr:`~mne.Annotations.onset`, :attr:`~mne.Annotations.duration`, :attr:`~mne.Annotations.description`, and :attr:`~mne.Annotations.ch_names` attributes of :class:`mne.Annotations`, by :newcontrib:`Famous Raj Bhat`. \ No newline at end of file +Document :attr:`~mne.Annotations.onset`, :attr:`~mne.Annotations.duration`, :attr:`~mne.Annotations.description`, and :attr:`~mne.Annotations.ch_names` attributes of :class:`mne.Annotations`, by `Famous077`_. \ No newline at end of file diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 4e6f21a1308..a6cf3c5e87c 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -102,7 +102,7 @@ .. _Federico Zamberlan: https://github.com/fzamberlan .. _Felix Klotzsche: https://github.com/eioe .. _Felix Raimundo: https://github.com/gamazeps -.. _Famous Raj Bhat: https://github.com/Famous077 +.. _Famous077: https://github.com/Famous077 .. _Florian Hofer: https://github.com/hofaflo .. _Florin Pop: https://github.com/florin-pop .. _Frederik Weber: https://github.com/Frederik-D-Weber From b20ab9fee9fda04c995b485abafb551fae22e854 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 08:42:35 +0000 Subject: [PATCH 12/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- doc/changes/names.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 865e19f9563..7f40c33d5fa 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -96,12 +96,12 @@ .. _Evgeny Goldstein: https://github.com/evgenygoldstein .. _Ezequiel Mikulan: https://github.com/ezemikulan .. _Fahimeh Mamashli: https://github.com/fmamashli +.. _Famous077: https://github.com/Famous077 .. _Farzin Negahbani: https://github.com/Farzin-Negahbani .. _Federico Raimondo: https://github.com/fraimondo .. _Federico Zamberlan: https://github.com/fzamberlan .. _Felix Klotzsche: https://github.com/eioe .. _Felix Raimundo: https://github.com/gamazeps -.. _Famous077: https://github.com/Famous077 .. _Florian Hofer: https://github.com/hofaflo .. _Florin Pop: https://github.com/florin-pop .. _Frederik Weber: https://github.com/Frederik-D-Weber From 58816a0b9dde9dcac72885285e425ac22331cf85 Mon Sep 17 00:00:00 2001 From: Famous Date: Thu, 19 Mar 2026 22:17:43 +0530 Subject: [PATCH 13/25] ENH: validate array lengths in onset/duration/description/ch_names setters --- mne/annotations.py | 106 ++++++++++++++++++++++++---------- mne/tests/test_annotations.py | 26 +++++++++ 2 files changed, 103 insertions(+), 29 deletions(-) diff --git a/mne/annotations.py b/mne/annotations.py index 552fcbc776c..f2d611ace9e 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -158,41 +158,62 @@ def _validate_extras(extras, length: int): return _AnnotationsExtrasList(extras or [None] * length) -def _check_o_d_s_c_e(onset, duration, description, ch_names, extras): +def _check_onset(onset): + """Convert and validate onset to a 1D float array.""" onset = np.atleast_1d(np.array(onset, dtype=float)) if onset.ndim != 1: raise ValueError( f"Onset must be a one dimensional array, got {onset.ndim} (shape " f"{onset.shape})." ) + return onset + + +def _check_duration(duration, n): + """Convert and validate duration to a 1D float array of length n.""" duration = np.array(duration, dtype=float) if duration.ndim == 0 or duration.shape == (1,): - duration = np.repeat(duration, len(onset)) + duration = np.repeat(duration, n) if duration.ndim != 1: raise ValueError( f"Duration must be a one dimensional array, got {duration.ndim}." ) + return duration + +def _check_description(description, n): + """Convert and validate description to a 1D str array of length n.""" description = np.array(description, dtype=str) if description.ndim == 0 or description.shape == (1,): - description = np.repeat(description, len(onset)) + description = np.repeat(description, n) if description.ndim != 1: raise ValueError( f"Description must be a one dimensional array, got {description.ndim}." ) _safe_name_list(description, "write", "description") + return description - # ch_names: convert to ndarray of tuples + +def _check_ch_names_annot(ch_names, n): + """Convert and validate ch_names to an ndarray of tuples of length n.""" _validate_type(ch_names, (None, tuple, list, np.ndarray), "ch_names") if ch_names is None: - ch_names = [()] * len(onset) + ch_names = [()] * n ch_names = list(ch_names) for ai, ch in enumerate(ch_names): _validate_type(ch, (list, tuple, np.ndarray), f"ch_names[{ai}]") ch_names[ai] = tuple(ch) for ci, name in enumerate(ch_names[ai]): _validate_type(name, str, f"ch_names[{ai}][{ci}]") - ch_names = _ndarray_ch_names(ch_names) + return _ndarray_ch_names(ch_names) + + +def _check_o_d_s_c_e(onset, duration, description, ch_names, extras): + onset = _check_onset(onset) + n = len(onset) + duration = _check_duration(duration, n) + description = _check_description(description, n) + ch_names = _check_ch_names_annot(ch_names, n) if not (len(onset) == len(duration) == len(description) == len(ch_names)): raise ValueError( @@ -436,7 +457,13 @@ def onset(self): @onset.setter def onset(self, onset): - self._onset = np.atleast_1d(np.array(onset, dtype=float)) + onset = _check_onset(onset) + if len(onset) != len(self._duration): + raise ValueError( + f"Length of onset ({len(onset)}) must match the length of " + f"existing duration ({len(self._duration)})." + ) + self._onset = onset @property def duration(self): @@ -455,7 +482,14 @@ def duration(self): @duration.setter def duration(self, duration): - self._duration = np.atleast_1d(np.array(duration, dtype=float)) + n = len(self._onset) + duration = _check_duration(duration, n) + if len(duration) != n: + raise ValueError( + f"Length of duration ({len(duration)}) must match the length of " + f"existing onset ({n})." + ) + self._duration = duration @property def description(self): @@ -475,7 +509,14 @@ def description(self): @description.setter def description(self, description): - self._description = np.atleast_1d(np.array(description, dtype=str)) + n = len(self._onset) + description = _check_description(description, n) + if len(description) != n: + raise ValueError( + f"Length of description ({len(description)}) must match the " + f"length of existing onset ({n})." + ) + self._description = description @property def ch_names(self): @@ -490,6 +531,13 @@ def ch_names(self): @ch_names.setter def ch_names(self, ch_names): + n = len(self._onset) + ch_names = _check_ch_names_annot(ch_names, n) + if len(ch_names) != n: + raise ValueError( + f"Length of ch_names ({len(ch_names)}) must match the length of " + f"existing onset ({n})." + ) self._ch_names = ch_names @property @@ -647,11 +695,11 @@ 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) - self.duration = np.append(self.duration, duration) - self.description = np.append(self.description, description) - self.ch_names = np.append(self.ch_names, ch_names) - self.extras.extend(extras) + 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) + self._extras.extend(extras) self._sort() return self @@ -674,10 +722,10 @@ def delete(self, idx): Index of the annotation to remove. Can be array-like to remove multiple indices. """ - self.onset = np.delete(self.onset, idx) - self.duration = np.delete(self.duration, idx) - self.description = np.delete(self.description, idx) - self.ch_names = np.delete(self.ch_names, idx) + self._onset = np.delete(self._onset, idx) + self._duration = np.delete(self._duration, idx) + self._description = np.delete(self._description, idx) + self._ch_names = np.delete(self._ch_names, idx) if isinstance(idx, int_like): del self.extras[idx] elif len(idx) > 0: @@ -814,11 +862,11 @@ def _sort(self): # the onset-then-duration hierarchy vals = sorted(zip(self.onset, self.duration, range(len(self)))) order = list(list(zip(*vals))[-1]) if len(vals) else [] - self.onset = self.onset[order] - self.duration = self.duration[order] - self.description = self.description[order] - self.ch_names = self.ch_names[order] - self.extras = [self.extras[i] for i in order] + self._onset = self._onset[order] + self._duration = self._duration[order] + self._description = self._description[order] + self._ch_names = self._ch_names[order] + self._extras = [self._extras[i] for i in order] return order def _get_crop_lims(self, tmin, tmax, use_orig_time): @@ -922,12 +970,12 @@ def crop( ch_names.append(ch) extras.append(extra) logger.debug(f"Cropping complete (kept {len(onsets)})") - self.onset = np.array(onsets, float) - self.duration = np.array(durations, float) - assert (self.duration >= 0).all() - self.description = np.array(descriptions, dtype=str) - self.ch_names = _ndarray_ch_names(ch_names) - self.extras = extras + self._onset = np.array(onsets, float) + self._duration = np.array(durations, float) + assert (self._duration >= 0).all() + self._description = np.array(descriptions, dtype=str) + self._ch_names = _ndarray_ch_names(ch_names) + self._extras = extras if emit_warning: omitted = np.array(out_of_bounds).sum() diff --git a/mne/tests/test_annotations.py b/mne/tests/test_annotations.py index 1c9a1416a29..036ace26f72 100644 --- a/mne/tests/test_annotations.py +++ b/mne/tests/test_annotations.py @@ -1751,6 +1751,32 @@ def test_annotation_duration_setting(): with pytest.raises(TypeError, match=" got instead"): a.set_durations({"aaa", 2.2}) +def test_setter_validation(): + """Test that onset/duration/description/ch_names setters validate length.""" + annots = Annotations(onset=[1, 3, 2, 4], duration=0, description="foo") + + # onset mismatch should raise + with pytest.raises(ValueError, match="Length of onset"): + annots.onset = annots.onset[:2] + + # duration mismatch should raise + with pytest.raises(ValueError, match="Length of duration"): + annots.duration = annots.duration[:2] + + # description mismatch should raise (the bug drammock reported) + with pytest.raises(ValueError, match="Length of description"): + annots.description = annots.description[:2] + + # scalar duration should broadcast without error + annots.duration = 1.0 + assert len(annots.duration) == 4 + assert all(annots.duration == 1.0) + + # scalar description should broadcast without error + annots.description = "bad" + assert len(annots.description) == 4 + assert all(annots.description == "bad") + @pytest.mark.parametrize("meas_date", (None, 1)) @pytest.mark.parametrize("set_meas_date", ("before", "after")) From a5aa6a0c04ea5831ab7d6275ca44d2aa18acf110 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 16:50:19 +0000 Subject: [PATCH 14/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mne/tests/test_annotations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mne/tests/test_annotations.py b/mne/tests/test_annotations.py index 036ace26f72..0c9d6d1cf50 100644 --- a/mne/tests/test_annotations.py +++ b/mne/tests/test_annotations.py @@ -1751,6 +1751,7 @@ def test_annotation_duration_setting(): with pytest.raises(TypeError, match=" got instead"): a.set_durations({"aaa", 2.2}) + def test_setter_validation(): """Test that onset/duration/description/ch_names setters validate length.""" annots = Annotations(onset=[1, 3, 2, 4], duration=0, description="foo") From 40ab2d2575610d2403db409397eb5410937081e2 Mon Sep 17 00:00:00 2001 From: Famous Date: Fri, 20 Mar 2026 00:36:03 +0530 Subject: [PATCH 15/25] FIX: use private attributes in HEDAnnotations __setstate__ to avoid setter validation error --- mne/annotations.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mne/annotations.py b/mne/annotations.py index f2d611ace9e..953a080a5d9 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -1290,11 +1290,11 @@ def __getstate__(self): def __setstate__(self, state): """Unpack from serialized format.""" self._orig_time = state["_orig_time"] - self.onset = state["onset"] - self.duration = state["duration"] - self.description = state["description"] - self.ch_names = state["ch_names"] - self.extras = state.get("_extras", [None] * len(self.onset)) + self._onset = state["onset"] + self._duration = state["duration"] + self._description = state["description"] + self._ch_names = state["ch_names"] + self._extras = state.get("_extras", [None] * len(self._onset)) self._hed_version = state["_hed_version"] self.hed_string = _HEDStrings( state["hed_string"], hed_version=self._hed_version From 9157d6d7e0cae496b1cf9878091f533a7e5c1bcb Mon Sep 17 00:00:00 2001 From: Famous Date: Sat, 21 Mar 2026 02:48:26 +0530 Subject: [PATCH 16/25] DOC: add cross-reference hyperlinks in See Also sections and clarify private attribute usage in append --- mne/annotations.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/mne/annotations.py b/mne/annotations.py index 953a080a5d9..b04dcbc5189 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -451,7 +451,8 @@ def onset(self): See Also -------- - duration, description + Annotations.duration : Duration of each annotation. + Annotations.description : Description of each annotation. """ return self._onset @@ -476,7 +477,8 @@ def duration(self): See Also -------- - onset, description + Annotations.onset : Onset of each annotation. + Annotations.description : Description of each annotation. """ return self._duration @@ -503,7 +505,8 @@ def description(self): See Also -------- - onset, duration + Annotations.onset : Onset of each annotation. + Annotations.duration : Duration of each annotation. """ return self._description @@ -526,6 +529,12 @@ def ch_names(self): ------- ch_names : list of tuple Channel names associated with each annotation. + + See Also + -------- + Annotations.onset : Onset of each annotation. + Annotations.duration : Duration of each annotation. + Annotations.description : Description of each annotation. """ return self._ch_names @@ -1332,6 +1341,10 @@ def append( onset, duration, description, ch_names, extras = _check_o_d_s_c_e( onset, duration, description, ch_names, extras ) + # Write directly to private attributes to avoid triggering the public + # setter validation, which would raise an error due to temporary length + # mismatches while fields are being extended one at a time. + # The data is already validated by _check_o_d_s_c_e above. hed_string = self._check_hed_strings(hed_string, len(onset)) hed_objs = [ self.hed_string._validate_hed_string(v, self.hed_string._schema) From 5db54f22f336633fc14ccaee7caef6812499fb43 Mon Sep 17 00:00:00 2001 From: Famous Date: Sat, 21 Mar 2026 15:54:32 +0530 Subject: [PATCH 17/25] DOC: use :attr: cross-references in See Also sections of Annotations properties --- mne/annotations.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/mne/annotations.py b/mne/annotations.py index b04dcbc5189..bc7bc01be9a 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -451,8 +451,8 @@ def onset(self): See Also -------- - Annotations.duration : Duration of each annotation. - Annotations.description : Description of each annotation. + :attr:`~mne.Annotations.duration` + :attr:`~mne.Annotations.description` """ return self._onset @@ -477,8 +477,8 @@ def duration(self): See Also -------- - Annotations.onset : Onset of each annotation. - Annotations.description : Description of each annotation. + :attr:`~mne.Annotations.onset` + :attr:`~mne.Annotations.description` """ return self._duration @@ -505,8 +505,8 @@ def description(self): See Also -------- - Annotations.onset : Onset of each annotation. - Annotations.duration : Duration of each annotation. + :attr:`~mne.Annotations.onset` + :attr:`~mne.Annotations.duration` """ return self._description @@ -532,9 +532,9 @@ def ch_names(self): See Also -------- - Annotations.onset : Onset of each annotation. - Annotations.duration : Duration of each annotation. - Annotations.description : Description of each annotation. + :attr:`~mne.Annotations.onset` + :attr:`~mne.Annotations.duration` + :attr:`~mne.Annotations.description` """ return self._ch_names From eb45f5b0db1eddf85cd0c97a5f017f221c4c0bdd Mon Sep 17 00:00:00 2001 From: Famous Date: Sat, 4 Apr 2026 12:50:09 +0530 Subject: [PATCH 18/25] ENH: add length validation to _check_onset, fix description setter, move comment to correct location --- mne/annotations.py | 62 +++++++++++++++++----------------------------- 1 file changed, 23 insertions(+), 39 deletions(-) diff --git a/mne/annotations.py b/mne/annotations.py index bc7bc01be9a..f559fb57d2a 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -158,7 +158,7 @@ def _validate_extras(extras, length: int): return _AnnotationsExtrasList(extras or [None] * length) -def _check_onset(onset): +def _check_onset(onset, n=None): """Convert and validate onset to a 1D float array.""" onset = np.atleast_1d(np.array(onset, dtype=float)) if onset.ndim != 1: @@ -166,6 +166,11 @@ def _check_onset(onset): f"Onset must be a one dimensional array, got {onset.ndim} (shape " f"{onset.shape})." ) + if n is not None and len(onset) != n: + raise ValueError( + f"Length of onset ({len(onset)}) must match the length of " + f"existing duration ({n})." + ) return onset @@ -190,6 +195,11 @@ def _check_description(description, n): raise ValueError( f"Description must be a one dimensional array, got {description.ndim}." ) + if len(description) != n: + raise ValueError( + f"Length of description ({len(description)}) must match the " + f"length of existing onset ({n})." + ) _safe_name_list(description, "write", "description") return description @@ -458,12 +468,7 @@ def onset(self): @onset.setter def onset(self, onset): - onset = _check_onset(onset) - if len(onset) != len(self._duration): - raise ValueError( - f"Length of onset ({len(onset)}) must match the length of " - f"existing duration ({len(self._duration)})." - ) + onset = _check_onset(onset, n=len(self._onset)) self._onset = onset @property @@ -484,13 +489,8 @@ def duration(self): @duration.setter def duration(self, duration): - n = len(self._onset) + n = len(self._duration) duration = _check_duration(duration, n) - if len(duration) != n: - raise ValueError( - f"Length of duration ({len(duration)}) must match the length of " - f"existing onset ({n})." - ) self._duration = duration @property @@ -512,13 +512,8 @@ def description(self): @description.setter def description(self, description): - n = len(self._onset) + n = len(self._description) description = _check_description(description, n) - if len(description) != n: - raise ValueError( - f"Length of description ({len(description)}) must match the " - f"length of existing onset ({n})." - ) self._description = description @property @@ -540,13 +535,8 @@ def ch_names(self): @ch_names.setter def ch_names(self, ch_names): - n = len(self._onset) + n = len(self._ch_names) ch_names = _check_ch_names_annot(ch_names, n) - if len(ch_names) != n: - raise ValueError( - f"Length of ch_names ({len(ch_names)}) must match the length of " - f"existing onset ({n})." - ) self._ch_names = ch_names @property @@ -704,6 +694,10 @@ 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 ) + # Write directly to private attributes to avoid triggering the public + # setter validation, which would raise an error due to temporary length + # mismatches while fields are being extended one at a time. + # The data is already validated by _check_o_d_s_c_e above. self._onset = np.append(self._onset, onset) self._duration = np.append(self._duration, duration) self._description = np.append(self._description, description) @@ -1299,15 +1293,9 @@ def __getstate__(self): def __setstate__(self, state): """Unpack from serialized format.""" self._orig_time = state["_orig_time"] - self._onset = state["onset"] - self._duration = state["duration"] - self._description = state["description"] - self._ch_names = state["ch_names"] - self._extras = state.get("_extras", [None] * len(self._onset)) - self._hed_version = state["_hed_version"] - self.hed_string = _HEDStrings( - state["hed_string"], hed_version=self._hed_version - ) + self._onset, self._duration, self._description, self._ch_names, self._extras = ( + _check_o_d_s_c_e(state["onset"], state["duration"], state["description"], state["ch_names"], state.get("_extras", None)) +) @fill_doc def append( @@ -1341,11 +1329,8 @@ def append( onset, duration, description, ch_names, extras = _check_o_d_s_c_e( onset, duration, description, ch_names, extras ) - # Write directly to private attributes to avoid triggering the public - # setter validation, which would raise an error due to temporary length - # mismatches while fields are being extended one at a time. - # The data is already validated by _check_o_d_s_c_e above. hed_string = self._check_hed_strings(hed_string, len(onset)) + hed_objs = [ self.hed_string._validate_hed_string(v, self.hed_string._schema) for v in hed_string @@ -1364,7 +1349,6 @@ def append( def __iadd__(self, other): """Add (concatenate) two HEDAnnotations objects in-place.""" if not isinstance(other, type(self)): - # Convert self to plain Annotations, preserving HED in extras extras = _hed_extras_from_hed_annotations(self) result = Annotations( onset=self.onset, From dc2516df25299577283dec4680791f3f7f3a07d0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 07:20:33 +0000 Subject: [PATCH 19/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mne/annotations.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/mne/annotations.py b/mne/annotations.py index f559fb57d2a..8f933758fa2 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -1294,8 +1294,14 @@ def __setstate__(self, state): """Unpack from serialized format.""" self._orig_time = state["_orig_time"] self._onset, self._duration, self._description, self._ch_names, self._extras = ( - _check_o_d_s_c_e(state["onset"], state["duration"], state["description"], state["ch_names"], state.get("_extras", None)) -) + _check_o_d_s_c_e( + state["onset"], + state["duration"], + state["description"], + state["ch_names"], + state.get("_extras", None), + ) + ) @fill_doc def append( @@ -1330,7 +1336,7 @@ def append( onset, duration, description, ch_names, extras ) hed_string = self._check_hed_strings(hed_string, len(onset)) - + hed_objs = [ self.hed_string._validate_hed_string(v, self.hed_string._schema) for v in hed_string From 8b5a5dc571607468fb6d023f12f721ef71ea8902 Mon Sep 17 00:00:00 2001 From: Famous Date: Sat, 4 Apr 2026 20:50:29 +0530 Subject: [PATCH 20/25] FIX: add length validation to _check_duration and restore hed_string in HEDAnnotations.__setstate__ --- mne/annotations.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mne/annotations.py b/mne/annotations.py index 8f933758fa2..7a06bad303b 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -183,6 +183,11 @@ def _check_duration(duration, n): raise ValueError( f"Duration must be a one dimensional array, got {duration.ndim}." ) + if len(duration) != n: + raise ValueError( + f"Length of duration ({len(duration)}) must match the length of " + f"existing onset ({n})." + ) return duration @@ -1302,6 +1307,10 @@ def __setstate__(self, state): state.get("_extras", None), ) ) + self._hed_version = state["_hed_version"] + self.hed_string = _HEDStrings( + state["hed_string"], hed_version=self._hed_version + ) @fill_doc def append( From bae3b81a24cd98a25a05bded1647461c83051ea6 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Wed, 27 May 2026 07:14:36 +0200 Subject: [PATCH 21/25] Validate ch_names length --- mne/annotations.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mne/annotations.py b/mne/annotations.py index 7a06bad303b..abd4b5d538d 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -215,6 +215,11 @@ def _check_ch_names_annot(ch_names, n): if ch_names is None: ch_names = [()] * n ch_names = list(ch_names) + if len(ch_names) != n: + raise ValueError( + f"Length of ch_names ({len(ch_names)}) must match the length of " + f"existing annotations ({n})." + ) for ai, ch in enumerate(ch_names): _validate_type(ch, (list, tuple, np.ndarray), f"ch_names[{ai}]") ch_names[ai] = tuple(ch) From 090006460dcc556fc84d2d590baab075fe2dbd45 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Wed, 27 May 2026 07:15:08 +0200 Subject: [PATCH 22/25] Use np.atleast_1d() for consistency --- mne/annotations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/annotations.py b/mne/annotations.py index abd4b5d538d..a88a9deacad 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -193,7 +193,7 @@ def _check_duration(duration, n): def _check_description(description, n): """Convert and validate description to a 1D str array of length n.""" - description = np.array(description, dtype=str) + description = np.atleast_1d(np.array(description, dtype=str)) if description.ndim == 0 or description.shape == (1,): description = np.repeat(description, n) if description.ndim != 1: From 324831cb569210814dd09ed1d193791244cfb0a0 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Wed, 27 May 2026 07:15:21 +0200 Subject: [PATCH 23/25] Fix error message --- mne/annotations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/annotations.py b/mne/annotations.py index a88a9deacad..cae9d2db88c 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -169,7 +169,7 @@ def _check_onset(onset, n=None): if n is not None and len(onset) != n: raise ValueError( f"Length of onset ({len(onset)}) must match the length of " - f"existing duration ({n})." + f"existing annotations ({n})." ) return onset From 9aea4c623ed38a40c95d36a813d83205a51f1e2b Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Wed, 27 May 2026 07:15:37 +0200 Subject: [PATCH 24/25] Remove redundant check --- mne/annotations.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/mne/annotations.py b/mne/annotations.py index cae9d2db88c..7439c5ba8a3 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -235,13 +235,6 @@ def _check_o_d_s_c_e(onset, duration, description, ch_names, extras): description = _check_description(description, n) ch_names = _check_ch_names_annot(ch_names, n) - if not (len(onset) == len(duration) == len(description) == len(ch_names)): - raise ValueError( - "Onset, duration, description, and ch_names must be " - f"equal in sizes, got {len(onset)}, {len(duration)}, " - f"{len(description)}, and {len(ch_names)}." - ) - extras = _validate_extras(extras, len(onset)) return onset, duration, description, ch_names, extras From 7f920a87995546600463702d7a568d8fc8d386a4 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Wed, 27 May 2026 07:15:48 +0200 Subject: [PATCH 25/25] Add ch_names check --- mne/tests/test_annotations.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mne/tests/test_annotations.py b/mne/tests/test_annotations.py index 0c9d6d1cf50..b91627ef7da 100644 --- a/mne/tests/test_annotations.py +++ b/mne/tests/test_annotations.py @@ -1778,6 +1778,14 @@ def test_setter_validation(): assert len(annots.description) == 4 assert all(annots.description == "bad") + # ch_names mismatch should raise + with pytest.raises(ValueError, match="Length of ch_names"): + annots.ch_names = [(), ()] + + # valid ch_names assignment (correct length) should succeed + annots.ch_names = [("MEG 0111",), (), (), ()] + assert annots.ch_names[0] == ("MEG 0111",) + @pytest.mark.parametrize("meas_date", (None, 1)) @pytest.mark.parametrize("set_meas_date", ("before", "after"))