Skip to content

Detection Theory Curves

Detection theory curves

What you're seeing

Left — Pd vs dwell M at Pfa = 1e-5, for SNR = 0, 3, 6, 10 dB. Curves shift left as SNR increases: more per-sample SNR trades against coherent integration depth. Filled circles mark where each curve first crosses Pd = 0.9 — M = 18, 9, 5, 2.

Right — minimum dwell for Pd ≥ 0.9 vs SNR at fixed Pfa = 1e-5. Every 3 dB of extra SNR roughly halves the required dwell: at 0 dB you need 18 dwells for Pd = 0.9; at +6 dB you need only 5.

How it works

det_pd, det_dwell, and det_threshold implement the closed-form Marcum Q functions. No simulation is needed to set a threshold or predict performance:

import numpy as np

from doppler.detection import det_dwell, det_pd, det_threshold

PFA = 1e-5
PD_TARGET = 0.9
SNR_DB = [0, 3, 6, 10]  # curves for left panel
MAX_DWELL = 64
DWELL_X = np.arange(1, MAX_DWELL + 1)

ETA = det_threshold(PFA)  # threshold is Pfa-only; computed once

# Left panel: Pd vs dwell for each SNR.
snr_amps = [10 ** (db / 20) for db in SNR_DB]
pd_curves = [[det_pd(snr, int(d), ETA) for d in DWELL_X] for snr in snr_amps]

# Right panel: minimum dwell achieving Pd = 0.9 vs SNR from -3 to 15 dB.
snr_db_sweep = np.linspace(-3, 15, 300)
snr_amp_sweep = 10 ** (snr_db_sweep / 20)
min_dwell = [
    det_dwell(float(s), PD_TARGET, PFA, MAX_DWELL) for s in snr_amp_sweep
]

# Mask SNRs where det_dwell returned -1 (not achievable within MAX_DWELL).
valid = np.array(min_dwell)
mask = valid > 0

# Self-checks: the theory functions must be internally consistent.
# Coherent integration only helps: Pd is non-decreasing in dwell.
for snr_db, pds in zip(SNR_DB, pd_curves):
    assert np.all(np.diff(pds) >= -1e-12), (
        f"Pd not monotone in dwell at {snr_db} dB"
    )

# det_dwell() must return the *minimum* dwell: Pd first crosses the
# target at M, i.e. Pd(M) >= target and Pd(M-1) < target.
for snr_db, snr_amp in zip(SNR_DB, snr_amps):
    m = det_dwell(snr_amp, PD_TARGET, PFA, MAX_DWELL)
    assert m > 0, f"Pd={PD_TARGET} unreachable at {snr_db} dB"
    assert det_pd(snr_amp, m, ETA) >= PD_TARGET, "det_dwell undershoots"
    assert m == 1 or det_pd(snr_amp, m - 1, ETA) < PD_TARGET, (
        "det_dwell is not minimal"
    )
    print(f"SNR {snr_db:+3d} dB: minimum dwell M = {m}")

# Integration gain compensates SNR: a stronger signal never needs a
# longer dwell, so the right-panel curve is non-increasing.
assert np.all(np.diff(valid[mask]) <= 0), "min dwell not monotone in SNR"

det_threshold inverts the Rayleigh CDF at Pfa to get the CFAR gate eta. det_dwell binary-searches over M until det_pd(snr, M, eta) >= pd_target.

python src/doppler/examples/detection_curves.py   # → detection_curves.png

See Monte Carlo vs Marcum Q for the 30,000-trial validation of these closed-form curves against the envelope and power detectors.