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 (CorrDetector, CorrDetector2D), 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. For how PSD (below) anchors the dBFS reference for every measurement and display consumer, see the Spectral & Measurement API Map.


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. ref may be freed after this returns. With dwell == 1 every call produces output; with larger values the accumulator absorbs dwell frames before dumping.

Parameters:

Name Type Description Default
ref NDArray[complex64]

Reference signal, CF32, length n.

required
dwell int

Integration depth; must be >= 1. Pass 1 for immediate output on every call.

1
nthreads int

Accepted for API compatibility; ignored.

1
n_out int

Inverse/output length; 0 => native (n). Must be >= n. A larger value zero-pads the cross-spectrum before the inverse, returning the band-limited (Dirichlet) interpolation of the correlation on a finer length-n_out grid — same peak, sub-bin lag resolution. Native is bit-exact and allocates no extra buffer.

0

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=1, nthreads=1)
>>> corr.n, corr.dwell, corr.count
(4, 1, 0)

n property

n: int

FFT / reference length (samples).

n_out property

n_out: int

Output length (== n unless decoupled).

dwell property

dwell: int

Integration depth; dump every dwell calls.

count property

count: int

Frames accumulated so far (0 … dwell-1).

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: NDArray[complex64],
    out: NDArray[complex64] | None = None,
) -> NDArray[np.complex64]

Correlate one frame and optionally dump the coherent accumulator. Runs: forward FFT → pointwise multiply with ref_spec → accumulate the cross-spectrum; on dump, inverse FFT → normalise (÷ n). Accumulating in the frequency domain and inverting once is exactly the per-frame inverse summed, by linearity of the IFFT — valid because the dwell is coherent (a complex sum); a non-coherent (magnitude) integration could not defer the inverse. On the dwell-th call out is written, the accumulator is zeroed, and the counter resets; the function returns n_out. All other calls return 0 and leave out unmodified. In Python, a dump returns an ndarray and a no-dump returns None.

Parameters:

Name Type Description Default
x NDArray[complex64]

Input.

required

Returns:

Type Description
NDArray[complex64]

n_out on a dump call (or max_out if smaller), 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)]

execute_max_out

execute_max_out() -> int

Maximum output samples per execute call (== n_out).

Returns:

Type Description
int

Output.

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 Corr 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 Corr 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 Corr 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__() -> Corr

Enter a context manager, returning this object.

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

Returns:

Type Description
Corr

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

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.

...

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

Reference image, 2-D (ny, nx) CF32 ndarray in Python.

required
dwell int

Integration depth; must be >= 1.

1
nthreads int

Accepted for API compatibility; ignored.

1
ny_out int

Inverse/output rows; 0 => native (ny). Must be >= ny. A larger output zero-pads the cross-spectrum before the inverse, returning the band-limited (Dirichlet) interpolation of the correlation on a finer (ny_out, nx_out) grid — same peak, sub-bin resolution. Native is bit-exact and allocates no extra buffers.

0
nx_out int

Inverse/output columns; 0 => native (nx). Must be >= nx.

0

Examples:

>>> from doppler.spectral import Corr2D
>>> import numpy as np
>>> ref = np.zeros((4, 4), dtype=np.complex64); ref[0, 0] = 1.0
>>> c = Corr2D(ref=ref, dwell=1, nthreads=1)
>>> c.ny, c.nx, c.dwell, c.count
(4, 4, 1, 0)

ny property

ny: int

Row count.

nx property

nx: int

Column count.

ny_out property

ny_out: int

Output rows (== ny unless decoupled).

nx_out property

nx_out: int

Output columns (== nx unless decoupled).

n_out property

n_out: int

ny_out * nx_out — output element count.

dwell property

dwell: int

Integration depth.

count property

count: int

Frames accumulated (0 … dwell-1).

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: NDArray[complex64],
    out: NDArray[complex64] | None = None,
) -> 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 → accumulate the cross-spectrum; on dump, IFFT2 → normalise (÷ nynx). Accumulating in the frequency domain and inverting once is exactly the per-frame inverse summed, by linearity of the IFFT — valid because the dwell is coherent (a complex sum); a non-coherent (magnitude) integration could not defer the inverse. 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 NDArray[complex64]

Input.

required

Returns:

Type Description
NDArray[complex64]

ny*nx on a dump (or max_out if smaller), 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)]

execute_max_out

execute_max_out() -> int

Maximum output samples per execute call (always == ny*nx).

Returns:

Type Description
int

Output.

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 Corr2D 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 Corr2D 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 Corr2D 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__() -> Corr2D

Enter a context manager, returning this object.

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

Returns:

Type Description
Corr2D

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

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.

...

Streaming detection

CorrDetector 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 CorrDetector

ref = np.exp(2j * np.pi * 0.1 * np.arange(1024)).astype(np.complex64)
det = CorrDetector(ref, dwell=4, threshold=12.0)      # ~12 dB peak-to-noise


def stream_chunks():                              # a real CF32 source
    for _ in range(4):
        yield (np.random.randn(1024)
               + 1j * np.random.randn(1024)).astype(np.complex64)


for chunk in stream_chunks():                     # any chunk size
    for hit in det.push(chunk.astype(np.complex64)):
        print("detection:", hit)                  # (lag, peak, noise, stat)

CorrDetector2D is the 2-D streaming detector over a grid.

CorrDetector

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 threshold; a det_result_t is emitted when it passes. Setting 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]

Reference signal, CF32 ndarray of length n.

required
dwell int

Int-dump depth; must be >= 1.

1
noise_lo int

Lower noise bin index (inclusive, 0-based).

0
noise_hi int

Upper noise bin index (inclusive, < n).

n-1
noise_mode Literal['mean', 'median', 'min', 'max']

Noise aggregation: "mean", "median", "min", or "max".

"mean"
threshold float

Test-stat gate; 0.0 = always emit.

0.0
nthreads int

Accepted for API compatibility; ignored.

1

Examples:

>>> from doppler.spectral import CorrDetector
>>> import numpy as np
>>> ref = np.zeros(8, dtype=np.complex64); ref[0] = 1.0
>>> det = CorrDetector(ref=ref, dwell=1, noise_lo=1, noise_hi=7,
...                noise_mode="mean", threshold=0.0)
>>> det.n, det.dwell, det.ring_cap
(8, 1, 512)

n property

n: int

Frame / FFT length in complex samples.

dwell property

dwell: int

Integration depth; dump every dwell calls.

count property

count: int

Frames accumulated so far (0 … dwell-1).

ring_cap property

ring_cap: int

Ring buffer capacity in complex samples.

noise_lo property

noise_lo: int

Noise bin range lower bound (inclusive).

noise_hi property

noise_hi: int

Noise bin range upper bound (inclusive).

threshold property

threshold: float

0 = always fire; >0 = gate on test_stat.

last_corr property

last_corr: NDArray[complex64]

The correlation vector from the most recent push() that produced a result (None before that). This is a zero-copy view into a buffer owned by the detector and reused every push() -- the next push() (even one that doesn't produce a result) overwrites it in place. Copy the array before the next push() if you need to retain it.

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 CorrDetector
>>> import numpy as np
>>> ref = np.zeros(8, dtype=np.complex64); ref[0] = 1.0
>>> det = CorrDetector(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 result.

Examples:

>>> from doppler.spectral import CorrDetector
>>> import numpy as np
>>> ref = np.zeros(8, dtype=np.complex64); ref[0] = 1.0
>>> det = CorrDetector(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)

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 CorrDetector 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 CorrDetector 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 CorrDetector 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__() -> CorrDetector

Enter a context manager, returning this object.

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

Returns:

Type Description
CorrDetector

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

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.

...

CorrDetector2D

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 ref and the push input.

Parameters:

Name Type Description Default
ref NDArray[complex64]

2-D reference image, (ny, nx) CF32 ndarray in Python.

required
dwell int

Int-dump depth; must be >= 1.

1
noise_lo int

Lower flat-index noise bin (inclusive, 0-based).

0
noise_hi int

Upper flat-index noise bin (inclusive, < ny*nx).

ny*nx-1
noise_mode Literal['mean', 'median', 'min', 'max']

Noise aggregation: "mean", "median", "min", or "max".

"mean"
threshold float

Test-stat gate; 0.0 = always emit.

0.0
nthreads int

Accepted for API compatibility; ignored.

1

Examples:

>>> from doppler.spectral import CorrDetector2D
>>> import numpy as np
>>> ref = np.zeros((4, 4), dtype=np.complex64); ref[0, 0] = 1.0
>>> det = CorrDetector2D(ref=ref, dwell=1, noise_lo=1, noise_hi=15,
...                  noise_mode="mean", threshold=0.0)
>>> det.ny, det.nx, det.n, det.dwell
(4, 4, 16, 1)

ny property

ny: int

Number of rows.

nx property

nx: int

Number of columns.

n property

n: int

ny * nx — total frame length.

dwell property

dwell: int

Integration depth.

count property

count: int

Frames accumulated (0 … dwell-1).

ring_cap property

ring_cap: int

Ring buffer capacity in complex samples.

noise_lo property

noise_lo: int

Noise bin range lower bound (inclusive).

noise_hi property

noise_hi: int

Noise bin range upper bound (inclusive).

threshold property

threshold: float

0 = always fire; >0 = gate on test_stat.

last_corr property

last_corr: NDArray[complex64]

The correlation vector from the most recent push() that produced a result (None before that). This is a zero-copy view into a buffer owned by the detector and reused every push() -- the next push() (even one that doesn't produce a result) overwrites it in place. Copy the array before the next push() if you need to retain it.

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 CorrDetector2D
>>> import numpy as np
>>> ref = np.zeros((4, 4), dtype=np.complex64); ref[0, 0] = 1.0
>>> det = CorrDetector2D(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 result.

Examples:

>>> from doppler.spectral import CorrDetector2D
>>> import numpy as np
>>> ref = np.zeros((4, 4), dtype=np.complex64); ref[0, 0] = 1.0
>>> det = CorrDetector2D(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)

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 CorrDetector2D 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 CorrDetector2D 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 CorrDetector2D 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__() -> CorrDetector2D

Enter a context manager, returning this object.

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

Returns:

Type Description
CorrDetector2D

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

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.

...

Averaging PSD & measurements

PSD is a stateful Welch-method (averaging) power-spectral-density estimator — the single PSD core the measurement suite also consumes (see the Power Spectra & Measurements guide for the usage walk-through). A capture longer than n is split into floor(len/n) segments; each is windowed, zero-padded to nfft = next_pow2(n·pad), FFT'd, converted to power, fftshifted to DC-centred order and folded into a running average (AccTrace, mode of "mean" / "exp" / "maxhold" / "minhold"). Feed complex baseband with accumulate() or real input with accumulate_real().

import numpy as np
from doppler.spectral import PSD, find_peaks_f32

cf32_capture = (np.random.randn(8192)
                + 1j * np.random.randn(8192)).astype(np.complex64)
w = PSD(n=1024, fs=1e6, window="kaiser", beta=8.0,
          pad=2, full_scale=1.0, bits=0, mode="mean")   # bits>0 -> 2**(bits-1)
w.accumulate(cf32_capture)                 # or w.accumulate_real(f32_capture)
w.n, w.nfft                                # 1024, 2048 (= next_pow2(1024 * 2))

# display spectra (DC-centred, dBFS w.r.t. full_scale)
psd_db = w.psd_db()                        # averaged power spectrum, dB
psd_dbhz = w.psd_dbhz()                    # PSD, dB/Hz (ENBW / fs normalised)

# raw linear power (cg²-normalised; full_scale NOT applied)
two = w.power_twosided()                   # length nfft, DC-centred
one = w.power_onesided()                   # length nfft//2 + 1, folded to [0, fs/2]

# band / level statistics
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)

The pad factor interpolates the spectrum (finer bin spacing, not finer resolution); full_scale sets the 0-dBFS reference for the dB getters only — the linear power_* accessors are unaffected. All spectra are DC-centred, matching find_peaks_f32's bin → frequency convention. The PSD getters return None until the first frame is accumulated.

PSD

Create an averaging PSD estimator.

Parameters:

Name Type Description Default
n int

Window / frame length in samples. Must be >= 2.

1024
fs float

Sample rate in Hz (used for dB/Hz and band frequencies).

1.0
window Literal['hann', 'kaiser', 'blackman-harris']

Window index: 0 = Hann, 1 = Kaiser, 2 = Blackman-Harris.

"hann"
beta float

Kaiser beta (ignored for Hann/Blackman-Harris).

0.0
pad int

Zero-pad factor (>= 1); nfft = next_pow2(n * pad).

1
full_scale float

Amplitude that reads 0 dBFS in the dB getters (> 0). Ignored when bits

0.

1.0
bits int

ADC depth: when > 0, sets full_scale = 2^(bits-1) (the single definition of the dBFS reference); 0 = use full_scale directly.

0
mode Literal['mean', 'exp', 'maxhold', 'minhold']

Averaging mode index (0=mean, 1=exp, 2=maxhold, 3=minhold).

"mean"
alpha float

EMA smoothing factor (exp mode only).

0.1

Examples:

>>> from doppler.spectral import PSD
>>> w = PSD(n=1024, fs=1.0e6, window="kaiser", beta=8.0, mode="mean")
>>> w.n, w.fs
(1024, 1000000.0)
>>> round(w.rbw / (w.fs / w.n), 3) == round(w.enbw, 3)
True

n property

n: int

Window / frame length (samples).

nfft property

nfft: int

Zero-padded transform length.

fs property

fs: float

Sample rate, Hz.

full_scale property

full_scale: float

Amplitude that reads 0 dBFS.

bits property

bits: int

ADC depth that set full_scale, else 0.

enbw property

enbw: float

Equivalent noise bandwidth, bins.

rbw property

rbw: float

Rbw.

count property

count: int

Frames folded in so far.

mode property

mode: int

Reduction 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 PSD
>>> n = 64
>>> w = PSD(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

accumulate_real

accumulate_real(x: NDArray[float32]) -> None

Window, zero-pad, FFT and fold floor(n_in/n) real frames into the average.

Parameters:

Name Type Description Default
x NDArray[float32]

Real samples (f32).

required

reset

reset() -> None

Discard the running average; counters return to zero.

psd_db

psd_db(
    count: int = 1, out: NDArray[float32] | None = None
) -> NDArray[np.float32]

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

Returns:

Type Description
NDArray[float32]

min(n, max_out), or 0 if empty.

psd_db_max_out

psd_db_max_out() -> int

Output capacity hint for psd_db(); equals nfft.

Returns:

Type Description
int

Output.

psd_dbhz

psd_dbhz(
    count: int = 1, out: NDArray[float32] | None = None
) -> 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 PSD
>>> w = PSD(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

psd_dbhz_max_out

psd_dbhz_max_out() -> int

Output capacity hint for psd_dbhz(); equals n.

Returns:

Type Description
int

Output.

power_twosided

power_twosided(
    count: int = 1, out: NDArray[float32] | None = None
) -> NDArray[np.float32]

Averaged linear power, DC-centred two-sided (length nfft); cg^2-normalised.

Returns:

Type Description
NDArray[float32]

min(nfft, max_out), or 0 if empty.

power_twosided_max_out

power_twosided_max_out() -> int

Output capacity hint for psd_power_twosided(); equals nfft.

Returns:

Type Description
int

Output.

power_onesided

power_onesided(
    count: int = 1, out: NDArray[float32] | None = None
) -> NDArray[np.float32]

Averaged linear power, one-sided fold (length nfft/2+1); cg^2-normalised.

Returns:

Type Description
NDArray[float32]

min(nfft/2 + 1, max_out), or 0 if empty.

power_onesided_max_out

power_onesided_max_out() -> int

Output capacity hint for psd_power_onesided(); equals nfft/2+1.

Returns:

Type Description
int

Output.

band_power

band_power(
    bands: NDArray[float64],
    out: NDArray[float32] | None = None,
) -> 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]

min(n_bands, max_out), or 0 if empty.

Examples:

>>> import numpy as np
>>> from doppler.spectral import PSD
>>> w = PSD(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,)

band_power_max_out

band_power_max_out() -> int

Output capacity hint for band_power(); 0 (binding sizes from bands).

Returns:

Type Description
int

Output.

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

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 PSD 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 PSD 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 PSD 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__() -> PSD

Enter a context manager, returning this object.

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

Returns:

Type Description
PSD

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

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.

...

Spectral helpers

Window functions (hann_window, kaiser_window + its kaiser_enbw equivalent noise bandwidth and kaiser_beta_for_sidelobe window-design helper, and blackman_harris_window for deep sidelobe rejection ~92 dB), 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 = FFT(1024, -1).execute_cf32(x * w)          # returns the transform
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

Fill w with a Hann (raised-cosine) window. Computes w(k) = 0.5*(1 - cos(2π k/(N-1))) for k = 0..N-1. The window tapers smoothly to zero at both endpoints, providing ~31 dB first-sidelobe rejection. Takes no shape parameter; use Kaiser for adjustable roll-off.

Parameters:

Name Type Description Default
w NDArray[float32]

Output buffer modified in-place; must be length >= 1.

required

Examples:

>>> from doppler.spectral import hann_window
>>> import numpy as np
>>> w = np.zeros(8, dtype=np.float32)
>>> hann_window(w)
>>> [round(v, 4) for v in w.tolist()]
[0.0, 0.1883, 0.6113, 0.9505, 0.9505, 0.6113, 0.1883, 0.0]

kaiser_window

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

Fill w with a Kaiser window of shape parameter beta. I0 is computed via the converging power-series expansion. Increasing beta raises sidelobe attenuation at the cost of a wider main lobe (beta=0 → rectangular, beta≈6 → ~60 dB sidelobe rejection). The output is normalised so that w[0] = w[N-1] = I0(0)/I0(beta).

Parameters:

Name Type Description Default
w NDArray[float32]

Output buffer modified in-place; must be length >= 1.

required
beta float

Window shape parameter (float, >= 0).

required

Examples:

>>> from doppler.spectral import kaiser_window
>>> import numpy as np
>>> w = np.zeros(8, dtype=np.float32)
>>> kaiser_window(w, 6.0)
>>> [round(v, 4) for v in w.tolist()]
[0.0149, 0.1998, 0.5913, 0.9454, 0.9454, 0.5913, 0.1998, 0.0149]

kaiser_enbw

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

Compute the equivalent noise bandwidth of a window in bins. ENBW = N * sum(w²) / (sum(w))² quantifies how many noise bins the window smears into the main lobe. A rectangular window has ENBW = 1.0; tapered windows are > 1.0. Works with any window type, not just Kaiser.

Parameters:

Name Type Description Default
w NDArray[float32]

Float32 window coefficients array; any length >= 1.

required

Returns:

Type Description
float

ENBW in bins (dimensionless).

Examples:

>>> from doppler.spectral import kaiser_enbw, hann_window
>>> import numpy as np
>>> w = np.zeros(8, dtype=np.float32)
>>> hann_window(w)
>>> round(kaiser_enbw(w), 4)
1.7143

kaiser_beta_for_sidelobe

kaiser_beta_for_sidelobe(atten_db: float) -> float

Kaiser beta achieving a target window peak-sidelobe attenuation.

Inverts the Kaiser window-design formula (Kaiser 1974) so the window's own peak sidelobe sits at -atten_db: A > 60 dB : beta = 0.12438 * (A + 6.3) 13.26 < A <= 60 dB : beta = 0.76609(A-13.26)^0.4 + 0.09834(A-13.26) A <= 13.26 dB : beta = 0.0 (rectangular, sidelobes ~ -13.3 dB) Picking the smallest beta meeting a dynamic-range target keeps the main lobe (hence ENBW / resolution bandwidth) as narrow as the requirement allows — the basis of the measurement suite's auto-window selection.

This differs from doppler.resample.kaiser_beta(), which uses the Kaiser FIR-filter formula (A there is a filter stopband ripple, not a window sidelobe — about 13 dB lower for the same beta).

Parameters:

Name Type Description Default
atten_db float

Desired window peak-sidelobe attenuation in dB (positive).

required

Returns:

Type Description
float

Kaiser beta (>= 0.0).

Examples:

>>> from doppler.spectral import kaiser_beta_for_sidelobe
>>> round(kaiser_beta_for_sidelobe(90.0), 4)
11.9778
>>> kaiser_beta_for_sidelobe(10.0)
0.0

blackman_harris_window

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

Fill w with a 4-term Blackman-Harris window. Computes the minimum 4-term Blackman-Harris window: w(k) = 0.35875 - 0.48829cos(2πk/(N-1)) + 0.14128cos(4πk/(N-1)) - 0.01168*cos(6πk/(N-1)) for k = 0..N-1. Provides approximately 92 dB first-sidelobe rejection, far deeper than Hann (~31 dB) or Kaiser at β=8 (~80 dB). Use for quantization and decimation spectra where you need to see low-level artefacts below the noise floor.

Parameters:

Name Type Description Default
w NDArray[float32]

Output buffer modified in-place; must be length >= 1.

required

Examples:

>>> from doppler.spectral import blackman_harris_window
>>> import numpy as np
>>> w = np.zeros(8, dtype=np.float32)
>>> blackman_harris_window(w)
>>> [round(v, 4) for v in w.tolist()]
[0.0001, 0.0334, 0.3328, 0.8894, 0.8894, 0.3328, 0.0334, 0.0001]

magnitude_db_cf32

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

Convert a CF32 complex spectrum to F32 dB magnitudes. Computes out(k) = 20*log10(max(|x(k)|, lin_floor)) + offset_db for each bin. The lin_floor guard prevents log10(0); a value of 1e-12 corresponds to a -240 dB noise floor. offset_db shifts the entire output for calibration (e.g., normalise to 0 dBFS).

Parameters:

Name Type Description Default
x NDArray[complex64]

CF32 complex spectrum array, length x_len.

required
lin_floor float

Linear amplitude floor (must be > 0, e.g. 1e-12).

required
offset_db float

Calibration offset added to every output bin.

required

Returns:

Type Description
NDArray[float32]

Output.

Examples:

>>> from doppler.spectral import magnitude_db_cf32
>>> import numpy as np
>>> x = np.array([1+0j, 0.1+0j, 0+0j], dtype=np.complex64)
>>> magnitude_db_cf32(x, 1e-12, 0.0).tolist()
[0.0, -20.0, -240.0]

magnitude_db_cf64

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

Convert a CF64 complex spectrum to F32 dB magnitudes. Double-precision variant of magnitude_db_cf32(). Accepts a CF64 input array and a double lin_floor; output is still F32 because downstream display code typically works in single precision. The formula and offset_db semantics are identical.

Parameters:

Name Type Description Default
x NDArray[complex128]

CF64 complex spectrum array, length x_len.

required
lin_floor float

Linear amplitude floor (double, must be > 0).

required
offset_db float

Calibration offset added to every output bin.

required

Returns:

Type Description
NDArray[float32]

Output.

Examples:

>>> from doppler.spectral import magnitude_db_cf64
>>> import numpy as np
>>> x = np.array([1+0j, 10+0j], dtype=np.complex128)
>>> magnitude_db_cf64(x, 1e-12, 0.0).tolist()
[0.0, 20.0]

find_peaks_f32

find_peaks_f32(
    db: NDArray[float32], n_peaks: int, min_db: float
) -> Any

Find up to n_peaks local maxima in a DC-centred F32 dB spectrum. Three-step algorithm: (1) local-max scan — db[k] > db[k-1] && db[k] >= db[k+1] with db[k] > min_db; (2) parabolic interpolation on each local maximum to produce sub-bin freq_norm accuracy; (3) sort descending and return the top n_peaks. freq_norm is DC-centred: bin i maps to freq_norm = (i - N/2) / N so DC (bin N/2) → 0.0 and the first negative frequency bin → −0.5. The spectrum must have at least 3 bins.

Parameters:

Name Type Description Default
db NDArray[float32]

F32 dB spectrum, DC-centred, length >= 3.

required
n_peaks int

Maximum number of peaks to return.

required
min_db float

Amplitude gate; local maxima below this are discarded.

required

Returns:

Type Description
Any

Number of dp_peak_t entries written to result.

Examples:

>>> from doppler.spectral import find_peaks_f32
>>> import numpy as np
>>> db = np.full(32, -60.0, dtype=np.float32)
>>> db[7] = -15.0; db[8] = -10.0; db[9] = -15.0
>>> peaks = find_peaks_f32(db, 2, -30.0)
>>> peaks
[(-0.25, -10.0)]

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.

GalleryCarrierAcquisition: RRC Pulse Shaping, Correlation and Detection, 2-D Acquisition Grid, Gallery, Four WCDMA Carriers — PSD, band_power, AccTrace GuidesDSSS Burst Acquisition, Power Spectra & Measurements DesignAPI taxonomy: the DSP building-block hierarchy and its naming axis, Corr2D: decoupled (interpolated) inverse length, DSSS acquisition: stateless, parallel, dynamics-capable, Design, Measurement Suite — single-tone ADC / spectral metrics, Spectral & Measurement API Map ContributingCode coverage, DSSS Primary Use Cases for Code Acquisition Design