Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions docs/research/sota-2026-05-22/R20_2-threshold-handoff.md
Original file line number Diff line number Diff line change
@@ -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.
142 changes: 142 additions & 0 deletions examples/research-sota/09-quantum-fusion/r20_2_threshold_handoff.py
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading