fix: condition-probe assertion uses snapshot width, not mock-clock phase

The old `phase=` field compared `trigger_time()` against the mock's absolute
`glitch_at + glitch_width` target.  The app's stream epoch is the first packet
it received, not the mock's t = 0; the two clocks differ by however many
packets the CONNECT handshake cost, making the comparison meaningless.

Replace it with a snapshot-relative measurement: scan backwards from x = 0 in
the held event snapshot for the most recent upward crossing of `condition.level`
(the same `prev < level && val >= level` test the detector uses) and negate the
crossing's stamp to get the measured pulse width.  A `measured` near 0.002 s is
the glitch caught exactly; near 0.200 s means an ordinary edge was latched.

Remove the now-dead `glitch_at`, `glitch_width`, `square_period` fields from
`ConditionProbe`.  No other files changed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Sonnet 4.6 committed 2026-10-07 17:48:25 +02:00
1 parent d147678b1c
commit 0876ecfe22
1 file changed
+49 -15
+49 -15
View File
@@ -8403,17 +8403,26 @@ gboolean fields_probe(gpointer user) {
/// Headless self-check for the qualified conditions: arms a pulse-width condition on the
/// mock's `square`, whose narrow pulse the mock injects once per square period, and reports
/// the latched t0 against where the pulse's closing edge must be.
/// the measured pulse width from the held event snapshot.
///
/// The assertion is the *phase* rather than an absolute t0: the probe arms at whatever stream
/// time the app happened to reach, so which glitch it catches is not fixed, but every glitch
/// closes at `--glitch-at + --glitch-width + k * (2 / --freq)`. A `phase` of a few sample
/// periods is the pulse being found exactly; a phase near half the square period means the
/// condition fired on an ordinary edge instead.
/// The assertion is snapshot-relative rather than an absolute t0. The app's stream clock
/// epoch is the first packet it received, not the mock's t = 0; the two clocks differ by
/// however many packets the CONNECT handshake cost (typically a whole number of 100-sample /
/// 10 ms packets), so comparing trigger_time() against a mock-clock target is meaningless.
///
/// A held event snapshot's stamps are edge-relative: stamps == 0 is the latching event,
/// which for a pulse-width condition is the pulse's *closing* edge. The pulse's opening
/// edge therefore sits at stamps ≈ -glitch_width. The probe scans backwards from x = 0 to
/// find the most recent upward crossing of level 0 (the same prev < level && value >= level
/// test the detector uses) and negates the crossing's stamp to get the measured pulse width.
///
/// A `measured` near 0.002 s means the condition caught the 2 ms glitch pulse exactly.
/// A `measured` near 0.200 s (the ordinary square half-period) means the condition fired on
/// an ordinary edge rather than the narrow pulse -- that is the wrong answer to distinguish
/// against. If no snapshot is available or no upward crossing is found before x = 0, the
/// line still prints with `measured=nan` rather than being skipped, so a failure is still
/// one readable line.
struct ConditionProbe {
double glitch_at = 0.25;
double glitch_width = 0.002;
double square_period = 0.4;
int waited = 0;
};
ConditionProbe g_condition;
@@ -8428,13 +8437,38 @@ gboolean condition_probe_report(gpointer user) {
std::fflush(stderr);
return G_SOURCE_REMOVE;
}
const double want = g_condition.glitch_at + g_condition.glitch_width;
double phase = std::fmod(t0 - want, g_condition.square_period);
if (phase < 0.0) phase += g_condition.square_period;
if (phase > 0.5 * g_condition.square_period) phase -= g_condition.square_period;
std::fprintf(stderr, "[condition-probe] type=%d cmp=%d width=%.6f t0=%.6f phase=%.6f mode=%d\n",
// Measure the pulse width from the snapshot's own edge-relative stamps rather than
// comparing t0 against a mock-clock target. stamps == 0 is the closing edge (the
// latching event for a pulse-width condition); the opening edge sits at stamps ≈ -width.
// Scan backwards from the last sample at or before x = 0 for the most recent upward
// crossing of the condition level, then negate the crossing stamp to get the width.
double measured = std::numeric_limits<double>::quiet_NaN();
const double level = a->model->trigger().condition.level;
const std::size_t trig_ch = static_cast<std::size_t>(a->model->trigger().channel);
if (std::shared_ptr<const EventSnapshot> snap = a->model->display_snapshot()) {
if (snap != nullptr && trig_ch < snap->values.size()) {
const std::vector<f64>& st = snap->stamps;
const std::vector<f32>& vals = snap->values[trig_ch];
if (vals.size() == st.size() && !st.empty()) {
// Find the last index whose stamp is <= 0.
const auto it = std::upper_bound(st.begin(), st.end(), 0.0);
std::size_t end = static_cast<std::size_t>(it - st.begin());
// Scan backwards for the most recent upward crossing (prev < level, value >= level).
for (std::size_t i = end; i >= 2; --i) {
const double prev = static_cast<double>(vals[i - 2]);
const double val = static_cast<double>(vals[i - 1]);
if (prev < level && val >= level) {
measured = -st[i - 1];
break;
}
}
}
}
}
std::fprintf(stderr, "[condition-probe] type=%d cmp=%d width=%.6f t0=%.6f measured=%.6f mode=%d\n",
int(a->model->trigger().condition.type), int(a->model->trigger().condition.cmp),
a->model->trigger().condition.width_s, t0, phase, int(a->model->display_mode()));
a->model->trigger().condition.width_s, t0, measured,
int(a->model->display_mode()));
std::fflush(stderr);
return G_SOURCE_REMOVE;
}