Qu

Data & I/O

FIFO & double buffer

a small acquisition-loop simulation showing why a real sensor pipeline needs BOTH a `fifo` and a `double_buffer`, not just one or the other.

catalog/qu_fifo_double_buffer.qu

The scenario: a noisy sensor is sampled once per tick. A `fifo(capacity)` keeps a short rolling HISTORY of raw samples (for on-the-fly smoothing -- a moving average over the last few readings), while a `double_buffer` holds the single CURRENT, ready-to-read "frame" that a consumer (e.g. a display or a control loop) pulls from. The two data structures solve two different problems that a real acquisition loop always has at once:

What it exercises

How it works

Simulate a noisy sensor: a slow sine wave plus noise, sampled 12 times

N = 12
true_signal = 5 + 2 * sin(2 * pi * (0 to N - 1) / N)
noisy_samples = true_signal + 0.3 * randn(N)

considered "the current reading" a downstream consumer may read.

history = fifo(4)
frame = double_buffer(0.0)

print("Acquisition loop -- fifo(4) rolling history feeding a double-buffered frame")
for k = 0 to N - 1
    was_full_before = history.is_full()
    history.push(noisy_samples[k])

    # Moving average over whatever is currently in the ring (<=4 samples).
    n_in_ring = len(history)
    total = 0
    tmp = fifo(4)
    for i = 0 to n_in_ring - 1
        v = history.pop()
        total = total + v
        tmp.push(v)          # preserve the samples we just drained
    end for
    for i = 0 to n_in_ring - 1
        history.push(tmp.pop())
    end for
    avg = total / n_in_ring

    # Stage the smoothed value in the back buffer; the front buffer (what
    # `.read()` returns) does NOT change until we explicitly `.swap()`.
    stale_reading = frame.read()
    frame.write(avg)
    reading_before_swap = frame.read()
    frame.swap()
    reading_after_swap = frame.read()

    print("tick {k}: raw={noisy_samples[k]:.3f} ring_full_before_push={was_full_before} " +
          "avg={avg:.3f} pre_swap_read={reading_before_swap:.3f} post_swap_read={reading_after_swap:.3f}")
end for

print("")
print("Final rolling history (oldest to newest): peek={history.peek():.3f}, count={len(history)}, is_full={history.is_full()}")
print("Final current frame (front buffer): {frame.read():.3f}")

Plot the raw noisy samples against the ground truth for context

t = 0 to N - 1
plot(t, true_signal, label="true signal", color="black")
plot(t, noisy_samples, label="noisy sensor sample", color="gray")
xlabel("Tick")
ylabel("Value")
title("Sensor acquisition: fifo history + double-buffered current frame")
legend()
show plot

Results

printed
Acquisition loop -- fifo(4) rolling history feeding a double-buffered frame
tick 0: raw=5.696 ring_full_before_push=false avg=5.696 pre_swap_read=0.000 post_swap_read=5.696
tick 1: raw=5.702 ring_full_before_push=false avg=5.699 pre_swap_read=5.696 post_swap_read=5.699
tick 2: raw=6.242 ring_full_before_push=false avg=5.880 pre_swap_read=5.699 post_swap_read=5.880
tick 3: raw=7.298 ring_full_before_push=false avg=6.235 pre_swap_read=5.880 post_swap_read=6.235
tick 4: raw=7.049 ring_full_before_push=true avg=6.573 pre_swap_read=6.235 post_swap_read=6.573
tick 5: raw=6.075 ring_full_before_push=true avg=6.666 pre_swap_read=6.573 post_swap_read=6.666
tick 6: raw=5.103 ring_full_before_push=true avg=6.381 pre_swap_read=6.666 post_swap_read=6.381
tick 7: raw=4.239 ring_full_before_push=true avg=5.617 pre_swap_read=6.381 post_swap_read=5.617
tick 8: raw=2.939 ring_full_before_push=true avg=4.589 pre_swap_read=5.617 post_swap_read=4.589
tick 9: raw=2.898 ring_full_before_push=true avg=3.795 pre_swap_read=4.589 post_swap_read=3.795
tick 10: raw=2.913 ring_full_before_push=true avg=3.247 pre_swap_read=3.795 post_swap_read=3.247
tick 11: raw=4.322 ring_full_before_push=true avg=3.268 pre_swap_read=3.247 post_swap_read=3.268

Final rolling history (oldest to newest): peek=2.939, count=4, is_full=true
Final current frame (front buffer): 3.268
3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0 7.5 0 1 2 3 4 5 6 7 8 9 10 11 Sensor acquisition: fifo history + double-buffered current frame Tick Value true signal noisy sensor sample
Drawn by running the program above.

The whole program

Runnable as it stands: qu run catalog/qu_fifo_double_buffer.qu

# qu_fifo_double_buffer.qu -- a small acquisition-loop simulation showing

# why a real sensor pipeline needs BOTH a `fifo` and a `double_buffer`, not

# just one or the other.

#

# The scenario: a noisy sensor is sampled once per tick. A `fifo(capacity)`

# keeps a short rolling HISTORY of raw samples (for on-the-fly smoothing --

# a moving average over the last few readings), while a `double_buffer`

# holds the single CURRENT, ready-to-read "frame" that a consumer (e.g. a

# display or a control loop) pulls from. The two data structures solve two

# different problems that a real acquisition loop always has at once:

#   - fifo: bounded rolling history, oldest sample evicted automatically

#     once the ring is full -- exactly what a moving-average window needs.

#   - double_buffer: the consumer must NEVER see a half-written frame --

#     `.write` only ever touches the back buffer, so the smoothed value is

#     only exposed via `.read()` once `.swap()` makes it current.

#

# Constructs exercised (all real, verified-working builtins):

#   fifo(capacity)               -- bounded ring buffer of raw samples

#   f.push(value), f.pop()        -- ring-buffer push/evict, FIFO pop

#   f.peek(), f.is_full(), f.is_empty()

#   len(f)                       -- element count

#   double_buffer(initial)        -- front/back current-frame holder

#   b.write(value), b.read(), b.swap()


backend auto
seed(7)

#%% Simulate a noisy sensor: a slow sine wave plus noise, sampled 12 times

N = 12
true_signal = 5 + 2 * sin(2 * pi * (0 to N - 1) / N)
noisy_samples = true_signal + 0.3 * randn(N)

#%% A 4-sample rolling-history FIFO feeds a moving average into a

#%% double-buffered "current frame" the way a real acquisition loop would:

#%% raw samples land in the fifo every tick; only a SWAPPED frame is ever

#%% considered "the current reading" a downstream consumer may read.

history = fifo(4)
frame = double_buffer(0.0)

print("Acquisition loop -- fifo(4) rolling history feeding a double-buffered frame")
for k = 0 to N - 1
    was_full_before = history.is_full()
    history.push(noisy_samples[k])

    # Moving average over whatever is currently in the ring (<=4 samples).

    n_in_ring = len(history)
    total = 0
    tmp = fifo(4)
    for i = 0 to n_in_ring - 1
        v = history.pop()
        total = total + v
        tmp.push(v)          # preserve the samples we just drained

    end for
    for i = 0 to n_in_ring - 1
        history.push(tmp.pop())
    end for
    avg = total / n_in_ring

    # Stage the smoothed value in the back buffer; the front buffer (what

    # `.read()` returns) does NOT change until we explicitly `.swap()`.

    stale_reading = frame.read()
    frame.write(avg)
    reading_before_swap = frame.read()
    frame.swap()
    reading_after_swap = frame.read()

    print("tick {k}: raw={noisy_samples[k]:.3f} ring_full_before_push={was_full_before} " +
          "avg={avg:.3f} pre_swap_read={reading_before_swap:.3f} post_swap_read={reading_after_swap:.3f}")
end for

print("")
print("Final rolling history (oldest to newest): peek={history.peek():.3f}, count={len(history)}, is_full={history.is_full()}")
print("Final current frame (front buffer): {frame.read():.3f}")

#%% Plot the raw noisy samples against the ground truth for context

t = 0 to N - 1
plot(t, true_signal, label="true signal", color="black")
plot(t, noisy_samples, label="noisy sensor sample", color="gray")
xlabel("Tick")
ylabel("Value")
title("Sensor acquisition: fifo history + double-buffered current frame")
legend()
show plot