Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.1", # BinnedAggregate must resolve chunk_dim from axis=None
"sparse>=0.17.0",
"numpy>=2.2.6",
"numba>=0.65.1",
Expand Down
35 changes: 25 additions & 10 deletions src/ezmsg/event/binned.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
13 changes: 10 additions & 3 deletions src/ezmsg/event/peak.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down
59 changes: 59 additions & 0 deletions tests/test_binned.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import time
import warnings

import numpy as np
import pytest
Expand Down Expand Up @@ -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)
Loading