From 5126bd3b05a48ebaeb7dceefd6fcc1a5cf1ca638 Mon Sep 17 00:00:00 2001 From: ruv Date: Fri, 22 May 2026 07:57:37 -0400 Subject: [PATCH] =?UTF-8?q?research(R20.2):=20threshold-based=20hand-off?= =?UTF-8?q?=20=E2=80=94=20works=20at=200.5=20m,=20harmonic=20gap=20at=201?= =?UTF-8?q?=20m=20surfaces=20Pan-Tompkins=20requirement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements R20.1's catalogued refinement: when NV conf > 60% AND amplitude > 3 pT, trust NV entirely. Mixed result (5 distances): - 0.5 m: NV=72.00 ✓, smart=72.0 (+0.0 error, NV trusted) ✓ - 1.0 m: NV=144 (harmonic!), smart trusts wrong NV (+72 BPM error) - 1.5 m+: falls back to weighted (NV conf below threshold) Production lesson: the threshold-based policy is correct in spirit but incorrect with simple FFT rate estimator (picks harmonics). Production needs: 1. Harmonic rejection (Pan-Tompkins QRS or autocorrelation) 2. Cross-check vs breathing band 3. Per-frame plausibility window R20.1's 'production needs Pan-Tompkins' note is confirmed BINDING, not nice-to-have, before threshold hand-off can ship. ADR-114 implementation budget refined: +30-50 LOC for Pan-Tompkins. Five-step quantum arc: - R20 vision (tick 37) - Doc 17 bridge (tick 38) - ADR-114 spec (tick 39) - R20.1 working demo (tick 40) - R20.2 threshold refinement (this tick) Production ADR-114 cog now has all known refinements catalogued BEFORE any Rust code is written. Honest mixed result — catalogue-then-revisit pattern works: R20.1 flagged production gap; R20.2 attempted fix; fix surfaced deeper gap (harmonic rejection). Three layers of refinement. --- .../R20_2-threshold-handoff.md | 66 ++++++++ .../r20_2_threshold_handoff.py | 142 ++++++++++++++++++ .../r20_2_threshold_results.json | 71 +++++++++ 3 files changed, 279 insertions(+) create mode 100644 docs/research/sota-2026-05-22/R20_2-threshold-handoff.md create mode 100644 examples/research-sota/09-quantum-fusion/r20_2_threshold_handoff.py create mode 100644 examples/research-sota/09-quantum-fusion/r20_2_threshold_results.json diff --git a/docs/research/sota-2026-05-22/R20_2-threshold-handoff.md b/docs/research/sota-2026-05-22/R20_2-threshold-handoff.md new file mode 100644 index 0000000000..a744d468dd --- /dev/null +++ b/docs/research/sota-2026-05-22/R20_2-threshold-handoff.md @@ -0,0 +1,66 @@ +# R20.2 — Threshold-based hand-off: mixed result reveals production gap + +**Status:** implementation of R20.1's catalogued refinement; mixed result reveals harmonic-rejection requirement · **2026-05-22** + +## What R20.2 set out to fix + +R20.1's naive precision-weighted Bayesian gave 84 BPM for HR when classical (105 BPM, 38% conf) disagreed with NV @ 1 m (72 BPM, 64% conf). The fix specified: when NV confidence > 60% AND amplitude > 3 pT, trust NV entirely. + +## Result (5 distances) + +| Distance | NV amp | NV rate | NV conf | Naive | Smart | Error (smart) | Regime | +|---:|---:|---:|---:|---:|---:|---:|---| +| **0.5 m** | 50.00 pT | 72.00 ✓ | 84% | 82.3 | **72.0** | **+0.0** ✓ | nv_drives | +| 1.0 m | 6.25 pT | 144.00 ✗ harmonic | 67% | 129.9 | **144.0** | **+72.0 ✗** | nv_drives | +| 1.5 m | 1.85 pT | 72.00 ✓ | 39% | 88.3 | 88.3 | +16.3 | weighted_fallback | +| 2.0 m | 0.78 pT | 77.00 | 36% | 91.5 | 91.5 | +19.5 | weighted_fallback | +| 3.0 m | 0.23 pT | 78.00 | 38% | 91.5 | 91.5 | +19.5 | weighted_fallback | + +## What this reveals + +- **At 0.5 m**: threshold hand-off works perfectly (+0.0 error, NV trusted, breathing+HR correct) +- **At 1 m**: smart hand-off **loses** to naive because the simple FFT picked a 2× harmonic of the true HR (144 vs 72) +- **At 1.5-3 m**: falls back to weighted (NV below confidence threshold), same as naive + +## The production lesson + +The threshold-based policy is **correct in spirit** (trust NV when good) but **incorrect with simple FFT** (which picks harmonics for narrow-band signals). Production needs: + +1. **Harmonic rejection** in the rate estimator (e.g. autocorrelation-based, or Pan-Tompkins QRS for cardiac signals) +2. **Cross-check with classical breathing rate band** (true HR is rarely > 2× breathing rate × 6; the 144 result violates this and could be rejected) +3. **Per-frame plausibility window** (a healthy adult won't transition from 72 to 144 BPM in 1 second) + +R20.1's note already flagged "production needs Pan-Tompkins QRS detection". R20.2 confirms this is **binding, not nice-to-have** for the threshold hand-off to be safe. + +## What R20.2 DOES enable + +1. **Empirical confirmation** that the smart hand-off works at 0.5 m bedside (target deployment scenario per ADR-114). +2. **Identification of a critical production gap**: harmonic rejection in the rate estimator is mandatory before threshold hand-off can ship. +3. **Refined ADR-114 implementation budget**: add ~30-50 LOC for Pan-Tompkins QRS detection. + +## What R20.2 DOES NOT enable + +- A clean win across all distances — the 1 m harmonic shows real-world robustness needs more work. +- Validation on real cardiac signals (synthetic Gaussian-pulse-train; real ECG/cardiac-B has different harmonic structure). +- Multi-subject hand-off (single subject only). + +## Honest scope + +This is a **mixed result, honestly reported**. The smart hand-off is right in principle; the FFT rate estimator beneath it is the weak link. Production fix is well-understood (Pan-Tompkins or autocorrelation), but the demo as written doesn't include it. + +## Composes with + +- R20.1 (this is the catalogued refinement) +- ADR-114 (production implementation needs Pan-Tompkins per R20.2) +- R13 NEGATIVE (this confirms classical HR is unusable, which is why we need NV at all) +- Doc 16 (cube-of-distance: at 3 m NV is below threshold and we fall back to weighted) + +## Honest meta-observation + +R20.2 is the **5-minute follow-up** to R20.1. The catalogue-then-revisit pattern works: R20.1 flagged production gap; R20.2 attempted the fix; the attempt surfaced a deeper gap (harmonic rejection). Three layers of refinement in one quantum integration arc. + +## Connection back + +R20 (vision, tick 37) → Doc 17 (bridge, tick 38) → ADR-114 (spec, tick 39) → R20.1 (working demo, tick 40) → **R20.2 (threshold refinement, this tick)**. + +Five-step quantum integration arc. Production ADR-114 cog now has all known refinements catalogued before any Rust code is written. diff --git a/examples/research-sota/09-quantum-fusion/r20_2_threshold_handoff.py b/examples/research-sota/09-quantum-fusion/r20_2_threshold_handoff.py new file mode 100644 index 0000000000..f76ca7a657 --- /dev/null +++ b/examples/research-sota/09-quantum-fusion/r20_2_threshold_handoff.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""R20.2 — Threshold-based hand-off fix for ADR-114 Bayesian fusion. + +See docs/research/sota-2026-05-22/R20_2-threshold-handoff.md. + +R20.1's naive precision-weighted Bayesian fusion gave 84 BPM for HR when +classical (105 BPM, 38% conf) and NV @ 1 m (72 BPM, 64% conf) disagreed. +Production needs threshold-based hand-off: when NV confidence > 60% +AND B-field amplitude > 3 pT, trust NV entirely (reject classical HR). + +This implements the fix and verifies it recovers correct HR (72 BPM) +at bedside while gracefully degrading to classical when NV degrades. + +Pure NumPy. Reuses R20.1 simulators. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import sys +import numpy as np + +# Reuse R20.1 simulator functions by importing them +sys.path.insert(0, str(Path(__file__).parent)) +from r20_1_quantum_classical_fusion import ( + simulate_csi_breathing, + simulate_nv_cardiac, + estimate_rate_from_signal, + extract_hrv_contour, +) + + +def fusion_threshold_handoff(classical_rate, classical_conf, + nv_rate, nv_conf, nv_amplitude_pT, + nv_conf_threshold=0.60, + nv_amplitude_threshold_pT=3.0): + """Threshold-based hand-off: + - If NV is "good enough" (conf > 0.6 AND amplitude > 3 pT), trust NV entirely. + - Else fall back to precision-weighted average. + - If NV has no signal, classical drives. + """ + nv_trusted = (nv_conf > nv_conf_threshold) and (nv_amplitude_pT > nv_amplitude_threshold_pT) + if nv_trusted: + return nv_rate, nv_conf, "nv_drives" + if classical_conf < 1e-3: + return nv_rate, nv_conf, "fallback_nv" + if nv_conf < 1e-3: + return classical_rate, classical_conf, "fallback_classical" + # Precision-weighted fallback (R20.1's naive default) + w_c = classical_conf + w_n = nv_conf + fused = (w_c * classical_rate + w_n * nv_rate) / (w_c + w_n + 1e-9) + conf = float(1 - (1 - classical_conf) * (1 - nv_conf)) + return fused, conf, "weighted_fallback" + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--out", default="examples/research-sota/09-quantum-fusion/r20_2_threshold_results.json") + args = parser.parse_args() + + rng = np.random.default_rng(42) + true_breathing = 15.0 + true_hr = 72.0 + + # Same setup as R20.1 + t_csi, csi = simulate_csi_breathing(duration_s=60, fs=50, true_rate_bpm=true_breathing, rng=rng) + _, csi_hr_conf, _ = estimate_rate_from_signal(t_csi, csi, search_band=(0.8, 3.0)) + csi_hr_rate, csi_hr_conf, _ = estimate_rate_from_signal(t_csi, csi, search_band=(0.8, 3.0)) + + # NV at five distances to show degradation + results = [] + for d in [0.5, 1.0, 1.5, 2.0, 3.0]: + t_nv, nv, amp = simulate_nv_cardiac(duration_s=60, fs=200, true_hr_bpm=true_hr, + distance_m=d, rng=np.random.default_rng(int(42 + d * 10))) + nv_rate, nv_conf, nv_snr = estimate_rate_from_signal(t_nv, nv, search_band=(0.8, 3.0)) + + # R20.1 naive precision-weighted + w_c, w_n = csi_hr_conf, nv_conf + naive = (w_c * csi_hr_rate + w_n * nv_rate) / (w_c + w_n + 1e-9) + + # R20.2 threshold hand-off + smart, smart_conf, regime = fusion_threshold_handoff( + csi_hr_rate, csi_hr_conf, nv_rate, nv_conf, amp + ) + + err_naive = abs(naive - true_hr) + err_smart = abs(smart - true_hr) + + results.append({ + "distance_m": d, + "nv_amplitude_pT": amp, + "nv_rate_bpm": nv_rate, + "nv_conf": nv_conf, + "naive_fused_bpm": naive, + "smart_fused_bpm": smart, + "regime": regime, + "true_hr_bpm": true_hr, + "naive_error_bpm": err_naive, + "smart_error_bpm": err_smart, + }) + + out = { + "true_hr_bpm": true_hr, + "classical_hr_rate": csi_hr_rate, + "classical_hr_conf": csi_hr_conf, + "results_per_distance": results, + "thresholds": {"nv_conf": 0.60, "nv_amplitude_pT": 3.0}, + } + Path(args.out).parent.mkdir(parents=True, exist_ok=True) + Path(args.out).write_text(json.dumps(out, indent=2)) + + print(f"=== R20.2 threshold-based hand-off ===") + print(f"True HR: {true_hr} BPM") + print(f"Classical HR: {csi_hr_rate:.2f} BPM (conf {csi_hr_conf*100:.1f}%)") + print() + print(f"{'distance':>9} {'NV amp':>8} {'NV rate':>8} {'NV conf':>8} {'naive':>7} {'naive err':>9} {'smart':>7} {'smart err':>9} {'regime':>20}") + for r in results: + print(f"{r['distance_m']:>7.1f} m " + f"{r['nv_amplitude_pT']:>6.2f} pT " + f"{r['nv_rate_bpm']:>6.2f} BPM " + f"{r['nv_conf']*100:>6.1f}% " + f"{r['naive_fused_bpm']:>5.1f} BPM " + f"{r['naive_error_bpm']:>+6.1f} BPM " + f"{r['smart_fused_bpm']:>5.1f} BPM " + f"{r['smart_error_bpm']:>+6.1f} BPM " + f"{r['regime']:>20}") + print() + # Total error + total_naive = sum(r['naive_error_bpm'] for r in results) + total_smart = sum(r['smart_error_bpm'] for r in results) + print(f"Total naive error across 5 distances: {total_naive:.1f} BPM") + print(f"Total smart error across 5 distances: {total_smart:.1f} BPM") + print(f"Improvement factor: {total_naive / max(total_smart, 0.1):.2f}x") + print() + print(f"Wrote {args.out}") + + +if __name__ == "__main__": + main() diff --git a/examples/research-sota/09-quantum-fusion/r20_2_threshold_results.json b/examples/research-sota/09-quantum-fusion/r20_2_threshold_results.json new file mode 100644 index 0000000000..179caa6e39 --- /dev/null +++ b/examples/research-sota/09-quantum-fusion/r20_2_threshold_results.json @@ -0,0 +1,71 @@ +{ + "true_hr_bpm": 72.0, + "classical_hr_rate": 105.0, + "classical_hr_conf": 0.3805459134253063, + "results_per_distance": [ + { + "distance_m": 0.5, + "nv_amplitude_pT": 50.0, + "nv_rate_bpm": 72.0, + "nv_conf": 0.8348130933810896, + "naive_fused_bpm": 82.33276175218474, + "smart_fused_bpm": 72.0, + "regime": "nv_drives", + "true_hr_bpm": 72.0, + "naive_error_bpm": 10.332761752184737, + "smart_error_bpm": 0.0 + }, + { + "distance_m": 1.0, + "nv_amplitude_pT": 6.25, + "nv_rate_bpm": 144.0, + "nv_conf": 0.6689134324169522, + "naive_fused_bpm": 129.85815561865903, + "smart_fused_bpm": 144.0, + "regime": "nv_drives", + "true_hr_bpm": 72.0, + "naive_error_bpm": 57.858155618659026, + "smart_error_bpm": 72.0 + }, + { + "distance_m": 1.5, + "nv_amplitude_pT": 1.8518518518518514, + "nv_rate_bpm": 72.0, + "nv_conf": 0.39058395452018735, + "naive_fused_bpm": 88.28521417307823, + "smart_fused_bpm": 88.28521417307823, + "regime": "weighted_fallback", + "true_hr_bpm": 72.0, + "naive_error_bpm": 16.28521417307823, + "smart_error_bpm": 16.28521417307823 + }, + { + "distance_m": 2.0, + "nv_amplitude_pT": 0.78125, + "nv_rate_bpm": 77.0, + "nv_conf": 0.3549718835175086, + "naive_fused_bpm": 91.48678132427328, + "smart_fused_bpm": 91.48678132427328, + "regime": "weighted_fallback", + "true_hr_bpm": 72.0, + "naive_error_bpm": 19.48678132427328, + "smart_error_bpm": 19.48678132427328 + }, + { + "distance_m": 3.0, + "nv_amplitude_pT": 0.23148148148148143, + "nv_rate_bpm": 78.0, + "nv_conf": 0.3829009843660022, + "naive_fused_bpm": 91.45835525791024, + "smart_fused_bpm": 91.45835525791024, + "regime": "weighted_fallback", + "true_hr_bpm": 72.0, + "naive_error_bpm": 19.45835525791024, + "smart_error_bpm": 19.45835525791024 + } + ], + "thresholds": { + "nv_conf": 0.6, + "nv_amplitude_pT": 3.0 + } +} \ No newline at end of file