Skip to content

Python Detection Statistics API

The doppler.detection module is the detection-theory layer over the C detection core: closed-form relationships between probability of detection (Pd), probability of false alarm (Pfa), SNR, and coherent dwell length for a square-law detector. Pair it with the streaming CorrDetectordetection tells you what threshold and dwell to use, CorrDetector runs the detection.

Every quantity comes in two forms: an amplitude-SNR version (det_*, where SNR is the linear signal/noise amplitude ratio) and a power-SNR version (det_*_power, the linear power ratio = amplitude²). The two are equivalent detectors — det_pd(s, ...) equals det_pd_power(s**2, ...).

The threshold depends only on the target false-alarm rate; Pd then depends on the SNR and the coherent dwell. The whole chain is closed-form and stateless:

>>> from doppler.detection import det_threshold, det_pd, det_dwell, det_snr
>>> thr = det_threshold(pfa=1e-6)               # threshold for Pfa = 1e-6
>>> round(thr, 4)
5.2565
>>> round(det_pd(snr=1.613, dwell=8, threshold=thr), 2)
0.9
>>> det_dwell(snr=0.5, pd_min=0.9, pfa=1e-6, max_dwell=256)
84
>>> round(det_snr(dwell=8, pd_min=0.9, pfa=1e-6), 3)  # inverse of det_pd
1.613

The underlying Marcum Q-function is exposed directly — under H0 (a = 0) it is the Rayleigh tail exp(-b²/2):

>>> from doppler.detection import marcum_q
>>> round(marcum_q(m=1, a=0.0, b=1.0), 5)  # P(Rayleigh > 1) = exp(-0.5)
0.60653
>>> round(marcum_q(m=1, a=2.0, b=1.0), 5)  # signal present (a = 2)
0.91811

Amplitude-SNR (dB)

det_threshold

det_threshold(pfa: float) -> float

Threshold eta for a given false-alarm probability.

Exact closed-form inversion of Pfa = exp(-eta^2/2):

eta = sqrt(-2 * ln(pfa))

The threshold is independent of dwell and SNR; it depends only on the desired Pfa.

Parameters:

Name Type Description Default
pfa float

Desired false-alarm probability; must be in (0, 1).

required

Returns:

Type Description
float

Threshold eta > 0.

Examples:

>>> from doppler.detection import det_threshold
>>> round(det_threshold(pfa=1e-6), 4)
5.2565

det_pd

det_pd(snr: float, dwell: int, threshold: float) -> float

Detection probability for given per-sample amplitude SNR and dwell.

Computes Pd = Q_1(a, eta) where a = sqrt(2 * dwell) * snr.

At snr = 0, det_pd returns Pfa (the false-alarm rate, as expected for a noise-only input). As snr or dwell increase, Pd approaches 1.

Parameters:

Name Type Description Default
snr float

Per-sample amplitude SNR (signal / noise amplitude, linear). snr = 0 gives Pd = Pfa.

required
dwell int

Coherent integration depth; must be >= 1.

required
threshold float

Test-stat threshold eta, e.g. from det_threshold().

required

Returns:

Type Description
float

Detection probability in [0, 1].

Examples:

>>> from doppler.detection import det_pd, det_threshold
>>> thr = det_threshold(pfa=1e-6)
>>> round(det_pd(snr=1.613, dwell=8, threshold=thr), 2)  # Pd 0.9
0.9
>>> round(det_pd(snr=0.0, dwell=8, threshold=thr), 6)    # Pd = Pfa
1e-06

det_dwell

det_dwell(
    snr: float, pd_min: float, pfa: float, max_dwell: int
) -> int

Minimum dwell such that Pd >= pd_min for the given SNR and Pfa.

Iterates dwell = 1, 2, ..., max_dwell, computing det_pd() at each step. Returns the first dwell that satisfies the Pd requirement, or -1 if none is found within max_dwell iterations.

Parameters:

Name Type Description Default
snr float

Per-sample amplitude SNR (linear).

required
pd_min float

Required detection probability, e.g. 0.9.

required
pfa float

False-alarm probability; used to derive eta.

required
max_dwell int

Search upper bound; prevents infinite loops for low SNR.

required

Returns:

Type Description
int

Minimum dwell >= 1, or -1 if not achievable.

Examples:

>>> from doppler.detection import det_dwell
>>> det_dwell(snr=0.5, pd_min=0.9, pfa=1e-6, max_dwell=256)
84

det_snr

det_snr(dwell: int, pd_min: float, pfa: float) -> float

Minimum per-sample amplitude SNR achieving Pd >= pd_min.

Binary search over SNR in [0, hi] where hi is doubled from 1.0 until det_pd(hi, dwell, threshold) >= pd_min. 64 bisection iterations yield ~1e-19 relative precision on the final interval.

Parameters:

Name Type Description Default
dwell int

Coherent integration depth; must be >= 1.

required
pd_min float

Required detection probability.

required
pfa float

False-alarm probability; used to derive eta.

required

Returns:

Type Description
float

Minimum amplitude SNR >= 0.

Examples:

>>> from doppler.detection import det_snr, det_pd, det_threshold
>>> snr = det_snr(dwell=8, pd_min=0.9, pfa=1e-6)
>>> round(snr, 3)
1.613
>>> pd = det_pd(snr=snr, dwell=8, threshold=det_threshold(pfa=1e-6))
>>> abs(pd - 0.9) < 1e-9   # det_snr inverts det_pd, to tolerance
True

Non-coherent integration

When coherent integration is capped (Doppler walk, data bits, oscillator drift, Doppler rate), N_nc coherent looks are combined by summing squared magnitude. The detector becomes the order-N_nc Marcum-Q; these helpers package it and reduce to the coherent (order-1) versions above at n_noncoh = 1. They drive the doppler.dsss.Acquisition engine's coherent/non-coherent split.

det_threshold_noncoherent

det_threshold_noncoherent(
    pfa: float, n_noncoh: int
) -> float

CFAR threshold eta_nc for a non-coherent detector of n_noncoh looks.

Solves marcum_q(n_noncoh, 0, eta_nc) = pfa (the order-M central tail, monotone decreasing in eta_nc) by bisection. For n_noncoh = 1 this is the exact closed form sqrt(-2 ln pfa) (== det_threshold).

Parameters:

Name Type Description Default
pfa float

Per-test false-alarm probability in (0, 1).

required
n_noncoh int

Number of non-coherent looks; must be >= 1.

required

Returns:

Type Description
float

Threshold eta_nc on the normalized statistic R.

Examples:

>>> from doppler.detection import det_threshold_noncoherent
>>> from doppler.detection import det_threshold
>>> round(det_threshold_noncoherent(pfa=1e-3, n_noncoh=4), 3)
5.111
>>> det_threshold_noncoherent(pfa=1e-6, n_noncoh=1) == det_threshold(
...     pfa=1e-6)
True

det_pd_noncoherent

det_pd_noncoherent(
    snr: float, n_coh: int, n_noncoh: int, threshold: float
) -> float

Detection probability for n_noncoh non-coherent looks.

Computes Pd = Q_{n_noncoh}(a, threshold) with the non-centrality a = sqrt(2 * n_coh * n_noncoh) * snr. At n_noncoh = 1 this is exactly det_pd(snr, n_coh, threshold); at snr = 0 it returns the per-test Pfa.

Parameters:

Name Type Description Default
snr float

Per-sample amplitude SNR (signal / noise amplitude).

required
n_coh int

Coherent integration length in samples (dwell * N).

required
n_noncoh int

Number of non-coherent looks; must be >= 1.

required
threshold float

Threshold eta_nc, e.g. from det_threshold_noncoherent().

required

Returns:

Type Description
float

Detection probability in [0, 1].

Examples:

>>> from doppler.detection import det_pd_noncoherent, det_pd
>>> from doppler.detection import det_threshold_noncoherent
>>> from doppler.detection import det_threshold
>>> eta = det_threshold(pfa=1e-6)
>>> det_pd_noncoherent(snr=0.5, n_coh=8, n_noncoh=1, threshold=eta) \
...     == det_pd(snr=0.5, dwell=8, threshold=eta)  # -> coherent
True
>>> eta4 = det_threshold_noncoherent(pfa=1e-3, n_noncoh=4)
>>> round(det_pd_noncoherent(
...     snr=0.3, n_coh=16, n_noncoh=4, threshold=eta4), 2)
0.19

det_n_noncoh

det_n_noncoh(
    snr: float,
    n_coh: int,
    pd_min: float,
    pfa: float,
    max_n_noncoh: int,
) -> int

Minimum non-coherent looks achieving Pd >= pd_min at fixed n_coh.

Iterates n_noncoh = 1, 2, ..., max_n_noncoh, recomputing the threshold (det_threshold_noncoherent, which grows with the look count) at each step. Returns the first look count that meets the Pd requirement, or -1 if none does within max_n_noncoh. Used by the acquisition engine's (M, N_nc) split.

Parameters:

Name Type Description Default
snr float

Per-sample amplitude SNR (linear).

required
n_coh int

Coherent integration length in samples (dwell * N).

required
pd_min float

Required detection probability, e.g. 0.9.

required
pfa float

Per-test false-alarm probability.

required
max_n_noncoh int

Search upper bound on the look count.

required

Returns:

Type Description
int

Minimum n_noncoh >= 1, or -1 if not achievable.

Examples:

>>> from doppler.detection import det_n_noncoh
>>> det_n_noncoh(
...     snr=2.0, n_coh=16, pd_min=0.9, pfa=1e-3, max_n_noncoh=64)
1

Estimator smoothing

det_ema_alpha sizes a first-order EMA probabilistically: treat the quantity being smoothed as a DC level in noise with a per-sample estimator SNR (mean² / variance), pick the output SNR the decision needs, and the coefficient follows from the EMA's variance reduction (2 − α)/α. It is how the DLL's code-lock detector sizes its CFAR noise-reference bandwidth (Dll.configure_lock(..., ref_snr_db=...)), and the same call sizes any lock-metric smoother when the per-look SNR is known from C/N0:

from doppler.detection import det_ema_alpha

# signal-free power reference: exponential samples = 0 dB per sample;
# a 20 dB estimator SNR needs an ~50-look EMA
assert round(1 / det_ema_alpha(0.0, 20.0), 1) == 50.5

# only the requested gain matters, not where the pair sits in dB
assert abs(det_ema_alpha(10.0, 30.0) - det_ema_alpha(0.0, 20.0)) < 1e-15

# already good enough -> no averaging
assert det_ema_alpha(6.0, 3.0) == 1.0

det_ema_alpha

det_ema_alpha(snr_in_db: float, snr_out_db: float) -> float

EMA coefficient for a target estimator SNR (DC level in noise).

Sizes a first-order EMA y = (1-alpha)*y + alpha*x that estimates a DC level from noisy i.i.d. measurements x. Per sample the estimator SNR (mean^2 / variance) is snr_in; the EMA improves it by its variance reduction (2-alpha)/alpha, so the output SNR is snr_out = snr_in * (2-alpha)/alpha. Solving for the coefficient:

alpha = 2 * snr_in / (snr_in + snr_out) (SNRs linear)

Returns 1.0 (no averaging) when snr_out_db <= snr_in_db. Typical inputs: a signal-free power reference |n|^2 is exponential (0 dB per sample); a lock signal at known C/N0 has per-look SNR from its coherent integration (minus squaring loss), and this picks the smoothing bandwidth that makes the lock decision variable meet a chosen decision SNR.

Parameters:

Name Type Description Default
snr_in_db float

Per-sample estimator SNR, dB (mean^2 / variance).

required
snr_out_db float

Desired EMA-output estimator SNR, dB.

required

Returns:

Type Description
float

EMA coefficient alpha in (0, 1].

Examples:

>>> from doppler.detection import det_ema_alpha
>>> det_ema_alpha(0.0, 0.0)      # no gain requested -> no averaging
1.0
>>> round(1 / det_ema_alpha(0.0, 20.0), 1)   # 20 dB gain ~ 50 looks
50.5
>>> round(1 / det_ema_alpha(10.0, 30.0), 1)  # same 20 dB gain, shifted
50.5

Lock verification

A loop that computes a lock statistic still needs a decision rule: when is the statistic high enough, long enough, to declare lock — and low enough, long enough, to drop it? LockDet is that rule factored out once: separate declare/drop thresholds (level hysteresis) plus consecutive-look verify counts (time hysteresis). Consecutive looks compound probabilistically — n looks at per-look probability p reach p^n — so the verify counts are derived, not guessed: det_verify_count sizes them from a per-look rate and a compound budget, and det_verify_delay predicts the declare latency they cost. The DLL's code-lock latch and the M-PSK receiver's two-way acquisition↔tracking handover both run on an embedded C lockdet.

from doppler.detection import LockDet, det_verify_count, det_verify_delay

# declare side: per-decision pfa 1e-3, false-declare budget 1e-9 -> 3 straight
n_up = det_verify_count(1e-3, 1e-9)
assert n_up == 3

# drop side: per-look miss rate 1-pd = 0.2, false-drop budget 1e-4 -> 6
n_down = det_verify_count(0.2, 1e-4)
assert n_down == 6

# the price in latency: mean looks to a declare at pd = 0.9
assert round(det_verify_delay(0.9, n_up), 2) == 3.72

d = LockDet(up_thresh=8.5, down_thresh=7.0, n_up=n_up, n_down=n_down)
assert [d.step(9.0), d.step(9.0), d.step(9.0)] == [0, 0, 1]  # 3rd hit locks
assert d.step(7.5) == 1  # inside the hysteresis band: sticky

det_verify_count

det_verify_count(p_look: float, p_target: float) -> int

Verify count: consecutive looks needed to compound to a budget.

n consecutive independent looks at per-look probability p compound to p^n, so the smallest n with p_look^n <= p_target is ceil(ln p_target / ln p_look) (clamped to >= 1). One function serves both sides of a lock detector (lockdet_core.h): the declare count from (per-look pfa, false-declare budget) and the drop count from (per-look miss rate 1 - pd, false-drop budget). Degenerate inputs resolve naturally: a target already met by one look returns 1; p_look >= 1 can never compound below a smaller target and returns INT_MAX.

Parameters:

Name Type Description Default
p_look float

Per-look probability (pfa or 1 - pd), in (0, 1).

required
p_target float

Compound probability budget, in (0, 1).

required

Returns:

Type Description
int

Smallest verify count n with p_look^n <= p_target.

Examples:

>>> from doppler.detection import det_verify_count
>>> det_verify_count(1e-3, 1e-6)   # two 1e-3 looks reach 1e-6
2
>>> det_verify_count(1e-3, 1e-9)
3
>>> det_verify_count(0.5, 1e-3)    # drop side: pd = 0.5 per look
10
>>> det_verify_count(1e-3, 0.5)    # budget already met -> 1
1

det_verify_delay

det_verify_delay(p_look: float, n: int) -> float

Expected looks until a run of n consecutive successes completes.

The mean waiting time of the consecutive-run process a lockdet verify counter implements: at per-look success probability p, the first run of n straight successes takes on average

E[T] = (1 - p^n) / (p^n * (1 - p)) looks,

which is the declare latency bought by a verify count of n (multiply by the look period for time). Limits are handled exactly: p = 1 gives n (the run completes immediately), p = 0 gives infinity.

Parameters:

Name Type Description Default
p_look float

Per-look success probability (e.g. pd), in [0, 1].

required
n int

Run length (the verify count); clamped to >= 1.

required

Returns:

Type Description
float

Expected number of looks to the first length-n run.

Examples:

>>> from doppler.detection import det_verify_delay
>>> det_verify_delay(1.0, 8)             # certain hits: exactly n
8.0
>>> round(det_verify_delay(0.5, 2), 6)   # 2 straight coin heads: 6
6.0
>>> round(det_verify_delay(0.9, 8), 1)
13.2

One-shot (burst) decisions use the same machinery with one twist: when the noise reference is estimated from as many samples as the signal sum (the BurstDespreader lock test), the exact H0 law is F(n, n), not chi-square — det_threshold_f prices that gate exactly, for every n:

from doppler.detection import det_threshold_f

# F(2,2) tail is 1/(1+g): the quantile is exactly (1-pfa)/pfa
assert round(det_threshold_f(1e-3, 2), 6) == 999.0

# the estimate hardens with dof: the gate approaches the known-noise one
assert det_threshold_f(1e-3, 16) > det_threshold_f(1e-3, 64) > 1.0

det_threshold_f

det_threshold_f(pfa: float, n: int) -> float

Upper quantile of F(n, n) — the exact H0 law for a ratio test whose noise reference is estimated from as many samples as the signal sum.

A chi-square threshold (det_threshold_noncoherent) prices a statistic normalised by a KNOWN noise power. When the noise power is instead estimated from n same-burst samples (the BurstDespreader lock test: sum Re^2 against sum Im^2), the ratio's tail fattens to F(n, n) and the chi-square gate realizes tens of times the priced pfa (41x at n = 16, pfa = 1e-3). This helper returns the exact gate: P(chi2_n / chi2_n > g) = I_{1/(1+g)}(n/2, n/2) = pfa, solved on the regularized incomplete beta — valid for every n >= 1, odd included. As n grows the estimate hardens and g approaches the known-noise value. Threshold a BurstDespreader as lock_stat > sqrt(stat_n * det_threshold_f(pfa, stat_n)).

Parameters:

Name Type Description Default
pfa float

Tail probability budget, in (0, 1).

required
n int

Degrees of freedom on each side (>= 1).

required

Returns:

Type Description
float

The F(n, n) upper-pfa quantile; 0 on invalid input.

Examples:

>>> from doppler.detection import det_threshold_f
>>> round(det_threshold_f(1e-3, 2), 6)  # exact: (1 - pfa)/pfa
999.0
>>> round(det_threshold_f(1e-3, 4), 4)
53.4358
>>> round(det_threshold_f(1e-3, 64), 4)  # hardens toward known-noise
2.1931

LockDet

LockDet component.

Parameters:

Name Type Description Default
up_thresh float

up_thresh constructor parameter.

1.0
down_thresh float

down_thresh constructor parameter.

1.0
n_up int

n_up constructor parameter.

1
n_down int

n_down constructor parameter.

1

Examples:

Create with defaults:

>>> from doppler.detection import LockDet
>>> obj = LockDet(up_thresh=1.0, down_thresh=1.0, n_up=1, n_down=1)

up_thresh property writable

up_thresh: float

declare side: hit when metric > up_thresh.

down_thresh property writable

down_thresh: float

drop side: miss when metric < down_thresh.

n_up property

n_up: int

consecutive hits required to declare (>= 1).

n_down property

n_down: int

consecutive misses required to drop (>= 1).

cnt property

cnt: int

Running consecutive-look verify counter: hits toward a declare while unlocked, misses toward a drop while locked.

locked property

locked: bool

Current decision (True = locked).

step

step(x: float) -> int

Feed one look of the lock metric; return the current decision.

Unlocked: a hit (x > up_thresh) advances the verify run and the n_up-th consecutive hit declares lock; any miss resets the run. Locked: a miss (x < down_thresh) advances the run and the n_down-th consecutive miss drops the lock; any hit (x >= down_thresh) resets it. A metric inside the [down_thresh, up_thresh] band is sticky — it neither advances a declare nor a drop.

Parameters:

Name Type Description Default
x float

Lock metric for this look.

required

Returns:

Type Description
int

Decision after this look (1 = locked, 0 = not).

Examples:

>>> from doppler.detection import LockDet
>>> d = LockDet(up_thresh=1.5, down_thresh=1.2, n_up=2, n_down=3)
>>> [d.step(2.0), d.step(2.0)]     # declared on the 2nd straight hit
[0, 1]
>>> d.step(1.3)                    # in the hysteresis band: stays up
1
>>> [d.step(1.0), d.step(1.0), d.step(1.0)]  # 3rd straight miss drops
[1, 1, 0]

steps

steps(
    x: NDArray[float64], out: NDArray[int32] | None = None
) -> NDArray[np.int32]

Run a block of lock-metric looks through the detector. Applies lockdet_step() to each look in turn, so the decision flag and the in-flight verify run carry across the block exactly as they would look by look — a signal can be processed in frames of any size with no seam.

Parameters:

Name Type Description Default
x NDArray[float64]

Lock-metric looks, one scalar per look (length >= n).

required

Returns:

Type Description
NDArray[int32]

Output.

Examples:

>>> import numpy as np
>>> from doppler.detection import LockDet
>>> d = LockDet(up_thresh=1.5, down_thresh=1.2, n_up=2, n_down=2)
>>> x = np.array([2.0, 2.0, 1.0, 2.0])   # declares on the 2nd hit
>>> d.steps(x).tolist()
[0, 1, 1, 1]

configure

configure(
    up_thresh: float,
    down_thresh: float,
    n_up: int,
    n_down: int,
) -> None

Re-tune thresholds and verify counts; a live lock survives, the in-flight verify run restarts under the new config.

The current locked flag survives (a live lock is not dropped by a re-tune); the in-flight verify counter is cleared so the next run is counted entirely under the new config.

Parameters:

Name Type Description Default
up_thresh float

Declare threshold (hit when metric > up_thresh).

required
down_thresh float

Drop threshold (miss when metric < down_thresh).

required
n_up int

Consecutive hits to declare; clamped to >= 1.

required
n_down int

Consecutive misses to drop; clamped to >= 1.

required

Examples:

>>> from doppler.detection import LockDet
>>> d = LockDet(up_thresh=1.5, down_thresh=1.2, n_up=2, n_down=2)
>>> d.configure(up_thresh=3.0, down_thresh=2.5, n_up=1, n_down=1)
>>> d.up_thresh          # thresholds re-tuned in place
3.0
>>> d.step(4.0)          # a single hit now declares (n_up=1)
1

reset

reset() -> None

Drop the lock and clear the verify counter; keep the config.

Examples:

>>> from doppler.detection import LockDet
>>> d = LockDet(up_thresh=1.5, down_thresh=1.2, n_up=1, n_down=1)
>>> d.step(2.0)          # one hit declares lock (n_up=1)
1
>>> d.reset()            # drop it and clear the verify run
>>> d.locked
False

state_bytes

state_bytes() -> int

Size in bytes of this object's serialized state.

The exact length get_state returns and set_state requires. It depends on how the object was constructed (state arrays are sized at construction), so read it from the instance rather than assuming a constant.

Raises RuntimeError if the LockDet has already been destroyed.

Returns:

Type Description
int

Byte length of one serialized state blob.

get_state

get_state() -> bytes

Serialize this object's mutable state to bytes.

Captures exactly the state that evolves as the object runs, so a blob taken now and restored later resumes from this point. Construction parameters are not included: restore into an object built the same way.

The blob is opaque and always state_bytes() long. Its layout is an implementation detail of the C core and is not a stable format across builds.

Raises RuntimeError if the LockDet has already been destroyed.

Returns:

Type Description
bytes

Opaque snapshot, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

Restore mutable state from a get_state() blob.

Overwrites the live state in place; the object keeps the parameters it was constructed with. Length is validated against state_bytes() before the blob is handed to the C core, and the core may reject it as well.

Raises TypeError if blob is not bytes, ValueError if its length differs from state_bytes() or the core rejects it, and RuntimeError if the LockDet has already been destroyed.

Parameters:

Name Type Description Default
blob bytes

A get_state() blob from this type, exactly state_bytes() long.

required

destroy

destroy() -> None

Release the underlying C resources immediately.

Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this to release them at a definite point instead, or use the object as a context manager, which calls it on exit.

Idempotent: calling it again on an already-released object does nothing. Every other method raises RuntimeError once it has run.

__enter__

__enter__() -> LockDet

Enter a context manager, returning this object.

Lets a LockDet be used in a with statement so its C resources are released deterministically on exit rather than at collection time.

Returns:

Type Description
LockDet

This same object, not a copy.

__exit__

__exit__(
    exc_type: object | None = ...,
    exc: object | None = ...,
    tb: object | None = ...,
) -> None

Exit a context manager, releasing the LockDet.

Equivalent to calling destroy(). Returns None, so an exception raised inside the with body propagates normally; this never suppresses one.

Parameters:

Name Type Description Default
exc_type object | None

Exception class, or None. Ignored.

...
exc object | None

Exception instance, or None. Ignored.

...
tb object | None

Traceback object, or None. Ignored.

...

Power-SNR (linear)

det_threshold_power

det_threshold_power(pfa: float) -> float

Power threshold p from Pfa for the power detector.

Exact closed-form: P(Exponential(1) > p) = exp(-p) = Pfa, so

p = -ln(Pfa)

Parameters:

Name Type Description Default
pfa float

Desired false-alarm probability; must be in (0, 1).

required

Returns:

Type Description
float

Threshold p > 0.

Examples:

>>> from doppler.detection import det_threshold_power
>>> round(det_threshold_power(pfa=1e-6), 3)   # -ln(1e-6) = 6*ln(10)
13.816

det_pd_power

det_pd_power(
    snr_power: float, dwell: int, power_threshold: float
) -> float

Detection probability for the power detector.

Pd = Q_1(sqrt(2·dwell·snr_power), sqrt(2·power_threshold))

The result equals det_pd() at the equivalent amplitude SNR: power SNR s corresponds to amplitude SNR sqrt(s), and the Q_1 arguments match.

Parameters:

Name Type Description Default
snr_power float

Per-sample power SNR (signal power / noise power at the correlator output, linear). 0 gives Pd = Pfa.

required
dwell int

Coherent integration depth; must be >= 1.

required
power_threshold float

Threshold p, e.g. from det_threshold_power().

required

Returns:

Type Description
float

Detection probability in [0, 1].

Examples:

>>> from doppler.detection import det_pd_power, det_threshold_power
>>> thr = det_threshold_power(pfa=1e-6)
>>> round(det_pd_power(
...     snr_power=2.6017, dwell=8, power_threshold=thr), 2)
0.9

det_dwell_power

det_dwell_power(
    snr_power: float,
    pd_min: float,
    pfa: float,
    max_dwell: int,
) -> int

Minimum dwell such that Pd >= pd_min for the power detector.

Parameters:

Name Type Description Default
snr_power float

Per-sample power SNR (linear).

required
pd_min float

Required detection probability.

required
pfa float

False-alarm probability; used to derive p.

required
max_dwell int

Search upper bound.

required

Returns:

Type Description
int

Minimum dwell >= 1, or -1 if not achievable.

Examples:

>>> from doppler.detection import det_dwell_power
>>> det_dwell_power(
...     snr_power=0.25, pd_min=0.9, pfa=1e-6, max_dwell=256)
84

det_snr_power

det_snr_power(
    dwell: int, pd_min: float, pfa: float
) -> float

Minimum per-sample power SNR achieving Pd >= pd_min.

Parameters:

Name Type Description Default
dwell int

Coherent integration depth; must be >= 1.

required
pd_min float

Required detection probability.

required
pfa float

False-alarm probability.

required

Returns:

Type Description
float

Minimum power SNR >= 0.

Examples:

>>> from doppler.detection import (det_snr_power, det_pd_power,
...                                det_threshold_power)
>>> sp = det_snr_power(dwell=8, pd_min=0.9, pfa=1e-6)
>>> round(sp, 4)
2.6017
>>> pd = det_pd_power(snr_power=sp, dwell=8,
...                   power_threshold=det_threshold_power(pfa=1e-6))
>>> abs(pd - 0.9) < 1e-9   # det_snr_power inverts det_pd_power
True

Primitive

marcum_q

marcum_q(m: int, a: float, b: float) -> float

Marcum Q function Q_M(a, b) for integer M >= 1.

Probability that a Rice(a, sigma=1) random variable exceeds b. For M=1: Q_1(a, b) = P(Rice(a,1) > b). General integer M relates to the noncentral chi-squared CDF with 2M degrees of freedom.

Computed via the Poisson-weighted chi-squared series (exact for M=1, converges in ~60 terms for practical a, b <= 15):

Q_M(a, b) = sum_{k=0}^inf w_k * Q_{M+k}(0, b)

where: w_k = exp(-u) * u^k/k! (u = a^2/2) Q_n(0,b) = exp(-v) * sum_{j=0}^{n-1} v^j/j! (v = b^2/2)

Each iteration advances both the Poisson weight and the chi-sum in O(1) using the recurrences w_{k+1} = w_k * u/(k+1) and Q_{n+1}(0,b) = Q_n(0,b) + exp(-v)*v^n/n!. Total cost: O(K) where K ~ max(u, M) + safety margin.

Special cases:

  • a = 0: Q_M(0, b) = exp(-b^2/2) * sum_{j=0}^{M-1} (b^2/2)^j/j!
  • b <= 0: Q_M(a, b) = 1.0

Parameters:

Name Type Description Default
m int

Integration order; must be >= 1.

required
a float

Non-centrality parameter (signal strength). a = 0 for H0.

required
b float

Threshold (same units as test_stat).

required

Returns:

Type Description
float

Q_M(a, b) in [0, 1].

Examples:

>>> from doppler.detection import marcum_q
>>> round(marcum_q(m=1, a=0.0, b=1.0), 5)  # P(Rayleigh>1) = exp(-.5)
0.60653
>>> round(marcum_q(m=1, a=0.0, b=2.0), 5)   # exp(-2)
0.13534
>>> round(marcum_q(m=2, a=0.0, b=2.0), 5)   # 3*exp(-2)
0.40601
>>> round(marcum_q(m=1, a=2.0, b=1.0), 5)   # signal present (a=2)
0.91811

GalleryStreaming Async Despreader, Measuring an Error Rate, Defensibly, CarrierAcquisition: RRC Pulse Shaping, Detection Theory Curves, Monte Carlo vs Marcum Q Theory, Lock Detection: Verify Counts + Hysteresis, M-PSK Receiver — Pull-in, Lock, and BER GuidesDSSS Burst Acquisition, Lock Detection Across doppler.track DesignAsynchronous symbol/code despreading, DSSS acquisition: stateless, parallel, dynamics-capable, MPSK Receiver