Python Correlation & Detection API¶
The doppler.spectral module is a single CPython extension over the C spectral
core. Its FFT engines are documented on the FFT page; this page
covers the rest of the module — correlation (Corr, Corr2D), streaming
detection (Detector, Detector2D), and the spectral helper functions
(windows, magnitude, peak-finding).
For the statistical-detection side (probability of detection, thresholds, dwell sizing), see the Detection Statistics page.
Correlation¶
Corr is a 1-D FFT correlator with coherent integrate-and-dump: it pre-computes
conj(FFT(ref)) once at construction, so each execute() costs two FFTs and n
complex multiplies. With dwell == 1 every call dumps; with a larger dwell,
the accumulator coherently integrates that many frames before returning a result
(and returns None in between).
import numpy as np
from doppler.spectral import Corr
ref = np.exp(2j * np.pi * 0.1 * np.arange(1024)).astype(np.complex64)
corr = Corr(ref, dwell=1)
frame = ref + 0.1 * (np.random.randn(1024) + 1j * np.random.randn(1024))
out = corr.execute(frame.astype(np.complex64)) # ndarray on a dump, else None
lag = int(np.argmax(np.abs(out))) # correlation peak position
Corr2D is the 2-D analogue over an ny × nx grid (flat row-major arrays).
Corr
¶
Allocate a 1-D FFT correlator with coherent integrate-and-dump. Pre-computes conj(FFT(ref)) once at construction so each execute() call costs only two FFTs and n complex multiplies. @p ref may be freed after this returns. With @p dwell == 1 every call produces output; with larger values the accumulator absorbs @p dwell frames before dumping.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ref
|
NDArray[complex64]
|
ref constructor parameter. |
...
|
dwell
|
int
|
dwell constructor parameter. |
1
|
nthreads
|
int
|
nthreads constructor parameter. |
1
|
reset
¶
Zero the accumulator and reset the integration counter to 0. Equivalent to starting a fresh dwell cycle without tearing down the FFT plans. Does NOT recompute ref_spec; use corr_set_ref() to replace the reference.
Examples:
execute
¶
Correlate one frame and optionally dump the coherent accumulator. Runs the six-step pipeline: forward FFT → pointwise multiply with ref_spec → inverse FFT → normalise (÷ n) → accumulate → conditional dump. On the @p dwell-th call the accumulator is copied to @p out, zeroed, and the counter resets; the function returns n. All other calls return 0 and leave @p out unmodified. In Python, a dump returns an ndarray and a no-dump returns None.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
complex
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
n on a dump call, 0 otherwise (None in Python). |
Examples:
>>> from doppler.spectral import Corr
>>> import numpy as np
>>> ref = np.zeros(4, dtype=np.complex64); ref[0] = 1.0
>>> corr = Corr(ref=ref, dwell=2)
>>> x = np.ones(4, dtype=np.complex64)
>>> corr.execute(x) is None # frame 1 — no dump yet
True
>>> corr.execute(x).tolist() # frame 2 — dump
[(2+0j), (2+0j), (2+0j), (2+0j)]
Corr2D
¶
Allocate a 2-D FFT correlator with coherent integrate-and-dump. Two-dimensional extension of corr_create(). The reference is a flat row-major ny×nx CF32 array; its conjugate spectrum is pre-computed once so each execute() call costs two 2-D FFTs plus ny*nx complex multiplies. The Python wrapper requires @p ref to be a 2-D ndarray with shape (ny, nx); it passes a flat view to C.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ref
|
NDArray[complex64]
|
ref constructor parameter. |
...
|
dwell
|
int
|
dwell constructor parameter. |
1
|
nthreads
|
int
|
nthreads constructor parameter. |
1
|
reset
¶
Zero the accumulator and reset the integration counter to 0. Equivalent to starting a fresh dwell cycle without rebuilding FFT plans or recomputing ref_spec.
Examples:
execute
¶
Correlate one 2-D frame and optionally dump the coherent accumulator. Runs the 2-D pipeline: FFT2 → pointwise multiply with ref_spec → IFFT2 → normalise (÷ nynx) → accumulate → conditional dump. The Python wrapper accepts a (ny, nx) CF32 ndarray; a dump returns a flat length-nynx ndarray, a no-dump returns None.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
complex
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
ny*nx on a dump, 0 otherwise (None in Python). |
Examples:
>>> from doppler.spectral import Corr2D
>>> import numpy as np
>>> ref = np.zeros((2, 2), dtype=np.complex64); ref[0, 0] = 1.0
>>> c = Corr2D(ref=ref, dwell=2)
>>> x = np.ones((2, 2), dtype=np.complex64)
>>> c.execute(x) is None # frame 1 — no dump
True
>>> c.execute(x).tolist() # frame 2 — dump
[(2+0j), (2+0j), (2+0j), (2+0j)]
Streaming detection¶
Detector wraps a correlator with a double-mapped ring buffer so you can
push arbitrary-sized chunks. After each integrate-and-dump it compares the
peak-to-noise test statistic against threshold and emits a detection result
when it passes (threshold = 0.0 fires on every dump). The ring capacity is
next_pow2(max(n, 512)) complex samples.
import numpy as np
from doppler.spectral import Detector
ref = np.exp(2j * np.pi * 0.1 * np.arange(1024)).astype(np.complex64)
det = Detector(ref, dwell=4, threshold=12.0) # ~12 dB peak-to-noise
for chunk in stream_chunks(): # any chunk size
result = det.execute(chunk.astype(np.complex64))
if result is not None:
print("detection:", result) # lag / metric / sample index
Detector2D is the 2-D streaming detector over a grid.
Detector
¶
Allocate a 1-D streaming signal detector backed by an FFT correlator. Combines a corr_state_t with a double-mapped ring buffer so that arbitrary chunk sizes can be pushed. After every int-dump the peak-to-noise test statistic is compared against @p threshold; a det_result_t is emitted when it passes. Setting @p threshold to 0.0 unconditionally fires on every dump. The ring capacity is next_pow2(max(n, 512)) complex samples.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ref
|
NDArray[complex64]
|
ref constructor parameter. |
...
|
dwell
|
int
|
dwell constructor parameter. |
1
|
noise_lo
|
int
|
noise_lo constructor parameter. |
0
|
noise_hi
|
int
|
noise_hi constructor parameter. |
n-1
|
noise_mode
|
Literal['mean', 'median', 'min', 'max']
|
noise_mode constructor parameter. |
"mean"
|
threshold
|
float
|
threshold constructor parameter. |
0.0
|
nthreads
|
int
|
nthreads constructor parameter. |
1
|
reset
¶
Reset the correlator, ring buffer, and last-corr flag. Discards any partial frame buffered in the ring and zeroes the coherent accumulator. Equivalent to starting fresh from the same reference without rebuilding any internal object.
Examples:
>>> from doppler.spectral import Detector
>>> import numpy as np
>>> ref = np.zeros(8, dtype=np.complex64); ref[0] = 1.0
>>> det = Detector(ref=ref, dwell=1, noise_lo=1, noise_hi=7,
... noise_mode="mean", threshold=0.0)
>>> _ = det.push(np.ones(8, dtype=np.complex64))
>>> det.reset()
>>> det.count
0
push
¶
Stream an arbitrary-length CF32 chunk through the detector pipeline. Writes samples into the ring buffer, drains complete n-sample frames through the correlator, and on every int-dump computes the test statistic peak_mag / noise_est. Detections that pass the threshold are appended to the Python return list as (lag, peak_mag, noise_est, test_stat) tuples. In Python the result is always a list, even when empty.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
complex
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
list[tuple[int, float, float, float]]
|
Number of det_result_t entries written to @p result. |
Examples:
>>> from doppler.spectral import Detector
>>> import numpy as np
>>> ref = np.zeros(8, dtype=np.complex64); ref[0] = 1.0
>>> det = Detector(ref=ref, dwell=1, noise_lo=1, noise_hi=7,
... noise_mode="mean", threshold=0.0)
>>> results = det.push(np.ones(8, dtype=np.complex64))
>>> len(results)
1
>>> lag, peak, noise, stat = results[0]
>>> lag, round(peak, 4), round(noise, 4), round(stat, 4)
(0, 1.0, 1.0, 1.0)
Detector2D
¶
Allocate a 2-D streaming signal detector backed by a 2-D correlator. Two-dimensional extension of detector_create(). Input frames are flat row-major CF32 arrays of length ny*nx streamed through a ring buffer. On every int-dump the peak flat index is decomposed into (row, col) and a det_result2d_t is emitted when test_stat > threshold. The Python wrapper accepts a (ny, nx) CF32 ndarray for both @p ref and the push input.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ref
|
NDArray[complex64]
|
ref constructor parameter. |
...
|
dwell
|
int
|
dwell constructor parameter. |
1
|
noise_lo
|
int
|
noise_lo constructor parameter. |
0
|
noise_hi
|
int
|
noise_hi constructor parameter. |
ny*nx-1
|
noise_mode
|
Literal['mean', 'median', 'min', 'max']
|
noise_mode constructor parameter. |
"mean"
|
threshold
|
float
|
threshold constructor parameter. |
0.0
|
nthreads
|
int
|
nthreads constructor parameter. |
1
|
reset
¶
Reset the 2-D correlator, ring buffer, and last-corr flag. Discards any partial frame buffered in the ring and zeroes the coherent accumulator. The reference spectrum and FFT plans are preserved.
Examples:
>>> from doppler.spectral import Detector2D
>>> import numpy as np
>>> ref = np.zeros((4, 4), dtype=np.complex64); ref[0, 0] = 1.0
>>> det = Detector2D(ref=ref, dwell=1, noise_lo=1, noise_hi=15,
... noise_mode="mean", threshold=0.0)
>>> _ = det.push(np.ones((4, 4), dtype=np.complex64))
>>> det.reset()
>>> det.count
0
push
¶
Stream an arbitrary-length CF32 chunk through the 2-D detector. Identical to detector_push() except frames are ny*nx complex samples and each detection event carries (row, col) for the peak location instead of a single lag index. In Python the result is always a list of (row, col, peak_mag, noise_est, test_stat) tuples.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
complex
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
list[tuple[int, int, float, float, float]]
|
Number of det_result2d_t entries written to @p result. |
Examples:
>>> from doppler.spectral import Detector2D
>>> import numpy as np
>>> ref = np.zeros((4, 4), dtype=np.complex64); ref[0, 0] = 1.0
>>> det = Detector2D(ref=ref, dwell=1, noise_lo=1, noise_hi=15,
... noise_mode="mean", threshold=0.0)
>>> results = det.push(np.ones((4, 4), dtype=np.complex64))
>>> len(results)
1
>>> row, col, peak, noise, stat = results[0]
>>> row, col, round(peak, 4), round(noise, 4), round(stat, 4)
(0, 0, 1.0, 1.0, 1.0)
Averaging PSD & measurements¶
Welch is a stateful Welch-style averaging power-spectral-density estimator.
Feed complex baseband frames with accumulate(); each length-n frame is
windowed, FFT'd, converted to power, fftshifted to DC-centred order and folded
into a running average (AccTrace, with the same
mode of "mean" / "exp" / "maxhold" / "minhold"). Then read the
averaged spectrum and derived measurements.
import numpy as np
from doppler.spectral import Welch, find_peaks_f32
w = Welch(n=1024, fs=1e6, window="kaiser", beta=8.0, mode="mean")
for frame in frames: # each frame: 1024 complex64 samples
w.accumulate(frame)
psd_db = w.psd_db() # averaged power spectrum, dB
psd_dbhz = w.psd_dbhz() # PSD, dB/Hz (ENBW / fs normalised)
per_band = w.band_power(np.array([-2e5, -1e5, 1e5, 2e5])) # dB per band
total = w.total_band_power(np.array([-2e5, -1e5, 1e5, 2e5]))
obw = w.occupied_bw(0.99) # occupied bandwidth, Hz
nf = w.noise_floor() # median dB level
snr = w.snr(-1e5, 1e5) # peak-in-band minus noise floor, dB
sfdr = w.sfdr(min_db=-120.0) # spurious-free dynamic range, dB
# spectral peaks compose with the free function on the averaged trace:
peaks = find_peaks_f32(w.psd_db(), n_peaks=5, min_db=-60.0)
All spectra are DC-centred, matching find_peaks_f32's bin → frequency
convention. The PSD getters return None until the first frame is accumulated.
Welch
¶
Create an averaging PSD estimator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
n constructor parameter. |
1024
|
fs
|
float
|
fs constructor parameter. |
1.0
|
window
|
Literal['hann', 'kaiser']
|
window constructor parameter. |
"hann"
|
beta
|
float
|
beta constructor parameter. |
0.0
|
mode
|
Literal['mean', 'exp', 'maxhold', 'minhold']
|
mode constructor parameter. |
"mean"
|
alpha
|
float
|
alpha constructor parameter. |
0.1
|
Examples:
Create with defaults:
>>> from doppler.spectral import Welch
>>> obj = Welch(n=1024, fs=1.0, window="hann", beta=0.0, mode="mean", alpha=0.1)
accumulate
¶
Window, FFT and fold floor(n_in/n) cf32 frames into the average.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
Complex baseband samples (cf32). |
required |
Examples:
>>> import numpy as np
>>> from doppler.spectral import Welch
>>> n = 64
>>> w = Welch(n=n, fs=1.0, window="hann", mode="mean")
>>> k = 8
>>> x = np.exp(2j*np.pi*k*np.arange(n)/n).astype(np.complex64)
>>> for _ in range(4):
... w.accumulate(x)
>>> psd = w.psd_db()
>>> psd.shape
(64,)
>>> int(np.argmax(psd)) == n // 2 + k
True
>>> w.count
4
psd_db
¶
Averaged power spectrum in dB (None before any accumulate).
Returns:
| Type | Description |
|---|---|
NDArray[float32]
|
n, or 0 if empty. |
psd_dbhz
¶
Averaged power spectral density in dB/Hz (None before any accumulate).
Returns:
| Type | Description |
|---|---|
NDArray[float32]
|
Output. |
Examples:
band_power
¶
Integrated power per band in dB; bands = [lo0,hi0,lo1,hi1,...] Hz.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bands
|
NDArray[float64]
|
Flat [lo,hi,...] band edges, Hz. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[float32]
|
n_bands, or 0 if empty. |
Examples:
total_band_power
¶
Total integrated power across all bands in dB.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bands
|
NDArray[float64]
|
Flat [lo,hi,...] band edges, Hz. |
required |
Returns:
| Type | Description |
|---|---|
float
|
Total band power in dB (dB floor if empty). |
occupied_bw
¶
Occupied bandwidth in Hz holding the given fraction of total power.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fraction
|
float
|
Power fraction in (0, 1], e.g. 0.99. |
required |
Returns:
| Type | Description |
|---|---|
float
|
Occupied bandwidth in Hz (0 if empty or no power). |
noise_floor
¶
Median of the averaged dB trace (noise-floor estimate).
Returns:
| Type | Description |
|---|---|
float
|
Median dB level (0 if empty). |
snr
¶
Peak-in-band level minus noise floor, in dB.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lo_hz
|
float
|
Band lower edge, Hz. |
required |
hi_hz
|
float
|
Band upper edge, Hz. |
required |
Returns:
| Type | Description |
|---|---|
float
|
SNR in dB (0 if empty). |
sfdr
¶
Spurious-free dynamic range in dB from the top two peaks.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
min_db
|
float
|
Minimum peak level considered, dB. |
required |
Returns:
| Type | Description |
|---|---|
float
|
Carrier-minus-highest-spur level in dB (0 if fewer than two peaks). |
Spectral helpers¶
Window functions (hann_window, kaiser_window + its kaiser_enbw equivalent
noise bandwidth), magnitude conversion to dB (magnitude_db_cf32 /
magnitude_db_cf64), and peak finding (find_peaks_f32) — the building blocks
for a spectrum display.
import numpy as np
from doppler.spectral import (
FFT, hann_window, magnitude_db_cf32, find_peaks_f32,
)
x = (np.random.randn(1024) + 1j * np.random.randn(1024)).astype(np.complex64)
w = np.empty(1024, dtype=np.float32)
hann_window(w) # fill in place
spec = np.empty_like(x)
FFT(1024, -1).execute(x * w, spec)
db = magnitude_db_cf32(spec, lin_floor=1e-12, offset_db=0.0)
peaks = find_peaks_f32(db, n_peaks=5, min_db=-60.0)
magnitude_db_cf32
¶
Magnitude db cf32.
magnitude_db_cf64
¶
magnitude_db_cf64(x: NDArray[complex128], lin_floor: float, offset_db: float) -> NDArray[np.float32]
Magnitude db cf64.
find_peaks_f32
¶
Find peaks f32.
obw_from_power
¶
Obw from power.