From fa5c6e697898c07631f411456f2a4cde94aeb5b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Sabl=C3=A9-Meyer?= Date: Thu, 16 Apr 2026 22:56:22 +0100 Subject: [PATCH 01/14] FIX Deprecate use_rounding=False default in time_as_index [circle full] `time_as_index` defaulted to truncation (`use_rounding=False`), causing `get_data(tmin=...)` to return different samples from `crop(tmin=...).get_data()` at certain times due to floating-point truncation. Change the default to `None` and rounding. Emits a `FutureWarning` prompting callers to pass `True` or `False` explicitly. The goal of this commit is to estiate the impact of this change; follow-up commit should internally update all use of `time_as_index` to explicitely set a desired default. --- mne/io/base.py | 17 +++++++++++++---- mne/io/tests/test_raw.py | 10 +++++++--- mne/utils/mixin.py | 26 ++++++++++++++++++++++---- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/mne/io/base.py b/mne/io/base.py index 8af949afea0..fa52978e42d 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -630,16 +630,25 @@ def last_samp(self): def _last_time(self): return self.last_samp / float(self.info["sfreq"]) - def time_as_index(self, times, use_rounding=False, origin=None): + def time_as_index(self, times, use_rounding=None, origin=None): """Convert time to indices. Parameters ---------- times : list-like | float | int List of numbers or a number representing points in time. - use_rounding : bool - If True, use rounding (instead of truncation) when converting - times to indices. This can help avoid non-unique indices. + use_rounding : bool | None + If True, use rounding when converting times to indices. This can + help avoid non-unique indices. + If False, use truncation when converting times to indices. + If None (the default), rounding is used but a FutureWarning is + emitted. Pass ``True`` or ``False`` explicitly to silence the + warning. + + .. versionchanged:: 2.0 + The default changed from ``False`` to ``None``, which will + round and emit a warning. In a future release the default + will change to ``True``. origin : datetime | float | int | None Time reference for times. If None, ``times`` are assumed to be relative to :term:`first_samp`. diff --git a/mne/io/tests/test_raw.py b/mne/io/tests/test_raw.py index bb6d58ee0d9..6dc062adc4a 100644 --- a/mne/io/tests/test_raw.py +++ b/mne/io/tests/test_raw.py @@ -620,14 +620,18 @@ def test_time_as_index(): """Test indexing of raw times.""" raw = read_raw_fif(raw_fname) - # Test original (non-rounding) indexing behavior - orig_inds = raw.time_as_index(raw.times) + # Test truncation behavior with explicit use_rounding=False + orig_inds = raw.time_as_index(raw.times, use_rounding=False) assert len(set(orig_inds)) != len(orig_inds) - # Test new (rounding) indexing behavior + # Test rounding behavior with explicit use_rounding=True new_inds = raw.time_as_index(raw.times, use_rounding=True) assert_array_equal(new_inds, np.arange(len(raw.times))) + # Test deprecation warning when use_rounding is not specified + with pytest.warns(FutureWarning, match="use_rounding=False is being changed"): + raw.time_as_index(raw.times) + @pytest.mark.parametrize("meas_date", [None, "orig"]) @pytest.mark.parametrize("first_samp", [0, 10000]) diff --git a/mne/utils/mixin.py b/mne/utils/mixin.py index f0d4b9f7524..f56bedb2e70 100644 --- a/mne/utils/mixin.py +++ b/mne/utils/mixin.py @@ -493,16 +493,25 @@ def _check_decim(info, decim, offset, check_filter=True): class TimeMixin: """Class for time operations on any MNE object that has a time axis.""" - def time_as_index(self, times, use_rounding=False): + def time_as_index(self, times, use_rounding=None): """Convert time to indices. Parameters ---------- times : list-like | float | int List of numbers or a number representing points in time. - use_rounding : bool - If True, use rounding (instead of truncation) when converting - times to indices. This can help avoid non-unique indices. + use_rounding : bool | None + If True, use rounding when converting times to indices. This can + help avoid non-unique indices. + If False, use truncation when converting times to indices. + If None (the default), rounding is used but a FutureWarning is + emitted. Pass ``True`` or ``False`` explicitly to silence the + warning. + + .. versionchanged:: 2.0 + The default changed from ``False`` to ``None``, which will + round and emit a warning. In a future release the default + will change to ``True``. Returns ------- @@ -511,6 +520,15 @@ def time_as_index(self, times, use_rounding=False): """ from ..source_estimate import _BaseSourceEstimate + if use_rounding is None: + warn( + "The default of use_rounding=False is being changed to True " + "in a future release. Pass use_rounding=True or " + "use_rounding=False explicitly to silence this warning.", + FutureWarning, + ) + use_rounding = True + if isinstance(self, _BaseSourceEstimate): sfreq = 1.0 / self.tstep else: From aeb99e9ec08250c4537a2bb22d0f7880bbeeb677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Sabl=C3=A9-Meyer?= Date: Fri, 17 Apr 2026 11:45:29 +0100 Subject: [PATCH 02/14] Temporarily remove the FutureWarning to estimate impact The goal here is to measure the impact of changing the default value of `use_rounding=False` in time_as_index`, with a FutureWarning deprectation step to warn users. Unfortunately the tests fail hard on seeing said warning so most tests fail but it's not because the underlying behavior has changed, it's because of the warning. This commit removes the warning to see where is the change in behavior surfacing actuall problems. --- mne/io/tests/test_raw.py | 6 ++++-- mne/utils/mixin.py | 15 +++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/mne/io/tests/test_raw.py b/mne/io/tests/test_raw.py index 6dc062adc4a..3179caf6e35 100644 --- a/mne/io/tests/test_raw.py +++ b/mne/io/tests/test_raw.py @@ -629,8 +629,10 @@ def test_time_as_index(): assert_array_equal(new_inds, np.arange(len(raw.times))) # Test deprecation warning when use_rounding is not specified - with pytest.warns(FutureWarning, match="use_rounding=False is being changed"): - raw.time_as_index(raw.times) + # Temporarily turned of to measure impact + # + # with pytest.warns(FutureWarning, match="use_rounding=False is being changed"): + # raw.time_as_index(raw.times) @pytest.mark.parametrize("meas_date", [None, "orig"]) diff --git a/mne/utils/mixin.py b/mne/utils/mixin.py index f56bedb2e70..adc2cabea62 100644 --- a/mne/utils/mixin.py +++ b/mne/utils/mixin.py @@ -521,12 +521,15 @@ def time_as_index(self, times, use_rounding=None): from ..source_estimate import _BaseSourceEstimate if use_rounding is None: - warn( - "The default of use_rounding=False is being changed to True " - "in a future release. Pass use_rounding=True or " - "use_rounding=False explicitly to silence this warning.", - FutureWarning, - ) + # Turned off temporarily to see the impact of the change without + # crashing on a FutureWarning + # + # warn( + # "The default of use_rounding=False is being changed to True " + # "in a future release. Pass use_rounding=True or " + # "use_rounding=False explicitly to silence this warning.", + # FutureWarning, + # ) use_rounding = True if isinstance(self, _BaseSourceEstimate): From 67e1796424a9dfd28b5c93fbd94eacdf3bf3bb24 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 10:48:06 +0000 Subject: [PATCH 03/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mne/io/tests/test_raw.py | 2 +- mne/utils/mixin.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mne/io/tests/test_raw.py b/mne/io/tests/test_raw.py index 3179caf6e35..9baeab23804 100644 --- a/mne/io/tests/test_raw.py +++ b/mne/io/tests/test_raw.py @@ -632,7 +632,7 @@ def test_time_as_index(): # Temporarily turned of to measure impact # # with pytest.warns(FutureWarning, match="use_rounding=False is being changed"): - # raw.time_as_index(raw.times) + # raw.time_as_index(raw.times) @pytest.mark.parametrize("meas_date", [None, "orig"]) diff --git a/mne/utils/mixin.py b/mne/utils/mixin.py index adc2cabea62..7bd5ed980f6 100644 --- a/mne/utils/mixin.py +++ b/mne/utils/mixin.py @@ -525,10 +525,10 @@ def time_as_index(self, times, use_rounding=None): # crashing on a FutureWarning # # warn( - # "The default of use_rounding=False is being changed to True " - # "in a future release. Pass use_rounding=True or " - # "use_rounding=False explicitly to silence this warning.", - # FutureWarning, + # "The default of use_rounding=False is being changed to True " + # "in a future release. Pass use_rounding=True or " + # "use_rounding=False explicitly to silence this warning.", + # FutureWarning, # ) use_rounding = True From b57e19395b2d3352d02ae2a09d12fd922277d627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Sabl=C3=A9-Meyer?= Date: Fri, 17 Apr 2026 13:35:31 +0100 Subject: [PATCH 04/14] Set `use_rounding=False` in `mne/io/egi/tests/test_egi.py Again, in order to check the impact of changing the default value of `use_rounding` in `time_as_index` from False to True, I am providing a (possibly temporary) fix to `mne/io/egi/tests/test_egi.py::test_egi_mff_pause` so that it passes and we can see if there are other downstream issues. --- mne/io/egi/tests/test_egi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mne/io/egi/tests/test_egi.py b/mne/io/egi/tests/test_egi.py index 261a9c80da3..fac6dd0aa09 100644 --- a/mne/io/egi/tests/test_egi.py +++ b/mne/io/egi/tests/test_egi.py @@ -104,7 +104,8 @@ def test_egi_mff_pause(fname, skip_times, event_times): for ii, annot in enumerate(raw.annotations): assert annot["description"] == "BAD_ACQ_SKIP" start, stop = raw.time_as_index( - [annot["onset"], annot["onset"] + annot["duration"]] + [annot["onset"], annot["onset"] + annot["duration"]], + use_rounding=False ) data, _ = raw[:, start:stop] assert_array_equal(data[other_picks], 0.0) From f7c6edeaa29737f676ee121e896cf764e4ae84f0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 12:39:03 +0000 Subject: [PATCH 05/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mne/io/egi/tests/test_egi.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mne/io/egi/tests/test_egi.py b/mne/io/egi/tests/test_egi.py index fac6dd0aa09..89cfb970655 100644 --- a/mne/io/egi/tests/test_egi.py +++ b/mne/io/egi/tests/test_egi.py @@ -104,8 +104,7 @@ def test_egi_mff_pause(fname, skip_times, event_times): for ii, annot in enumerate(raw.annotations): assert annot["description"] == "BAD_ACQ_SKIP" start, stop = raw.time_as_index( - [annot["onset"], annot["onset"] + annot["duration"]], - use_rounding=False + [annot["onset"], annot["onset"] + annot["duration"]], use_rounding=False ) data, _ = raw[:, start:stop] assert_array_equal(data[other_picks], 0.0) From b712a6a2fb771649f0cfdc6fc0c357bf0fa420e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Sabl=C3=A9-Meyer?= Date: Fri, 22 May 2026 17:56:26 +0100 Subject: [PATCH 06/14] RuntimeError if `index` != `np.round(index)` to asses --- mne/utils/mixin.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/mne/utils/mixin.py b/mne/utils/mixin.py index 7bd5ed980f6..c449f27a5fe 100644 --- a/mne/utils/mixin.py +++ b/mne/utils/mixin.py @@ -520,9 +520,16 @@ def time_as_index(self, times, use_rounding=None): """ from ..source_estimate import _BaseSourceEstimate + if isinstance(self, _BaseSourceEstimate): + sfreq = 1.0 / self.tstep + else: + sfreq = self.info["sfreq"] + index = (np.atleast_1d(times) - self.times[0]) * sfreq + if use_rounding is None: # Turned off temporarily to see the impact of the change without - # crashing on a FutureWarning + # crashing on a FutureWarning. Only crash, explicitely, if the + # values with and without rounding are different. # # warn( # "The default of use_rounding=False is being changed to True " @@ -530,13 +537,10 @@ def time_as_index(self, times, use_rounding=None): # "use_rounding=False explicitly to silence this warning.", # FutureWarning, # ) + if index != np.round(index): + raise RuntimeError(f"This would have returned a different value: {index=}, different from {np.round(index)=}.") use_rounding = True - if isinstance(self, _BaseSourceEstimate): - sfreq = 1.0 / self.tstep - else: - sfreq = self.info["sfreq"] - index = (np.atleast_1d(times) - self.times[0]) * sfreq if use_rounding: index = np.round(index) return index.astype(int) From a35d9b46e8ac920f3ac49440c5315da1f3d23ddc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 17:07:11 +0000 Subject: [PATCH 07/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mne/utils/mixin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mne/utils/mixin.py b/mne/utils/mixin.py index c449f27a5fe..fe9e527c75d 100644 --- a/mne/utils/mixin.py +++ b/mne/utils/mixin.py @@ -538,7 +538,9 @@ def time_as_index(self, times, use_rounding=None): # FutureWarning, # ) if index != np.round(index): - raise RuntimeError(f"This would have returned a different value: {index=}, different from {np.round(index)=}.") + raise RuntimeError( + f"This would have returned a different value: {index=}, different from {np.round(index)=}." + ) use_rounding = True if use_rounding: From 51f0b5698feca82c0d53176a4c50d28607d108c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Sabl=C3=A9-Meyer?= Date: Fri, 22 May 2026 18:33:42 +0100 Subject: [PATCH 08/14] Adds positional argument marker to `time_as_index` Co-authored-by: Eric Larson --- mne/io/base.py | 2 +- mne/utils/mixin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mne/io/base.py b/mne/io/base.py index c66dd1e5a76..efbf7504b45 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -637,7 +637,7 @@ def last_samp(self): def _last_time(self): return self.last_samp / float(self.info["sfreq"]) - def time_as_index(self, times, use_rounding=None, origin=None): + def time_as_index(self, times, *, use_rounding=None, origin=None): """Convert time to indices. Parameters diff --git a/mne/utils/mixin.py b/mne/utils/mixin.py index fe9e527c75d..46850df8c4c 100644 --- a/mne/utils/mixin.py +++ b/mne/utils/mixin.py @@ -493,7 +493,7 @@ def _check_decim(info, decim, offset, check_filter=True): class TimeMixin: """Class for time operations on any MNE object that has a time axis.""" - def time_as_index(self, times, use_rounding=None): + def time_as_index(self, times, *, use_rounding=None): """Convert time to indices. Parameters From 1bc09372e2963b8ede3e4bb155750a7f37077f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Sabl=C3=A9-Meyer?= Date: Fri, 22 May 2026 18:35:00 +0100 Subject: [PATCH 09/14] `index` is an array: use `np.array_equal` Co-authored-by: Eric Larson --- mne/utils/mixin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/utils/mixin.py b/mne/utils/mixin.py index 46850df8c4c..2b855aad69c 100644 --- a/mne/utils/mixin.py +++ b/mne/utils/mixin.py @@ -537,7 +537,7 @@ def time_as_index(self, times, *, use_rounding=None): # "use_rounding=False explicitly to silence this warning.", # FutureWarning, # ) - if index != np.round(index): + if not np.array_equal(index.astype(int), np.round(index)) raise RuntimeError( f"This would have returned a different value: {index=}, different from {np.round(index)=}." ) From 8372c7c79aa03f25f0863dd767398a3f95c0c6b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Sabl=C3=A9-Meyer?= Date: Fri, 22 May 2026 18:35:30 +0100 Subject: [PATCH 10/14] Limited impact: might merge sooner than 2.0 Co-authored-by: Eric Larson --- mne/io/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/io/base.py b/mne/io/base.py index efbf7504b45..b40d9782e00 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -652,7 +652,7 @@ def time_as_index(self, times, *, use_rounding=None, origin=None): emitted. Pass ``True`` or ``False`` explicitly to silence the warning. - .. versionchanged:: 2.0 + .. versionchanged:: 1.13 The default changed from ``False`` to ``None``, which will round and emit a warning. In a future release the default will change to ``True``. From 1f12a5ad3e65567c42b479fa31dd3289894b6c0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Sabl=C3=A9-Meyer?= Date: Fri, 22 May 2026 18:38:46 +0100 Subject: [PATCH 11/14] Merge + fix linting (>80 chars line) --- mne/utils/mixin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mne/utils/mixin.py b/mne/utils/mixin.py index 2b855aad69c..3edd38efe34 100644 --- a/mne/utils/mixin.py +++ b/mne/utils/mixin.py @@ -528,7 +528,7 @@ def time_as_index(self, times, *, use_rounding=None): if use_rounding is None: # Turned off temporarily to see the impact of the change without - # crashing on a FutureWarning. Only crash, explicitely, if the + # crashing on a FutureWarning. Only crash, explicitly, if the # values with and without rounding are different. # # warn( @@ -539,8 +539,8 @@ def time_as_index(self, times, *, use_rounding=None): # ) if not np.array_equal(index.astype(int), np.round(index)) raise RuntimeError( - f"This would have returned a different value: {index=}, different from {np.round(index)=}." - ) + f"Would have returned a different value: {index=}, " + f"different from {np.round(index)=}.") use_rounding = True if use_rounding: From 96e72bb9270398a8b7ec7268112dd84e0e98e757 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Sabl=C3=A9-Meyer?= Date: Fri, 22 May 2026 18:52:12 +0100 Subject: [PATCH 12/14] Fix missing `:` --- mne/utils/mixin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/utils/mixin.py b/mne/utils/mixin.py index 3edd38efe34..874386d67aa 100644 --- a/mne/utils/mixin.py +++ b/mne/utils/mixin.py @@ -537,7 +537,7 @@ def time_as_index(self, times, *, use_rounding=None): # "use_rounding=False explicitly to silence this warning.", # FutureWarning, # ) - if not np.array_equal(index.astype(int), np.round(index)) + if not np.array_equal(index.astype(int), np.round(index)): raise RuntimeError( f"Would have returned a different value: {index=}, " f"different from {np.round(index)=}.") From b706eac6b6dddcc6407e8c48816ff1b852283472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Sabl=C3=A9-Meyer?= Date: Fri, 22 May 2026 18:52:25 +0100 Subject: [PATCH 13/14] `use_rounding` explicitely positional in base.py --- mne/io/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/io/base.py b/mne/io/base.py index b40d9782e00..c13e26558c1 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -682,7 +682,7 @@ def time_as_index(self, times, *, use_rounding=None, origin=None): delta = (origin - first_samp_in_abs_time).total_seconds() times = np.atleast_1d(times) + delta - return super().time_as_index(times, use_rounding) + return super().time_as_index(times, use_rounding=use_rounding) @property def _raw_lengths(self): From 7d6df4c103227c74609d23da0de269f6189891c4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 17:53:14 +0000 Subject: [PATCH 14/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mne/utils/mixin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mne/utils/mixin.py b/mne/utils/mixin.py index 874386d67aa..d52728af374 100644 --- a/mne/utils/mixin.py +++ b/mne/utils/mixin.py @@ -540,7 +540,8 @@ def time_as_index(self, times, *, use_rounding=None): if not np.array_equal(index.astype(int), np.round(index)): raise RuntimeError( f"Would have returned a different value: {index=}, " - f"different from {np.round(index)=}.") + f"different from {np.round(index)=}." + ) use_rounding = True if use_rounding: