Skip to content

Add ResampleConcat; harden Resample against non-monotonic reference clocks#161

Merged
cboulay merged 1 commit into
devfrom
feature/resample-concat-nonmonotonic
Jun 30, 2026
Merged

Add ResampleConcat; harden Resample against non-monotonic reference clocks#161
cboulay merged 1 commit into
devfrom
feature/resample-concat-nonmonotonic

Conversation

@cboulay

@cboulay cboulay commented Jun 30, 2026

Copy link
Copy Markdown
Member

Summary

Combining two acquisition streams that share a nominal rate but drift slightly (e.g. two hubs at ~30 kHz with 0.1–0.5% effective-rate mismatch) is done by resampling one stream onto the other's clock and concatenating along the channel axis. The existing Resample → Merge graph has two failure modes when the reference clock misbehaves:

  1. Seizing. A backward jump in the reference clock leaves every incoming reference value behind the resampler's high-water mark, so it stops emitting and the whole graph hangs.
  2. Silent misalignment. When the resampler holds back or drops reference samples, the raw reference stream feeding MERGE.INPUT_SIGNAL_A is no longer sample-aligned with the resampled stream — Align does a one-time offset alignment then reads in lockstep, so it concatenates mismatched samples.

Changes

resample.py

  • output_reference setting + new OUTPUT_REFERENCE stream: buffers the reference data and emits it gathered onto the exact grid the signal was resampled onto, so a downstream Concat can combine the two with no second time-alignment (aligned by construction).
  • reference_reset_after_chunks (default 3): detects a sustained backward clock jump and re-anchors the high-water mark with a RuntimeWarning instead of seizing. Self-correcting jitter is dropped, keeping output monotonic. Set to inf to restore the previous behaviour.
  • push_reference CoordinateAxis fix: no longer unconditionally reads ax.gain (which crashed on a per-sample-timestamp reference).

resampleconcat.py (new)

  • ResampleConcat fuses ResampleProcessor (output_reference=True) and ConcatProcessor into a single linear two-input unit — no Align, no diamond graph. Reuses all interpolation logic from resample.py and all axis/attr-merge logic from concat.py; only the orchestration is new.

Tests

  • tests/unit/test_resample.py: CoordinateAxis reference, output_reference grid-alignment, reset recovery, and reset-disabled-stalls.
  • tests/unit/test_resampleconcat.py (new): two-rate combine with numeric provenance check, no-signal, reset recovery.
  • tests/integration/ezmsg/test_resample_merge_system.py (new): healthy graph; seize when recovery disabled (documents the original bug); recovery via OUTPUT_REFERENCE; composite recovery.

Full suite: 3596 passed, 5 skipped locally.

Notes

Output across a genuine reference clock reset is necessarily discontinuous — the hardening is a guardrail (don't hang, warn loudly) rather than a substitute for sanitising reference timestamps upstream.

Resampling one stream onto another stream's clock and concatenating the
result is a common pattern (e.g. merging two acquisition hubs with
slightly different effective rates). The existing Resample -> Merge graph
has two problems when the reference clock misbehaves:

1. A backward jump in the reference clock leaves every incoming reference
   value behind the resampler's high-water mark, so it stops producing
   output and the whole graph seizes.
2. Even when the resampler holds back or drops reference samples, the raw
   reference stream feeding Merge.INPUT_SIGNAL_A is no longer sample-aligned
   with the resampled stream (Align does a one-time alignment, then reads
   in lockstep), silently concatenating mismatched samples.

Changes to resample.py:
- output_reference setting: buffer reference data and emit it gathered onto
  the exact grid the signal was resampled onto, via a new OUTPUT_REFERENCE
  stream. A downstream Concat can combine the two with no second alignment.
- reference_reset_after_chunks: detect a sustained backward clock jump and
  re-anchor the high-water mark (with a RuntimeWarning) so output resumes
  instead of seizing. Self-correcting jitter is dropped, keeping output
  monotonic. Set to inf to restore the previous behaviour.
- push_reference now accepts a CoordinateAxis reference (no .gain attr).

New resampleconcat.py: ResampleConcat fuses ResampleProcessor
(output_reference=True) and ConcatProcessor into a single linear two-input
unit, aligned by construction -- no Align, no diamond graph. Reuses all
interpolation and axis/attr-merge logic from resample.py and concat.py.

Tests: unit coverage for the new resample features and the composite, plus
integration tests covering the healthy graph, the seize when recovery is
disabled, recovery via OUTPUT_REFERENCE, and the composite.
@cboulay cboulay merged commit 28f69d8 into dev Jun 30, 2026
14 checks passed
@cboulay cboulay deleted the feature/resample-concat-nonmonotonic branch June 30, 2026 05:37
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