143 lines
5.4 KiB
Python
143 lines
5.4 KiB
Python
#!/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()
|