Skip to content

estimate_onset's causal bound is unreachable: no producer ever records position.distance_m #460

Description

@mkovero

problem

MeasurementReport::ir_stats() (#346, PR #352) passes estimate_onset a causal lower bound — the earliest sample pure flight time admits — only when the report carries both a measured τ and position.distance_m:

// ac-core/src/measurement/report/ir_stats.rs, on PR #352's branch
let min_admissible_index = match (
    &self.interface_latency,
    self.position.as_ref().and_then(|p| p.distance_m),
) {
    (Some(InterfaceLatency::Measured(tau)), Some(distance_m)) => {Some() }
    _ => None,
};

Nothing that runs outside a test ever sets distance_m. Read at 298f77dc (PR #352 tip, main merged in):

  • ac-daemon/src/handlers/audio/plot.rs builds PositionSnapshot at lines 224, 548 and 954 — all three from temperature_c alone, ..Default::default() for the rest.
  • Every distance_m: Some(…) in ac-core (report/provenance.rs:348, report/ir_stats.rs:673, report_html/mod.rs:443, report_pdf/mod.rs:313,523, report_layout/sections.rs:302) is inside that file's #[cfg(test)] module.
  • ac-cli has no distance argument, config key or plumbing for plot ir.

So on real hardware min_admissible_index is always None, and every capture takes estimate_onset's unbounded ONSET_SEARCH_WINDOW_S path. The bounded path — the one #346's AC1/AC3 causality rejection and PR #352's "on the rig geometry is recorded" reasoning rest on — is exercised by unit tests and by nothing else.

evidence from the rig

#378 AC6, pupu, 2026-09-15 (record on PR #352): 48 acoustic and 10 loopback ac plot ir captures, all unbounded. On the electrical loopback, where onset and peak should nearly coincide, the pick landed 358 samples before the peak on every capture — the unbounded window reaching into the band-limited main lobe's leading skirt. A causal bound is exactly what PR #352 says rejects that; it never ran.

why it matters

This is the silent-config shape: a correctness guard that exists, is tested, reads as active in the code and the PR description, and does nothing on any real measurement. No output says the bound was skipped for want of a distance — onset_rule records "no causal bound", but nothing tells an operator that a distance could have been supplied, because it cannot.

It also means no rig session can currently test the bounded estimator at all: AC6 scored the unbounded path because that is the only one reachable.

acceptance criteria

  • An operator can record mic distance for a plot ir capture, and it reaches the persisted report's position.distance_m.
  • With a measured τ and a recorded distance, a rig capture's onset_rule shows the causal bound enforced — verified on a real capture, not only a fixture.
  • When the bound cannot be applied, the operator-facing output says which input was missing (τ, distance, or both), not only that no bound was enforced.
  • Test: a plot_ir round trip through the daemon with a supplied distance produces a report whose ir_stats() has min_admissible_index.is_some(). A test that builds PositionSnapshot by hand would pass while the producer still drops the field.

out of scope

related

#346, #352, #378 (AC6 record), #351 (τ rule on the other half of the same bound), #283.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingmeasurement-accuracyH1 estimator / THD floor / windowing / calibration correctnessready-to-implementSpec complete, developer can pick upsoftwareac Python/Rust codebasetier-1ac-core/measurement/ — QA runs the standards check

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions