Source code for ezmsg.event.binned
"""Bin an event stream into a lower-rate count (or rate) signal.
The binning is delegated to
:obj:`ezmsg.sigproc.binned_aggregate.BinnedAggregate` so this shares one
bin-boundary implementation with every other consumer of that binner. With
``fractional=True`` (the default) bins span a fractional ``bin_duration * fs``
samples with a carry accumulator and are labelled with the nominal
``bin_duration`` gain; with ``fractional=False`` they span a fixed
``int(bin_duration * fs)`` samples (sample-locked, matching
:obj:`ezmsg.sigproc.window.Window`). Because the grid comes from the shared
binner, two streams binned this way at the same ``bin_duration`` land on the
same grid for any input rate and can be aligned downstream (e.g. with
``ezmsg.sigproc.merge.Merge``).
Sparse ``sparse.COO`` inputs (e.g. the default
:obj:`ezmsg.event.peak.ThresholdCrossing` output) are densified to per-sample
contributions before binning; dense inputs are used as is. Set
``scale_by_value=True`` to weight each event by its stored value instead of
counting occurrences, and ``scale_output=True`` to divide the per-bin count by
``bin_duration`` (events/second).
"""
import ezmsg.core as ez
import sparse
from array_api_compat import get_namespace
from ezmsg.baseproc import BaseTransformer, BaseTransformerUnit
from ezmsg.sigproc.aggregate import AggregationFunction
from ezmsg.sigproc.binned_aggregate import BinnedAggregateSettings, BinnedAggregateTransformer
from ezmsg.util.messages.axisarray import AxisArray, replace
[docs]
class BinnedEventAggregatorSettings(ez.Settings):
bin_duration: float = 0.05
"""Duration of each output bin in seconds."""
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."""
fractional: bool = True
"""If True (default), bins span a fractional ``bin_duration * fs`` samples via
:obj:`BinnedAggregate` and are labelled with the nominal ``bin_duration``
gain. If False, bins span a fixed ``int(bin_duration * fs)`` samples
(sample-locked). See :obj:`BinnedAggregate`."""
scale_by_value: bool = False
"""If True, weight each event by its stored value; if False (default), every
nonzero entry contributes a count of 1."""
[docs]
class BinnedEventAggregator(BaseTransformer[BinnedEventAggregatorSettings, AxisArray, AxisArray]):
"""Count events per fixed-duration bin, delegating binning to sigproc.
The per-bin reduction, carry across message boundaries, and output time axis
all come from :obj:`BinnedAggregateTransformer`; this wrapper only converts
events to per-sample contributions and optionally rate-normalizes.
"""
[docs]
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,
)
)
def _process(self, message: AxisArray) -> AxisArray:
data = message.data
if isinstance(data, sparse.SparseArray):
data = data.todense()
xp = get_namespace(data)
# Per-sample contribution: the event value, or 1 per nonzero entry.
# float64 where usable (exact integer counts; matches the legacy output
# dtype); float32 otherwise. MLX *exposes* ``mx.float64`` as an attribute
# but the GPU rejects it ("float64 is not supported on the GPU"), so
# ``hasattr(xp, "float64")`` is not a sufficient capability check --
# detect the MLX namespace explicitly and fall back to float32.
is_mlx = getattr(xp, "__name__", "") == "mlx.core"
float_dtype = xp.float64 if (hasattr(xp, "float64") and not is_mlx) else xp.float32
contrib = data if self.settings.scale_by_value else (data != 0)
contrib = contrib.astype(float_dtype)
binned = self._binner(replace(message, data=contrib))
if self.settings.scale_output and binned.data.size:
binned = replace(binned, data=binned.data / self.settings.bin_duration)
return binned
[docs]
class BinnedEventAggregatorUnit(
BaseTransformerUnit[BinnedEventAggregatorSettings, AxisArray, AxisArray, BinnedEventAggregator]
):
SETTINGS = BinnedEventAggregatorSettings