Summary
RangedAggregateTransformer._hash_message hashes only message metadata:
def _hash_message(self, message: AxisArray) -> int:
axis = self.settings.axis or message.dims[0]
target_axis = message.get_axis(axis)
hash_components = (message.key,)
if hasattr(target_axis, "data"):
hash_components += (len(target_axis.data),)
elif isinstance(target_axis, AxisArray.LinearAxis):
hash_components += (target_axis.gain, target_axis.offset)
return hash(hash_components)
But _reset_state derives self._state.slices and self._state.out_axis from self.settings.bands. Since bands is not in the hash, changing it at runtime without also changing the message metadata leaves the cached slices in place, and _process keeps aggregating the old bands while reporting the new ones in settings.
update_settings covers the common path — NONRESET_SETTINGS_FIELDS is empty on this class, so a bands change through it calls _request_reset(). The gap is direct assignment:
proc.settings = dataclasses.replace(proc.settings, bands=[(10.0, 20.0)])
# no reset queued; the next message is aggregated with the previous bands
Second case: the passthrough shortcut
def __call__(self, message: AxisArray) -> AxisArray:
# Override for shortcut passthrough mode.
if self.settings.bands is None:
return message
return super().__call__(message)
Setting bands = None returns before hashing, so _hash is frozen at its pre-passthrough value. Setting bands back to a different list than before resumes with the slices from the original list.
Unlike the EWMA/scaler case (see #195), this is not about accumulated signal state — slices, out_axis, and ax_vec are all derived, so there is no discontinuity to reason about, just a stale cache producing wrong output.
Suggested fix
Add bands to the hash:
hash_components = (message.key, self.settings.bands)
bands is a list[tuple[float, float]], so it needs tuple(...) (or None) to be hashable. Requesting a reset in the bands is None branch of __call__ would also close the passthrough case.
Test to add
Assert that assigning new bands directly (not via update_settings) changes the output, and that toggling bands to None and back to a different list does not reuse the first list's slices.
Summary
RangedAggregateTransformer._hash_messagehashes only message metadata:But
_reset_statederivesself._state.slicesandself._state.out_axisfromself.settings.bands. Sincebandsis not in the hash, changing it at runtime without also changing the message metadata leaves the cached slices in place, and_processkeeps aggregating the old bands while reporting the new ones in settings.update_settingscovers the common path —NONRESET_SETTINGS_FIELDSis empty on this class, so abandschange through it calls_request_reset(). The gap is direct assignment:Second case: the passthrough shortcut
Setting
bands = Nonereturns before hashing, so_hashis frozen at its pre-passthrough value. Settingbandsback to a different list than before resumes with the slices from the original list.Unlike the EWMA/scaler case (see #195), this is not about accumulated signal state —
slices,out_axis, andax_vecare all derived, so there is no discontinuity to reason about, just a stale cache producing wrong output.Suggested fix
Add
bandsto the hash:bandsis alist[tuple[float, float]], so it needstuple(...)(orNone) to be hashable. Requesting a reset in thebands is Nonebranch of__call__would also close the passthrough case.Test to add
Assert that assigning new
bandsdirectly (not viaupdate_settings) changes the output, and that togglingbandstoNoneand back to a different list does not reuse the first list's slices.