Skip to content

Delegate EventRate binning to shared BinSchedule; add fractional=False mode#33

Merged
cboulay merged 6 commits into
devfrom
bin-schedule-delegation
Jun 26, 2026
Merged

Delegate EventRate binning to shared BinSchedule; add fractional=False mode#33
cboulay merged 6 commits into
devfrom
bin-schedule-delegation

Conversation

@cboulay

@cboulay cboulay commented Jun 26, 2026

Copy link
Copy Markdown
Member

Summary

EventRate (via BinnedKernelActivation) computed its bin grid — samples-per-bin, fractional carry, global bin index, output gain/offset — inline. That arithmetic was a duplicate of what ezmsg-sigproc's dense binner does, and the two only agreed by coincidence, which is what caused SBP-vs-SPK timestamp mismatches at off-nominal sample rates (e.g. ~30012 Hz) where a downstream Merge/AlignAlongAxis never lined up.

This routes BinnedKernelActivation's boundaries through the shared ezmsg.sigproc.util.binning.BinSchedule (shipped in ezmsg-sigproc v2.26.0), so the spike-rate branch and any sigproc binner land on an identical grid by shared code, not by luck.

Changes

  • Delegate boundaries to BinSchedule. BinnedKernelActivationState drops bin_accumulator in favor of a BinSchedule (fractional by default — EventRate bins track the nominal bin_duration). _process_events and _process_dense_count_sum now get n_bins, chunk-local bin ends, output gain and output offset from BinSchedule.advance(); the per-bin reduction and activation-state logic are unchanged.
  • New fractional=False (sample-locked) mode. EventRateSettings/BinnedKernelActivationSettings gain a fractional field. When False, bins are sample-locked to int(bin_duration*fs) samples — matching ezmsg.sigproc.window.Window — so a sample-locked SBP branch and the spike-rate branch can share one grid that way too.
  • Honest rate normalization. Output axis gain and rate_normalize now use the schedule's actual seconds-per-bin (step.output_gain) instead of the nominal bin_duration. In fractional mode these are identical, so default behavior is byte-for-byte unchanged.
  • Dependency: ezmsg-sigproc>=2.26.0 (ships BinSchedule).

Tests

  • New tests/test_bin_schedule_crosspackage.py: asserts EventRate and BinnedAggregate produce the same output time axis and bin counts under adversarial chunking at fs ∈ {1000, 30012, 30030} — for both fractional=True (shared wall-clock grid) and fractional=False (shared sample-locked grid, matching Window's gain).
  • Full suite green against released ezmsg-sigproc 2.26.0: 137 passed. The default fractional=True path is unchanged.

Context

Companion to ezmsg-sigproc's BinSchedule extraction (PR into generalize-binner, released in 2.26.0). Together they replace "agreement with EventRate by construction, untested" with one boundary primitive plus regression tests on both sides.

cboulay added 4 commits June 26, 2026 00:21
EventRate's bin grid (samples-per-bin, fractional carry, global bin index,
output gain/offset) was computed inline and duplicated the same arithmetic
ezmsg-sigproc's dense binner uses. Both now consume the shared
ezmsg.sigproc.util.binning.BinSchedule, so they land on an identical grid by
shared code rather than by coincidence.

- BinnedKernelActivationState drops bin_accumulator in favor of a BinSchedule
  (fractional, since EventRate bins always track the nominal bin_duration).
- _process_events and _process_dense_count_sum now get n_bins, chunk-local bin
  ends, output gain and output offset from BinSchedule.advance(); the per-bin
  reduction and activation-state logic are unchanged.
- New cross-package test asserts EventRate and BinnedAggregate(SUM) produce the
  same output time axis and bin counts at fs in {1000, 30012, 30030} under
  adversarial chunking -- the alignment the shared primitive guarantees.

Requires an ezmsg-sigproc release that ships BinSchedule; the dependency pin
will need bumping before this can merge.
BinSchedule already carries the fractional flag, so exposing a sample-locked
mode on EventRate is just plumbing plus using the schedule's actual gain.

- EventRateSettings/BinnedKernelActivationSettings gain a `fractional` field
  (default True; threaded through Rate). False bins on Window's
  int(bin_duration*fs) grid so a sample-locked SBP branch and the spike-rate
  branch share one grid.
- Both output paths now label the axis with step.output_gain (the schedule's
  actual seconds-per-bin) instead of the nominal bin_duration, and
  rate_normalize divides by that same actual duration. In fractional mode
  output_gain == bin_duration, so default behavior is unchanged (full suite
  still green).
- Cross-package test asserts EventRate(fractional=False) is sample-locked to
  Window's gain and shares the grid with BinnedAggregate(fractional=False).
binned.py still carried its own integer-locked event-counting binner, the last
event binner not on the shared grid. Rewrite it as a thin wrapper over
ezmsg.sigproc.binned_aggregate.BinnedAggregate (operation=SUM): densify sparse
events to per-sample contributions, delegate all boundary/carry/axis math to
the shared binner, and optionally rate-normalize.

Ported from #32 (Kyle McGraw), adjusted for the enhanced sigproc shipped in
2.26.0:
- fractional=False is documented and tested as int(bin_duration*fs) truncation
  (matching Window and BinSchedule), not round(); the sample-locked test now
  includes fs=30030 where truncation (600) and rounding (601) diverge.
- Adds scale_by_value (weight events by stored value), a binned_event_aggregator
  factory, and tests for the off-nominal fractional grid, chunk invariance,
  count-vs-rate scaling, scale_by_value, and the sample-locked grid.

This leaves Rate/EventRate on the kernel_activation path (event-optimized,
O(n_events) sparse) from the companion commits; BinnedEventAggregator is now
also on the shared grid, so no event binner remains on the old divergent one.
cboulay added 2 commits June 26, 2026 01:43
Drop references to consumer-pipeline concepts that public users of these
packages won't recognize -- named "spike-band-power"/"spike-rate" branches, the
"downstream Merge with no post-hoc reconciler" narrative, "real recordings", and
the confusing "wall-clock" framing. Describe the behavior in terms of this
package and the sigproc binner it delegates to (BinnedAggregate, Window,
BinSchedule, Merge, ThresholdCrossing) instead. Comment-only.
The small functional-constructor helpers are a deprecated pattern; new modules
shouldn't add them. BinnedEventAggregator(BinnedEventAggregatorSettings(...)) is
the supported construction path. Unused — no callers.
@cboulay

cboulay commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

Relationship to #32

Both PRs put the event binners on sigproc's shared BinSchedule (released in sigproc 2.26.0); they differ in how EventRate gets there:

This PR also carries over #32's BinnedEventAggregator modernization (so no event binner is left on the old divergent grid), adjusted for 2.26.0's int() truncation — see the note on #32. It additionally adds a fractional=False sample-locked mode to EventRate and a cross-package grid-equality test.

Net: EventRate stays sparse-efficient, and both event binners share one bin-boundary implementation with sigproc. Supersedes #32.

@cboulay cboulay merged commit 162014e into dev Jun 26, 2026
4 checks passed
@cboulay cboulay deleted the bin-schedule-delegation branch June 26, 2026 17:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant