Skip to content

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

n property

n: int

N.

dwell property

dwell: int

Dwell.

count property

count: int

Count.

reset

reset() -> None

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:

>>> 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=3)
>>> _ = corr.execute(np.ones(4, dtype=np.complex64))
>>> corr.count
1
>>> corr.reset()
>>> corr.count
0

execute

execute(x: complex) -> NDArray[np.complex64]

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)]

destroy

destroy() -> None

Release C resources immediately.

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

ny property

ny: int

Ny.

nx property

nx: int

Nx.

dwell property

dwell: int

Dwell.

count property

count: int

Count.

reset

reset() -> None

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:

>>> 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=3)
>>> _ = c.execute(np.ones((2, 2), dtype=np.complex64))
>>> c.count
1
>>> c.reset()
>>> c.count
0

execute

execute(x: complex) -> NDArray[np.complex64]

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)]

destroy

destroy() -> None

Release C resources immediately.


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

n property

n: int

N.

dwell property

dwell: int

Dwell.

count property

count: int

Count.

ring_cap property

ring_cap: int

Ring cap.

noise_lo property

noise_lo: int

Noise lo.

noise_hi property

noise_hi: int

Noise hi.

threshold property

threshold: float

Threshold.

last_corr property

last_corr: NDArray[complex64]

Last corr.

reset

reset() -> None

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

push(x: complex) -> list[tuple[int, float, float, float]]

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)

destroy

destroy() -> None

Release C resources immediately.

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

ny property

ny: int

Ny.

nx property

nx: int

Nx.

n property

n: int

N.

dwell property

dwell: int

Dwell.

count property

count: int

Count.

ring_cap property

ring_cap: int

Ring cap.

noise_lo property

noise_lo: int

Noise lo.

noise_hi property

noise_hi: int

Noise hi.

threshold property

threshold: float

Threshold.

last_corr property

last_corr: NDArray[complex64]

Last corr.

reset

reset() -> None

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

push(x: complex) -> list[tuple[int, int, float, float, float]]

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)

destroy

destroy() -> None

Release C resources immediately.


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)

n property

n: int

N.

fs property

fs: float

Fs.

enbw property

enbw: float

Enbw.

rbw property

rbw: float

Rbw.

count property

count: int

Count.

mode property

mode: int

Mode.

accumulate

accumulate(x: NDArray[complex64]) -> None

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

reset

reset() -> None

Discard the running average; counters return to zero.

psd_db

psd_db() -> NDArray[np.float32]

Averaged power spectrum in dB (None before any accumulate).

Returns:

Type Description
NDArray[float32]

n, or 0 if empty.

psd_dbhz

psd_dbhz() -> NDArray[np.float32]

Averaged power spectral density in dB/Hz (None before any accumulate).

Returns:

Type Description
NDArray[float32]

Output.

Examples:

>>> import numpy as np
>>> from doppler.spectral import Welch
>>> w = Welch(n=32, fs=2.0, window="hann", mode="mean")
>>> w.accumulate(np.ones(32, dtype=np.complex64))
>>> a = w.psd_db(); b = w.psd_dbhz()
>>> bool(np.allclose(a - b, (a - b)[0]))   # offset is a constant
True

band_power

band_power(bands: NDArray[float64]) -> NDArray[np.float32]

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:

>>> import numpy as np
>>> from doppler.spectral import Welch
>>> w = Welch(n=64, fs=1.0, window="hann", mode="mean")
>>> w.accumulate(np.ones(64, dtype=np.complex64))
>>> pb = w.band_power(np.array([-0.5, 0.0, 0.0, 0.5]))
>>> pb.shape
(2,)

total_band_power

total_band_power(bands: NDArray[float64]) -> float

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_bw(fraction: float) -> float

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

noise_floor() -> float

Median of the averaged dB trace (noise-floor estimate).

Returns:

Type Description
float

Median dB level (0 if empty).

snr

snr(lo_hz: float, hi_hz: float) -> float

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

sfdr(min_db: float) -> float

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).

destroy

destroy() -> None

Release C resources immediately.


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)

hann_window

hann_window(w: NDArray[float32]) -> None

Hann window.

kaiser_window

kaiser_window(w: NDArray[float32], beta: float) -> None

Kaiser window.

kaiser_enbw

kaiser_enbw(w: NDArray[float32]) -> float

Kaiser enbw.

magnitude_db_cf32

magnitude_db_cf32(x: NDArray[complex64], lin_floor: float, offset_db: float) -> NDArray[np.float32]

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(db: NDArray[float32], n_peaks: int, min_db: float) -> Any

Find peaks f32.

obw_from_power

obw_from_power(pwr: NDArray[float64], fs: float, frac: float) -> float

Obw from power.

noise_floor_db

noise_floor_db(db: NDArray[float32]) -> float

Noise floor db.