Skip to content

ResampleProcessor: max_chunk_delay wall-clock extrapolation is unreachable (src.available() < 3 early return) #179

Description

@cboulay

Problem

ResampleSettings.max_chunk_delay documents: "Maximum delay between outputs in seconds. If the delay exceeds this value, the transformer will extrapolate." In practice this extrapolation can never fire once input stops, because ResampleProcessor.__next__ returns early before the extrapolation condition (b_project) is ever evaluated.

The sequence:

  1. On each productive __next__, the source buffer is seeked to keep only ~2 samples (one sample before the last resampled value onward):
    seek_ix = np.where(x >= xnew[-1])[0]
    if len(seek_ix) > 0:
        self.state.src_buffer.seek(max(0, src_start_ix + seek_ix[0] - 1))
  2. The next call hits the guard at the top of __next__ and returns the empty template:
    if ref.is_empty() or src.available() < 3:
        return ...  # empty
  3. The b_project check (resample_rate is not None and time.monotonic() > last_write_time + max_chunk_delay) sits below that guard, so it is never reached.

This is independent of how the unit polls or wakes — it was equally unreachable under the previous busy-wait publisher (which called next() continuously), and remains so under the event-driven publisher from #178, whose timed wake fires at the right moment but finds the same early return.

Reproduction

import time
import numpy as np
from ezmsg.util.messages.axisarray import AxisArray
from ezmsg.sigproc.resample import ResampleProcessor, ResampleSettings

def mk(fs, offset, n):
    t = offset + np.arange(n) / fs
    return AxisArray(data=t[:, None], dims=["time", "ch"],
                     axes={"time": AxisArray.LinearAxis(gain=1/fs, offset=offset, unit="s")}, key="sig")

proc = ResampleProcessor(settings=ResampleSettings(
    axis="time", resample_rate=90.0, buffer_duration=4.0, max_chunk_delay=0.05))
for i in range(5):
    proc(mk(100.0, i * 0.3, 30))
    next(proc)

print(proc.state.src_buffer.available())  # 2  -> below the < 3 guard
time.sleep(0.06)                          # exceed max_chunk_delay
r = next(proc)
print(r.data.shape[0])                    # 0 -- expected: extrapolated samples past input end

Possible fix

Linear scipy.interpolate.interp1d needs only 2 points, so the src.available() < 3 guard could be relaxed (to < 2, or bypassed specifically when b_project is true) allowing the extrapolation path to run from the retained tail of the source buffer. Care needed:

  • repeated timeouts with no input should not emit unbounded/duplicate extrapolations — the high-water mark (last_ref_ax_val) already appears to bound this to one max_chunk_delay window past the reference buffer end, but that should be pinned down with a test;
  • fill_value="last" vs "extrapolate" behave differently out-of-bounds and both should be covered.

Test hook

tests/integration/ezmsg/test_resample_system.py::test_resample_system_prescribed_rate (added in #178) documents this limitation in its docstring and is the natural place to assert output beyond the input end once the guard is reworked. The unit-side timed wake is already in place, so no unit changes should be needed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions