From 3be68da4e210dfbf948a2ffb6588804e43ffe4b8 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Sun, 6 Sep 2026 22:15:57 -0400 Subject: [PATCH 1/3] Follow chunk_dim for binning, and deprecate BinnedEventAggregatorSettings.axis Binning carries an open partial bin across message boundaries, so it is only meaningful along the dimension messages accumulate along. That dimension is the producer's to declare -- `AxisArray.chunk_dim` -- and a setting letting a consumer disagree can only be used to be wrong. Deprecated here, removed in 2.0, matching what ezmsg-sigproc 3.8 does to the binner this delegates to. The default changes from "time" to None, so `BinnedAggregate` needs to be the version that resolves chunk_dim rather than indexing `axes[None]`; hence the ezmsg-sigproc>=3.8.0 pin. Two forwarding sites are wrapped in suppress_axis_deprecation(): * binned.py hands our own already-warned `axis` to BinnedAggregateSettings. Warning again would name a sigproc class for a setting the user made on ours, sending them to look in the wrong place. * peak.py builds an AdaptiveStandardScaler with axis="time". This module works on "time" throughout, and the scaler shares the state that normalises the samples being thresholded, so it must use the same axis. Suppressed rather than dropped for that reason -- letting the scaler follow chunk_dim while everything around it used "time" would be worse than either. Teaching the whole module to resolve chunk_dim is a separate change. --- pyproject.toml | 4 +-- src/ezmsg/event/binned.py | 35 ++++++++++++++++------- src/ezmsg/event/peak.py | 13 +++++++-- tests/test_binned.py | 59 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d693b1d..b5e5105 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,8 +13,8 @@ dependencies = [ # left to the transitive requirement: uv only enables pre-releases for a # package named with a pre-release marker in *this* file. "ezmsg>=3.10.0b2", - "ezmsg-baseproc>=1.12.0", - "ezmsg-sigproc>=2.26.1", + "ezmsg-baseproc>=1.13.0", + "ezmsg-sigproc>=3.8.0", # BinnedAggregate must resolve chunk_dim from axis=None "sparse>=0.17.0", "numpy>=2.2.6", "numba>=0.65.1", diff --git a/src/ezmsg/event/binned.py b/src/ezmsg/event/binned.py index 474fb3d..451fc2c 100644 --- a/src/ezmsg/event/binned.py +++ b/src/ezmsg/event/binned.py @@ -23,7 +23,12 @@ import ezmsg.core as ez import sparse from array_api_compat import get_namespace -from ezmsg.baseproc import BaseTransformer, BaseTransformerUnit +from ezmsg.baseproc import ( + BaseTransformer, + BaseTransformerUnit, + suppress_axis_deprecation, + warn_axis_deprecated, +) from ezmsg.sigproc.aggregate import AggregationFunction from ezmsg.sigproc.binned_aggregate import BinnedAggregateSettings, BinnedAggregateTransformer from ezmsg.util.messages.axisarray import AxisArray, replace @@ -36,8 +41,15 @@ class BinnedEventAggregatorSettings(ez.Settings): scale_output: bool = True """If True, divide each bin's count by ``bin_duration`` (events/second).""" - axis: str = "time" - """Name of the axis to bin along.""" + axis: str | None = None + """.. deprecated:: 1.4 + Scheduled for removal in 2.0. Binning carries an open partial bin across + message boundaries, which is only meaningful along the dimension messages + accumulate along; that dimension now comes from + :attr:`~ezmsg.util.messages.axisarray.AxisArray.chunk_dim`.""" + + def __post_init__(self) -> None: + warn_axis_deprecated(self, package="ezmsg-event", removal="2.0") fractional: bool = True """If True (default), bins span a fractional ``bin_duration * fs`` samples via @@ -60,14 +72,17 @@ class BinnedEventAggregator(BaseTransformer[BinnedEventAggregatorSettings, AxisA def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - self._binner = BinnedAggregateTransformer( - BinnedAggregateSettings( - axis=self.settings.axis, - bin_duration=self.settings.bin_duration, - operation=AggregationFunction.SUM, - fractional=self.settings.fractional, + # Forwarding this stage's own already-warned setting; warning again would + # name a sigproc class for something the user set on ours. + with suppress_axis_deprecation(): + self._binner = BinnedAggregateTransformer( + BinnedAggregateSettings( + axis=self.settings.axis, + bin_duration=self.settings.bin_duration, + operation=AggregationFunction.SUM, + fractional=self.settings.fractional, + ) ) - ) def _process(self, message: AxisArray) -> AxisArray: data = message.data diff --git a/src/ezmsg/event/peak.py b/src/ezmsg/event/peak.py index 79b90a8..941aaf8 100644 --- a/src/ezmsg/event/peak.py +++ b/src/ezmsg/event/peak.py @@ -23,6 +23,7 @@ BaseStatefulTransformer, BaseTransformerUnit, processor_state, + suppress_axis_deprecation, ) from ezmsg.sigproc.scaler import AdaptiveStandardScalerTransformer from ezmsg.util.messages.axisarray import AxisArray, replace # slice_along_axis, @@ -126,9 +127,15 @@ def _reset_state(self, message: AxisArray) -> None: self._state.scaler = None self._state.data_raw = None if self.settings.auto_scale_tau > 0: - self._state.scaler = AdaptiveStandardScalerTransformer( - time_constant=self.settings.auto_scale_tau, axis="time" - ) + # This module works on "time" throughout (see get_axis_idx calls + # below), so the scaler has to as well -- they share the state that + # normalises the samples being thresholded. Suppressed rather than + # dropped for that reason: letting the scaler follow chunk_dim while + # everything around it used "time" would be worse than either. + with suppress_axis_deprecation(): + self._state.scaler = AdaptiveStandardScalerTransformer( + time_constant=self.settings.auto_scale_tau, axis="time" + ) if self.settings.return_peak_val: self._state.data_raw = first_samp diff --git a/tests/test_binned.py b/tests/test_binned.py index 462f5bc..4f7ced7 100644 --- a/tests/test_binned.py +++ b/tests/test_binned.py @@ -1,4 +1,5 @@ import time +import warnings import numpy as np import pytest @@ -220,3 +221,61 @@ def test_fractional_false_sample_locked(fs: float): spb = int(bin_dur * fs) assert sum(m.data.shape[0] for m in out) == n // spb assert out[0].axes["time"].gain == pytest.approx(spb / fs) + + +class TestTheAxisSettingIsDeprecated: + """``axis`` is going away in 2.0: binning carries an open partial bin across + message boundaries, which is only meaningful along the dimension messages + accumulate along. That dimension comes from ``AxisArray.chunk_dim``.""" + + @staticmethod + def _msg(n=64, fs=200.0): + data = np.zeros((n, 2)) + data[::8] = 1.0 + return AxisArray( + data, + dims=["time", "ch"], + axes={"time": AxisArray.TimeAxis(fs=fs)}, + key="dev", + chunk_dim="time", + ) + + @staticmethod + def _axis_warnings(records): + return [r for r in records if issubclass(r.category, FutureWarning) and "deprecated" in str(r.message)] + + def test_setting_it_warns_about_our_class_not_sigprocs(self): + """The user set it on ``BinnedEventAggregatorSettings``; naming the + sigproc class we delegate to would send them looking in the wrong place.""" + with warnings.catch_warnings(record=True) as rec: + warnings.simplefilter("always") + BinnedEventAggregatorSettings(bin_duration=0.02, axis="time") + (record,) = self._axis_warnings(rec) + assert "BinnedEventAggregatorSettings.axis" in str(record.message) + + def test_leaving_it_unset_is_silent_through_processing(self): + """The delegation to BinnedAggregate forwards our setting, so without + suppression every use would warn about a class the user never touched.""" + with warnings.catch_warnings(record=True) as rec: + warnings.simplefilter("always") + proc = BinnedEventAggregator(BinnedEventAggregatorSettings(bin_duration=0.02)) + proc(self._msg()) + proc(self._msg()) + assert not self._axis_warnings(rec) + + def test_forwarding_our_own_setting_warns_only_once(self): + with warnings.catch_warnings(record=True) as rec: + warnings.simplefilter("always") + proc = BinnedEventAggregator(BinnedEventAggregatorSettings(bin_duration=0.02, axis="time")) + proc(self._msg()) + assert len(self._axis_warnings(rec)) == 1 + + def test_it_still_bins_along_a_configured_axis(self): + """Behaviour is unchanged until removal.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + explicit = BinnedEventAggregator(BinnedEventAggregatorSettings(bin_duration=0.02, axis="time")) + implicit = BinnedEventAggregator(BinnedEventAggregatorSettings(bin_duration=0.02)) + a = explicit(self._msg()) + b = implicit(self._msg()) + assert np.array_equal(a.data, b.data) From b22b1f3c0185976b1204a78cf3e7e9297824f1a4 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Sun, 6 Sep 2026 22:30:30 -0400 Subject: [PATCH 2/3] Suppress with the mechanism that raises the warning ezmsg-sigproc 3.8.0 shipped its own copy of the deprecation machinery before it moved to ezmsg-baseproc, so its warnings check a ContextVar that baseproc's suppress_axis_deprecation() does not set. Entering baseproc's context therefore silenced nothing, and forwarding our axis warned twice -- once for our setting and once for sigproc's. Importing the context manager from ezmsg.sigproc works against 3.8.0 and against later versions, which re-export baseproc's (so it is the same object and the same ContextVar). Our own settings keep warning through baseproc. --- src/ezmsg/event/binned.py | 7 ++++++- src/ezmsg/event/peak.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/ezmsg/event/binned.py b/src/ezmsg/event/binned.py index 451fc2c..16873a8 100644 --- a/src/ezmsg/event/binned.py +++ b/src/ezmsg/event/binned.py @@ -26,11 +26,16 @@ from ezmsg.baseproc import ( BaseTransformer, BaseTransformerUnit, - suppress_axis_deprecation, warn_axis_deprecated, ) from ezmsg.sigproc.aggregate import AggregationFunction from ezmsg.sigproc.binned_aggregate import BinnedAggregateSettings, BinnedAggregateTransformer + +# Suppression has to come from whichever module raises the warning, and +# ezmsg-sigproc 3.8.0 shipped its own copy of the mechanism before it moved +# to ezmsg-baseproc. Later versions re-export baseproc's, so this import is +# the one that works against both. +from ezmsg.sigproc.util.deprecation import suppress_axis_deprecation from ezmsg.util.messages.axisarray import AxisArray, replace diff --git a/src/ezmsg/event/peak.py b/src/ezmsg/event/peak.py index 941aaf8..5f510c0 100644 --- a/src/ezmsg/event/peak.py +++ b/src/ezmsg/event/peak.py @@ -23,9 +23,9 @@ BaseStatefulTransformer, BaseTransformerUnit, processor_state, - suppress_axis_deprecation, ) from ezmsg.sigproc.scaler import AdaptiveStandardScalerTransformer +from ezmsg.sigproc.util.deprecation import suppress_axis_deprecation from ezmsg.util.messages.axisarray import AxisArray, replace # slice_along_axis, From f8c361cad33a96d3c091d23c22f8abf6b147db0e Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Mon, 7 Sep 2026 00:31:17 -0400 Subject: [PATCH 3/3] Take suppression from ezmsg-baseproc, the one place it lives ezmsg-sigproc 3.8.1 shares baseproc's deprecation mechanism, so there is a single ContextVar again and the import that works is simply the obvious one. Pinned to >=3.8.1 accordingly, and the note explaining which of two mechanisms to reach for is gone with it. --- pyproject.toml | 2 +- src/ezmsg/event/binned.py | 7 +------ src/ezmsg/event/peak.py | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b5e5105..489ec2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ dependencies = [ # package named with a pre-release marker in *this* file. "ezmsg>=3.10.0b2", "ezmsg-baseproc>=1.13.0", - "ezmsg-sigproc>=3.8.0", # BinnedAggregate must resolve chunk_dim from axis=None + "ezmsg-sigproc>=3.8.1", # BinnedAggregate must resolve chunk_dim from axis=None "sparse>=0.17.0", "numpy>=2.2.6", "numba>=0.65.1", diff --git a/src/ezmsg/event/binned.py b/src/ezmsg/event/binned.py index 16873a8..451fc2c 100644 --- a/src/ezmsg/event/binned.py +++ b/src/ezmsg/event/binned.py @@ -26,16 +26,11 @@ from ezmsg.baseproc import ( BaseTransformer, BaseTransformerUnit, + suppress_axis_deprecation, warn_axis_deprecated, ) from ezmsg.sigproc.aggregate import AggregationFunction from ezmsg.sigproc.binned_aggregate import BinnedAggregateSettings, BinnedAggregateTransformer - -# Suppression has to come from whichever module raises the warning, and -# ezmsg-sigproc 3.8.0 shipped its own copy of the mechanism before it moved -# to ezmsg-baseproc. Later versions re-export baseproc's, so this import is -# the one that works against both. -from ezmsg.sigproc.util.deprecation import suppress_axis_deprecation from ezmsg.util.messages.axisarray import AxisArray, replace diff --git a/src/ezmsg/event/peak.py b/src/ezmsg/event/peak.py index 5f510c0..941aaf8 100644 --- a/src/ezmsg/event/peak.py +++ b/src/ezmsg/event/peak.py @@ -23,9 +23,9 @@ BaseStatefulTransformer, BaseTransformerUnit, processor_state, + suppress_axis_deprecation, ) from ezmsg.sigproc.scaler import AdaptiveStandardScalerTransformer -from ezmsg.sigproc.util.deprecation import suppress_axis_deprecation from ezmsg.util.messages.axisarray import AxisArray, replace # slice_along_axis,