Keep reconstructed timestamps monotone across segment joins - #21
Merged
Conversation
Real-gap handling fits each inter-gap segment independently so the true jump survives, which leaves the joins unconstrained: a segment's smoothed first sample is an estimate, and the shared path re-anchors every segment on its own median. Segment k+1 could therefore begin before segment k ended. On a real recording (a hub whose stamps were written at the wrong intra-chunk rate, rescued via its PTP sibling) the reconstruction emitted two backward steps of 33 sample periods -- 1.1 ms of time running backwards in a vector the gap-splitter and chunk anchoring both read as monotone. Re-cut a broken join to the jump the raw timestamps show across it, floored at one sample period, shifting the whole following segment to match and carrying the shift cumulatively. Using the raw jump is what keeps a real gap intact: shifting by just enough to restore order would collapse it to a single period, and the raw stamps are the only evidence left of how long the recording actually stopped. Healthy joins are untouched. Also give the shared-model gate an absolute floor (SHARED_MODEL_JITTER_FLOOR_ PERIODS) alongside the ratio, which removes the clean_resid > 0 special case: a perfect reference (regenerated stamps self-fit to ~1e-14 s, or exactly 0) made the ratio test rescue nothing precisely when the reference was most worth borrowing. A target must now clear both bars, so two already-clean streams never trigger each other however far apart their ratio falls. Validated on the file that surfaced it: backward steps 2 -> 0, and the clean session's two hubs are unchanged (0 gaps, 0 backsteps, 30000.10/30000.09 Hz). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two defects in the real-gap / shared-model reconstruction
1. Segment joins can run backwards
Real-gap handling fits each inter-gap segment independently so the true jump survives, which leaves the joins unconstrained: a segment's smoothed first sample is an estimate, and
reconstruct_sharedre-anchors every segment on its own median. Segmentk+1could therefore begin before segmentkended.Found on a real recording — a hub whose stamps were written at the wrong intra-chunk rate (32063.6 Hz against 30000.4 delivered), rescued via its PTP sibling. The reconstruction emitted two backward steps of 33 sample periods (1.1 ms) in a vector that
find_gapsand chunk anchoring both read as monotone, and that the module docstring promises is "smoothed, strictly monotone".The repair re-cuts a broken join to the jump the raw timestamps show across it, floored at one sample period, shifting the whole following segment to match and carrying the shift cumulatively. Using the raw jump is what keeps a real gap intact — shifting by just enough to restore order would collapse it to a single period, and the raw stamps are the only evidence left of how long the recording actually stopped. Healthy joins are untouched. Shifting rather than clamping preserves each segment's internal shape;
np.maximum.accumulatewould leave plateaus of duplicate stamps.2.
clean_resid > 0.0inverts the gate's intent at the limitA perfect reference — stamps regenerated as
i / fsself-fit to ~1e-14 s, or exactly 0 — made the ratio test rescue nothing, precisely when the reference is most worth borrowing.SHARED_MODEL_JITTER_FLOOR_PERIODS(1.0) now sits alongside the ratio, and the threshold ismax(RATIO * clean_resid, FLOOR * period), which subsumes the special case: a perfect source gives a ratio term of zero and the floor decides. A target must clear both bars, so two already-clean streams never trigger each other however far apart their ratio falls.One period separates the observed populations comfortably: clean recordings self-fit to 0.3–0.5 periods, the wrong-rate stream lands at ~37.
Validation
On the file that surfaced it: backward steps 2 → 0, and the clean session's two hubs are unchanged (0 gaps, 0 backsteps, 30000.105 / 30000.093 Hz). The new shared-path test is not vacuous — replaying the previous segment loop on the same input gives a −4.0 ms step.
8 new tests; 213 pass.
🤖 Generated with Claude Code