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 one forward FFT and n complex multiplies, plus the inverse FFT on the call that dumps. 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: the reference is a 2-D (ny, nx) array, execute takes the input in either shape, and it returns a flat row-major array.

DelayDoppler is the Corr2D view for a delay-Doppler map. It takes the raw stream, ny code periods back to back, and returns the centered ny_out × nx map: rows are Doppler bins doppler_step cycles/sample apart, with zero Doppler on row ny_out // 2, and columns are delay lags. See the design section.

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

required
dwell int

Integration depth; must be >= 1 (0 is refused). 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

Raises:

Type Description
ValueError

If construction fails. The exception message is Corr: invalid parameter (need dwell >= 1, and n_out 0 or at least len(ref)).

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

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.

Layout. A frame is nynx complex samples in row-major order: row i is samples inx .. (i+1)*nx - 1. For Corr2D that is a (ny, nx) image; for DelayDoppler it is the raw stream, ny code periods of nx = code_len samples back to back. The map a dump produces is flat, n_out = ny_out * nx_out values (ny values with col_out), also row-major: reshape it to (ny_out, nx_out). On a DelayDoppler, axis 0 is Doppler (centered, zero Doppler on row ny_out / 2) and axis 1 is delay (the lag, in samples).

Ownership. In Python, a call that dumps returns a new array when no out= is given, and a view of out= when one is (a 1-D array holding at least n_out values; it is written on a dump only, and the next dump overwrites it). A call that does not dump returns None and touches neither.

Parameters:

Name Type Description Default
x NDArray[complex64]

Input frame, flat row-major CF32, length ny*nx.

required
out NDArray[complex64] | None

Output buffer for the correlation map (CF32, length n_out: ny_out*nx_out, or ny with col_out); written only on a dump call.

None

Returns:

Type Description
NDArray[complex64] | None

Python: the map on a dump, None otherwise; a frame that is not ny*nx samples is REFUSED with nothing read, written or counted, as a ValueError (#1925). C: n_out on a dump (or max_out if smaller), 0 otherwise, and SIZE_MAX for the refused frame. None when the C call writes no output.

Raises:

Type Description
ValueError

If the C call returns SIZE_MAX, its refusal value, in place of a count. The exception message is input length is not the frame length (n: Corr's n, or ny*nx); execute takes exactly one frame.

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 (dp_correlator_state_t::n_out -- ny_out*nx_out normally, or ny when a single dp_correlator_state_t::col_out was selected).

Returns:

Type Description
int

n_out, fixed at create().

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 the state that evolves as the object runs, including any value a setter changed after construction, so a blob taken now and restored later resumes from this point. Construction parameters are not restored: restore into an object built the same way. A core may check some of them and refuse a blob built differently.

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, including any value a setter changed after construction. Construction parameters are not restored: restore into an object built the same way. Length is validated against state_bytes() before the blob is handed to the C core, which may reject it too: a core may check some construction parameters and refuse a blob built differently.

Raises TypeError if blob is not bytes, ValueError if its length differs from state_bytes(), the exception listed under Raises if 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

Raises:

Type Description
MemoryError

If the C call returns DP_ERR_MEMORY. The exception message is set_state: the blob's carrier needs the delay-Doppler map rebuilt, and the rebuild could not allocate; the engine is unchanged.

ValueError

If the C core rejects the blob with any other non-zero status. The exception message is set_state rejected the blob.

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

Workers for the single-row-reference fast path's per-row stage: 1 runs serially (no threads), N runs N workers (the caller included), <= 0 uses every online core. The count is capped at ny, one row per worker at most. The output is bit-identical whatever the count. The general 2-D path and the col_out path run serially and ignore it.

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
col_out int

Emit ONLY this correlation lag, or < 0 for the whole map. When set, a dump writes ny values -- one per row -- instead of ny*nx_out, computed as the time-domain sum over p of conj(ref(p)) * row((p + col_out) mod nx), which is what R(i, col_out) expands to once the 1/nx cancels. No transform runs in either direction, so the cost per row is O(nx) rather than O(nx log nx). Requires the single-row-reference fast path (a caller that knows its lag is by construction correlating against a code replica) and the NATIVE output grid: an interpolated column is a fractional lag, which no time-domain sum produces. create() returns NULL if col_out >= 0 with a reference the fast path rejects, with ny_out or nx_out decoupled, or with a lag outside 0 .. nx-1.

-1

Raises:

Type Description
ValueError

If construction fails. The exception message is Corr2D: invalid parameter (need dwell >= 1, and ny_out and nx_out each 0 or at least the ref extent).

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.

col_out property

col_out: int

Output column, or < 0 for the full map.

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

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.

Layout. A frame is nynx complex samples in row-major order: row i is samples inx .. (i+1)*nx - 1. For Corr2D that is a (ny, nx) image; for DelayDoppler it is the raw stream, ny code periods of nx = code_len samples back to back. The map a dump produces is flat, n_out = ny_out * nx_out values (ny values with col_out), also row-major: reshape it to (ny_out, nx_out). On a DelayDoppler, axis 0 is Doppler (centered, zero Doppler on row ny_out / 2) and axis 1 is delay (the lag, in samples).

Ownership. In Python, a call that dumps returns a new array when no out= is given, and a view of out= when one is (a 1-D array holding at least n_out values; it is written on a dump only, and the next dump overwrites it). A call that does not dump returns None and touches neither.

Parameters:

Name Type Description Default
x NDArray[complex64]

Input frame, flat row-major CF32, length ny*nx.

required
out NDArray[complex64] | None

Output buffer for the correlation map (CF32, length n_out: ny_out*nx_out, or ny with col_out); written only on a dump call.

None

Returns:

Type Description
NDArray[complex64] | None

Python: the map on a dump, None otherwise; a frame that is not ny*nx samples is REFUSED with nothing read, written or counted, as a ValueError (#1925). C: n_out on a dump (or max_out if smaller), 0 otherwise, and SIZE_MAX for the refused frame. None when the C call writes no output.

Raises:

Type Description
ValueError

If the C call returns SIZE_MAX, its refusal value, in place of a count. The exception message is input length is not the frame length (n: Corr's n, or ny*nx); execute takes exactly one frame.

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 (dp_correlator_state_t::n_out -- ny_out*nx_out normally, or ny when a single dp_correlator_state_t::col_out was selected).

Returns:

Type Description
int

n_out, fixed at create().

execute_all

execute_all(
    x: NDArray[complex64],
    out: NDArray[complex64] | None = None,
) -> NDArray[np.complex64]

Every map of a batch, unreduced: row h of the (H, n_out) result is the map of frame h (in C, map h is at out + h * n_out).

Python: x is the H frames back to back, a flat array of H * ny * nx samples (H >= 1): frame h is the samples from h * ny * nx up to, not including, (h + 1) * ny * nx. With no out= each call returns a new array; with out= it returns a view of it, and the next call into the same out= overwrites that view. out= is a 2-D complex64 array of shape (rows, n_out) with at least H rows (a shorter one is refused); the result is its first H rows. The object's dwell is untouched.

Parameters:

Name Type Description Default
x NDArray[complex64]

H frames back to back (CF32).

required
out NDArray[complex64] | None

H * n_out values (CF32).

None

Returns:

Type Description
NDArray[complex64]

Python: the (H, n_out) array; a refused batch (empty, not a whole number of frames, or an out= with fewer than H rows) is a ValueError with nothing written. C: H * n_out, or SIZE_MAX for the refused batch.

Raises:

Type Description
ValueError

If the C call returns SIZE_MAX, its refusal value, in place of a count. The exception message is execute_all: the input must be a whole, nonzero number of frames, and out= must hold every map.

Examples:

>>> import numpy as np
>>> from doppler.spectral import DelayDoppler
>>> rng = np.random.default_rng(0)
>>> ny, nx = 8, 32
>>> code = np.exp(2j * np.pi * rng.random(nx)).astype(np.complex64)
>>> dd = DelayDoppler(code, ny=ny)
>>> frames = np.concatenate([np.tile(np.roll(code, d), ny)
...                          for d in (3, 11)])   # delays 3 and 11
>>> maps = dd.execute_all(frames)
>>> maps.shape
(2, 256)
>>> [int(np.argmax(np.abs(m))) % nx for m in maps]   # each map's delay
[3, 11]
>>> dd.count                    # the dwell never moved
0

execute_all_max_out

execute_all_max_out(n_in: int) -> int

Samples dp_correlator_execute_all() writes for n_in input samples: H * n_out, H = n_in / (ny * nx).

Parameters:

Name Type Description Default
n_in int

Input samples about to be passed.

required

Returns:

Type Description
int

The capacity execute_all needs for n_in samples: H * n_out values (C: SIZE_MAX if that overflows).

Examples:

>>> import numpy as np
>>> from doppler.spectral import DelayDoppler
>>> rng = np.random.default_rng(0)
>>> ny, nx = 8, 32
>>> code = np.exp(2j * np.pi * rng.random(nx)).astype(np.complex64)
>>> dd = DelayDoppler(code, ny=ny)
>>> frames = np.concatenate([np.tile(np.roll(code, d), ny)
...                          for d in (3, 11)])   # delays 3 and 11
>>> dd.execute_all_max_out(len(frames))   # 2 maps of 8 x 32
512

execute_coherent

execute_coherent(
    x: NDArray[complex64],
    out: NDArray[complex64] | None = None,
) -> NDArray[np.complex64]

The coherent sum of a batch's maps, inverted once.

Python: x is the H frames back to back, a flat array of H * ny * nx samples (H >= 1).

The frames' product spectra are summed and the sum is inverted, as a dwell does: the result is bit-identical to what a fresh object with dwell = H dumps for the same frames, and equal to the sum of the H maps up to rounding. The object's own dwell is untouched.

Parameters:

Name Type Description Default
x NDArray[complex64]

H frames back to back (CF32).

required
out NDArray[complex64] | None

n_out values (CF32).

None

Returns:

Type Description
NDArray[complex64]

Python: the one map, a new (n_out,) array; a refused batch is a ValueError with nothing written. C: n_out, or SIZE_MAX for the refused batch.

Raises:

Type Description
ValueError

If the C call returns SIZE_MAX, its refusal value, in place of a count. The exception message is execute_coherent: the input must be a whole, nonzero number of frames, and out= must hold one map.

Examples:

>>> import numpy as np
>>> from doppler.spectral import DelayDoppler
>>> rng = np.random.default_rng(0)
>>> ny, nx = 8, 32
>>> code = np.exp(2j * np.pi * rng.random(nx)).astype(np.complex64)
>>> dd = DelayDoppler(code, ny=ny)
>>> frames = np.concatenate([np.tile(np.roll(code, d), ny)
...                          for d in (3, 11)])   # delays 3 and 11
>>> x = np.tile(code, ny)
>>> total = dd.execute_coherent(np.concatenate([x, -x]))
>>> bool(np.abs(total).max() < 1e-3)          # x and -x cancel
True

execute_coherent_max_out

execute_coherent_max_out() -> int

Samples dp_correlator_execute_coherent() writes: n_out.

Returns:

Type Description
int

n_out, fixed at create().

Examples:

>>> import numpy as np
>>> from doppler.spectral import DelayDoppler
>>> rng = np.random.default_rng(0)
>>> ny, nx = 8, 32
>>> code = np.exp(2j * np.pi * rng.random(nx)).astype(np.complex64)
>>> dd = DelayDoppler(code, ny=ny)
>>> frames = np.concatenate([np.tile(np.roll(code, d), ny)
...                          for d in (3, 11)])   # delays 3 and 11
>>> dd.execute_coherent_max_out()
256

execute_power_sum

execute_power_sum(
    x: NDArray[complex64],
    out: NDArray[float32] | None = None,
) -> NDArray[np.float32]

The power of a batch's maps, summed cell by cell: each output cell is the sum over h of that cell's |map_h|^2, summed in h order in float.

Python: x is the H frames back to back, a flat array of H * ny * nx samples (H >= 1).

Unlike the coherent sum, phase does not cancel: frames x and -x sum to twice one map's power, where their coherent sum is zero.

Parameters:

Name Type Description Default
x NDArray[complex64]

H frames back to back (CF32).

required
out NDArray[float32] | None

n_out powers (float).

None

Returns:

Type Description
NDArray[float32]

Python: the (n_out,) float32 powers, a new array; a refused batch is a ValueError with nothing written. C: n_out, or SIZE_MAX for the refused batch.

Raises:

Type Description
ValueError

If the C call returns SIZE_MAX, its refusal value, in place of a count. The exception message is execute_power_sum: the input must be a whole, nonzero number of frames, and out= must hold one map.

Examples:

>>> import numpy as np
>>> from doppler.spectral import DelayDoppler
>>> rng = np.random.default_rng(0)
>>> ny, nx = 8, 32
>>> code = np.exp(2j * np.pi * rng.random(nx)).astype(np.complex64)
>>> dd = DelayDoppler(code, ny=ny)
>>> frames = np.concatenate([np.tile(np.roll(code, d), ny)
...                          for d in (3, 11)])   # delays 3 and 11
>>> x = np.tile(code, ny)
>>> power = dd.execute_power_sum(np.concatenate([x, -x]))
>>> one = np.abs(dd.execute_all(x)[0]) ** 2
>>> bool(np.allclose(power, 2 * one))        # x and -x do not cancel
True

execute_power_sum_max_out

execute_power_sum_max_out() -> int

Powers dp_correlator_execute_power_sum() writes: n_out.

Returns:

Type Description
int

n_out, fixed at create().

Examples:

>>> import numpy as np
>>> from doppler.spectral import DelayDoppler
>>> rng = np.random.default_rng(0)
>>> ny, nx = 8, 32
>>> code = np.exp(2j * np.pi * rng.random(nx)).astype(np.complex64)
>>> dd = DelayDoppler(code, ny=ny)
>>> frames = np.concatenate([np.tile(np.roll(code, d), ny)
...                          for d in (3, 11)])   # delays 3 and 11
>>> dd.execute_power_sum_max_out()
256

execute_power_max

execute_power_max(
    x: NDArray[complex64],
) -> tuple[NDArray[np.float32], NDArray[np.uint32]]

The largest power over a batch's maps, cell by cell, and the map it came from.

Python: x is the H frames back to back, a flat array of H * ny * nx samples (H >= 1).

Each cell of out is the largest |map_h|^2 that cell takes over the maps, and the same cell of out1 is the h it came from. A tie goes to the lowest h: a later map replaces the running maximum only when it is strictly greater.

Parameters:

Name Type Description Default
x NDArray[complex64]

H frames back to back (CF32); H at most 2^32.

required

Returns:

Type Description
tuple[NDArray[float32], NDArray[uint32]]

Python: the tuple (power, index), each (n_out,): float32 powers and uint32 map indices, always new arrays (this call takes no out=); a refused batch, or H above 2**32, is a ValueError. C: n_out, or SIZE_MAX with nothing written.

Raises:

Type Description
ValueError

If the C call returns SIZE_MAX, its refusal value, in place of a count. The exception message is execute_power_max: the input must be a whole, nonzero number of frames (at most 2**32), and out= must hold one map.

Examples:

>>> import numpy as np
>>> from doppler.spectral import DelayDoppler
>>> rng = np.random.default_rng(0)
>>> ny, nx = 8, 32
>>> code = np.exp(2j * np.pi * rng.random(nx)).astype(np.complex64)
>>> dd = DelayDoppler(code, ny=ny)
>>> frames = np.concatenate([np.tile(np.roll(code, d), ny)
...                          for d in (3, 11)])   # delays 3 and 11
>>> power, index = dd.execute_power_max(frames)
>>> int(index[4 * nx + 3]), int(index[4 * nx + 11])  # which map peaks
(0, 1)

execute_peaks

execute_peaks(
    x: NDArray[complex64],
) -> tuple[
    NDArray[np.float32],
    NDArray[np.uint32],
    NDArray[np.uint32],
    NDArray[np.float32],
]

One record per map of a batch: its peak power, where the peak is, and its mean power.

Python: x is the H frames back to back, a flat array of H * ny * nx samples (H >= 1).

Entry h of each output describes map h: out its largest |map|^2, out1 and out2 the row and column of that cell (the first in row-major order on a tie), and out3 the mean |map|^2 over its n_out cells. A map is ny_out x nx_out; with col_out it is ny x 1, and the column reported is col_out. The peak-to-mean of each map is out over out3, entry by entry: a statistic of each map, which the same statistic of a pointwise maximum over maps does not give.

Parameters:

Name Type Description Default
x NDArray[complex64]

H frames back to back (CF32).

required

Returns:

Type Description
tuple[NDArray[float32], NDArray[uint32], NDArray[uint32], NDArray[float32]]

Python: the tuple (peak, row, col, mean), each (H,): float32 peak power, uint32 row and column, float32 mean power, always new arrays (this call takes no out=); a refused batch is a ValueError. C: H, or SIZE_MAX with nothing written.

Raises:

Type Description
ValueError

If the C call returns SIZE_MAX, its refusal value, in place of a count. The exception message is execute_peaks: the input must be a whole, nonzero number of frames, and out= must hold one record per frame.

Examples:

>>> import numpy as np
>>> from doppler.spectral import DelayDoppler
>>> rng = np.random.default_rng(0)
>>> ny, nx = 8, 32
>>> code = np.exp(2j * np.pi * rng.random(nx)).astype(np.complex64)
>>> dd = DelayDoppler(code, ny=ny)
>>> frames = np.concatenate([np.tile(np.roll(code, d), ny)
...                          for d in (3, 11)])   # delays 3 and 11
>>> peak, row, col, mean = dd.execute_peaks(frames)
>>> row.tolist(), col.tolist()     # zero Doppler is row ny / 2
([4, 4], [3, 11])
>>> bool(np.all(peak / mean > 100))   # each map's own peak-to-mean
True

bank_power_max

bank_power_max(
    x: NDArray[complex64],
    L: int,
    g_min: int,
    g_max: int,
    rates: NDArray[complex64],
) -> tuple[NDArray[np.float32], NDArray[np.uint32]]

The strongest power at every cell over a bank, and which hypothesis it came from.

dp_correlator_bank_fold() with the POWER_MAX reducer: at each cell, out holds the largest |map_h|^2 over every hypothesis h, and out1 that h, (g - g_min) * R + r, the lowest h on a tie. Every argument and refusal is dp_correlator_bank_fold()'s; in addition max_out below n_out is refused with nothing written.

As dp_correlator_bank_fold().

Parameters:

Name Type Description Default
x NDArray[complex64]

Input.

required
L int

Input.

required
g_min int

Input.

required
g_max int

Input.

required
rates NDArray[complex64]

Input.

required

Returns:

Type Description
tuple[NDArray[float32], NDArray[uint32]]

Python: the tuple (power, index), each (n_out,): float32 powers and uint32 hypothesis indices, always new arrays (this call takes no out=); a refusal is a ValueError. C: n_out, or SIZE_MAX with nothing written.

Raises:

Type Description
ValueError

If the C call returns SIZE_MAX, its refusal value, in place of a count. The exception message is bank_power_max: needs one frame, L >= 1, g_min <= g_max, rates a whole number of ny-long vectors, and out= holding one map.

Examples:

>>> import numpy as np
>>> from doppler.spectral import DelayDoppler
>>> rng = np.random.default_rng(0)
>>> ny, nx, d = 8, 32, 5                  # code delayed by 5 samples
>>> code = rng.choice([-1.0, 1.0], nx).astype(np.complex64)
>>> m = np.arange(ny * nx)
>>> x = (np.tile(np.roll(code, d), ny)    # on a carrier 1.5 bins off
...      * np.exp(2j * np.pi * 1.5 / nx * m)).astype(np.complex64)
>>> dd = DelayDoppler(code, ny=ny)
>>> no_rates = np.empty(0, np.complex64)
>>> pw, h = dd.bank_power_max(x, 2, -4, 4, no_rates)  # g / (2 nx), -4..4
>>> cell = int(np.argmax(pw))
>>> cell % nx, int(h[cell]) - 4           # (delay, g = h + g_min)
(5, 3)

bank_detect

bank_detect(
    x: NDArray[complex64],
    L_d: int,
    L_r: int,
    g_min: int,
    g_max: int,
    t_candidate: float,
    t_confirm: float,
    rates: NDArray[complex64],
) -> tuple[
    NDArray[np.float32],
    NDArray[np.int32],
    NDArray[np.uint32],
    NDArray[np.uint32],
    NDArray[np.uint32],
]

Detect on a coarse carrier grid, refine each candidate at its own cell on a fine one, and keep what confirms.

The bank searches on L_d and resolves on L_r (docs/design/corr2d-hypotheses.md section 6):

  1. Detect. dp_correlator_bank_power_max() over carriers g_min .. g_max on the 1 / (L_d nx) grid, with every rate.
  2. Candidates. Every cell whose power exceeds t_candidate and is at least each of its 8 neighbors, both axes circular (a delay lag and a Doppler bin both wrap).
  3. Refine. For a candidate found by coarse carrier g_d and rate r, the map value at its cell under every fine carrier g_r from m g_d - (m - 1) to m g_d + (m - 1), m = L_r / L_d: the carriers between its coarse neighbors, on the 1 / (L_r nx) grid. On the fast path and DelayDoppler with a native delay axis each is a time-domain correlation at the cell's lag and a slow-time DFT at its Doppler row, in double, with no FFT: 2 ny nx multiply-adds a carrier. Any other object makes that hypothesis's map and reads the cell. The best g_r wins, the lowest on a tie.
  4. Confirm. A candidate whose refined power exceeds t_confirm is a record.

Records come out in cell order (row-major), one entry of each output per record: out the refined power, out1 g_r on the fine grid, out2 the rate r, out3 the row and out4 the column, the cell as execute() lays out the map. Measured at 64 x 512 against an oracle that corrects the exact offset, L_d = 2 with L_r = 4 holds L = 4's sensitivity within 0.16 dB, for about half the bank (the design page). L_d = L_r is the one-stage bank. Thresholds are powers in the map's own units; the caller sets them for its noise, as for any detector.

As dp_correlator_bank_fold(); g_min and g_max are on the coarse grid.

Parameters:

Name Type Description Default
x NDArray[complex64]

Input.

required
L_d int

The search grid; at least 1.

required
L_r int

The resolution grid; a multiple of L_d.

required
g_min int

Input.

required
g_max int

Input.

required
t_candidate float

Candidate threshold, on the coarse power.

required
t_confirm float

Confirm threshold, on the refined power.

required
rates NDArray[complex64]

Input.

required

Returns:

Type Description
tuple[NDArray[float32], NDArray[int32], NDArray[uint32], NDArray[uint32], NDArray[uint32]]

Python: the tuple (power, g, rate, row, col), one entry per confirmed record (possibly none): float32 refined power, int32 refined carrier, uint32 rate, row and column, always new arrays (this call takes no out=); anything below is a ValueError. C: the record count (0 included), or SIZE_MAX with nothing written for anything dp_correlator_bank_fold() refuses, L_d == 0, an L_r that L_d does not divide, a refined carrier past int32, or max_out below n_out.

Raises:

Type Description
ValueError

If the C call returns SIZE_MAX, its refusal value, in place of a count. The exception message is bank_detect: needs one frame, L_d >= 1 dividing L_r, g_min <= g_max, rates a whole number of ny-long vectors, and out= holding one record per map cell.

Examples:

>>> import numpy as np
>>> from doppler.spectral import DelayDoppler
>>> rng = np.random.default_rng(0)
>>> ny, nx, d = 8, 32, 5
>>> code = rng.choice([-1.0, 1.0], nx).astype(np.complex64)
>>> m = np.arange(ny * nx)
>>> x = (np.tile(np.roll(code, d), ny)    # 1.25 bins: between L_d = 2
...      * np.exp(2j * np.pi * 1.25 / nx * m)).astype(np.complex64)
>>> dd = DelayDoppler(code, ny=ny)
>>> no_rates = np.empty(0, np.complex64)
>>> coarse, _ = dd.bank_power_max(x, 2, -4, 4, no_rates)
>>> t = 0.5 * coarse.max()
>>> power, g, rate, row, col = dd.bank_detect(x, 2, 4, -4, 4, t, t,
...                                           no_rates)
>>> len(power), int(g[0]), int(col[0])    # g on the 1/(4 nx) grid
(1, 5, 5)
>>> round(float(power[0] / coarse.max()), 3)  # the straddle recovered
1.233

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 the state that evolves as the object runs, including any value a setter changed after construction, so a blob taken now and restored later resumes from this point. Construction parameters are not restored: restore into an object built the same way. A core may check some of them and refuse a blob built differently.

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, including any value a setter changed after construction. Construction parameters are not restored: restore into an object built the same way. Length is validated against state_bytes() before the blob is handed to the C core, which may reject it too: a core may check some construction parameters and refuse a blob built differently.

Raises TypeError if blob is not bytes, ValueError if its length differs from state_bytes(), the exception listed under Raises if 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

Raises:

Type Description
MemoryError

If the C call returns DP_ERR_MEMORY. The exception message is set_state: the blob's carrier needs the delay-Doppler map rebuilt, and the rebuild could not allocate; the engine is unchanged.

ValueError

If the C core rejects the blob with any other non-zero status. The exception message is set_state rejected the blob.

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.

...

DelayDoppler

Allocate the delay-Doppler flavor: a code correlator that maps a raw stream of ny code periods onto (Doppler, delay).

Parameters:

Name Type Description Default
code NDArray[complex64]

The code replica, code_len samples (one period).

required
ny int

Periods per frame, the slow-time length; must be >= 1.

1
dwell int

Frames coherently accumulated per dump; must be >= 1.

1
nthreads int

Workers: 1 serial, N for N (the caller included), <= 0 every online core; capped at the most chunks a stage can use, max(ny, ceil(code_len / 16)).

1
ny_out int

Doppler bins in the map; 0 means ny. Must be >= ny.

0

Raises:

Type Description
ValueError

If construction fails. The exception message is DelayDoppler: invalid parameter (need a non-empty code, ny >= 1, dwell >= 1, and ny_out 0 or at least ny).

Examples:

>>> import numpy as np
>>> from doppler.spectral import DelayDoppler
>>> rng = np.random.default_rng(0)
>>> ny, nx, d, k = 16, 64, 5, 3       # delay 5, Doppler 3 bins
>>> code = np.exp(2j * np.pi * rng.random(nx)).astype(np.complex64)
>>> n = np.arange(ny * nx)
>>> f = k / (ny * nx)                 # cycles/sample
>>> x = (np.tile(np.roll(code, d), ny)
...      * np.exp(2j * np.pi * f * n)).astype(np.complex64)
>>> dd = DelayDoppler(code, ny=ny, dwell=2)
>>> out = np.empty(dd.execute_max_out(), np.complex64)
>>> for _ in range(2):                # one preallocated map per dump
...     m = dd.execute(x, out=out)
>>> m is not None and m.base is out
True
>>> row, col = np.unravel_index(np.argmax(np.abs(out)), (ny, nx))
>>> int(row) - ny // 2, int(col)      # (Doppler bins, delay)
(3, 5)
>>> dd.doppler_step == 1 / (ny * nx)
True

ny property

ny: int

Row count.

nx property

nx: int

Column count.

ny_out property

ny_out: int

Output rows (== ny 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).

doppler_step property

doppler_step: float

Doppler spacing between adjacent output rows, cycles/sample: 1 / (ny_out * nx). 0 for Corr2D.

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

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.

Layout. A frame is nynx complex samples in row-major order: row i is samples inx .. (i+1)*nx - 1. For Corr2D that is a (ny, nx) image; for DelayDoppler it is the raw stream, ny code periods of nx = code_len samples back to back. The map a dump produces is flat, n_out = ny_out * nx_out values (ny values with col_out), also row-major: reshape it to (ny_out, nx_out). On a DelayDoppler, axis 0 is Doppler (centered, zero Doppler on row ny_out / 2) and axis 1 is delay (the lag, in samples).

Ownership. In Python, a call that dumps returns a new array when no out= is given, and a view of out= when one is (a 1-D array holding at least n_out values; it is written on a dump only, and the next dump overwrites it). A call that does not dump returns None and touches neither.

Parameters:

Name Type Description Default
x NDArray[complex64]

Input frame, flat row-major CF32, length ny*nx.

required
out NDArray[complex64] | None

Output buffer for the correlation map (CF32, length n_out: ny_out*nx_out, or ny with col_out); written only on a dump call.

None

Returns:

Type Description
NDArray[complex64] | None

Python: the map on a dump, None otherwise; a frame that is not ny*nx samples is REFUSED with nothing read, written or counted, as a ValueError (#1925). C: n_out on a dump (or max_out if smaller), 0 otherwise, and SIZE_MAX for the refused frame. None when the C call writes no output.

Raises:

Type Description
ValueError

If the C call returns SIZE_MAX, its refusal value, in place of a count. The exception message is input length is not the frame length (n: Corr's n, or ny*nx); execute takes exactly one frame.

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 (dp_correlator_state_t::n_out -- ny_out*nx_out normally, or ny when a single dp_correlator_state_t::col_out was selected).

Returns:

Type Description
int

n_out, fixed at create().

execute_all

execute_all(
    x: NDArray[complex64],
    out: NDArray[complex64] | None = None,
) -> NDArray[np.complex64]

Every map of a batch, unreduced: row h of the (H, n_out) result is the map of frame h (in C, map h is at out + h * n_out).

Python: x is the H frames back to back, a flat array of H * ny * nx samples (H >= 1): frame h is the samples from h * ny * nx up to, not including, (h + 1) * ny * nx. With no out= each call returns a new array; with out= it returns a view of it, and the next call into the same out= overwrites that view. out= is a 2-D complex64 array of shape (rows, n_out) with at least H rows (a shorter one is refused); the result is its first H rows. The object's dwell is untouched.

Parameters:

Name Type Description Default
x NDArray[complex64]

H frames back to back (CF32).

required
out NDArray[complex64] | None

H * n_out values (CF32).

None

Returns:

Type Description
NDArray[complex64]

Python: the (H, n_out) array; a refused batch (empty, not a whole number of frames, or an out= with fewer than H rows) is a ValueError with nothing written. C: H * n_out, or SIZE_MAX for the refused batch.

Raises:

Type Description
ValueError

If the C call returns SIZE_MAX, its refusal value, in place of a count. The exception message is execute_all: the input must be a whole, nonzero number of frames, and out= must hold every map.

Examples:

>>> import numpy as np
>>> from doppler.spectral import DelayDoppler
>>> rng = np.random.default_rng(0)
>>> ny, nx = 8, 32
>>> code = np.exp(2j * np.pi * rng.random(nx)).astype(np.complex64)
>>> dd = DelayDoppler(code, ny=ny)
>>> frames = np.concatenate([np.tile(np.roll(code, d), ny)
...                          for d in (3, 11)])   # delays 3 and 11
>>> maps = dd.execute_all(frames)
>>> maps.shape
(2, 256)
>>> [int(np.argmax(np.abs(m))) % nx for m in maps]   # each map's delay
[3, 11]
>>> dd.count                    # the dwell never moved
0

execute_all_max_out

execute_all_max_out(n_in: int) -> int

Samples dp_correlator_execute_all() writes for n_in input samples: H * n_out, H = n_in / (ny * nx).

Parameters:

Name Type Description Default
n_in int

Input samples about to be passed.

required

Returns:

Type Description
int

The capacity execute_all needs for n_in samples: H * n_out values (C: SIZE_MAX if that overflows).

Examples:

>>> import numpy as np
>>> from doppler.spectral import DelayDoppler
>>> rng = np.random.default_rng(0)
>>> ny, nx = 8, 32
>>> code = np.exp(2j * np.pi * rng.random(nx)).astype(np.complex64)
>>> dd = DelayDoppler(code, ny=ny)
>>> frames = np.concatenate([np.tile(np.roll(code, d), ny)
...                          for d in (3, 11)])   # delays 3 and 11
>>> dd.execute_all_max_out(len(frames))   # 2 maps of 8 x 32
512

execute_coherent

execute_coherent(
    x: NDArray[complex64],
    out: NDArray[complex64] | None = None,
) -> NDArray[np.complex64]

The coherent sum of a batch's maps, inverted once.

Python: x is the H frames back to back, a flat array of H * ny * nx samples (H >= 1).

The frames' product spectra are summed and the sum is inverted, as a dwell does: the result is bit-identical to what a fresh object with dwell = H dumps for the same frames, and equal to the sum of the H maps up to rounding. The object's own dwell is untouched.

Parameters:

Name Type Description Default
x NDArray[complex64]

H frames back to back (CF32).

required
out NDArray[complex64] | None

n_out values (CF32).

None

Returns:

Type Description
NDArray[complex64]

Python: the one map, a new (n_out,) array; a refused batch is a ValueError with nothing written. C: n_out, or SIZE_MAX for the refused batch.

Raises:

Type Description
ValueError

If the C call returns SIZE_MAX, its refusal value, in place of a count. The exception message is execute_coherent: the input must be a whole, nonzero number of frames, and out= must hold one map.

Examples:

>>> import numpy as np
>>> from doppler.spectral import DelayDoppler
>>> rng = np.random.default_rng(0)
>>> ny, nx = 8, 32
>>> code = np.exp(2j * np.pi * rng.random(nx)).astype(np.complex64)
>>> dd = DelayDoppler(code, ny=ny)
>>> frames = np.concatenate([np.tile(np.roll(code, d), ny)
...                          for d in (3, 11)])   # delays 3 and 11
>>> x = np.tile(code, ny)
>>> total = dd.execute_coherent(np.concatenate([x, -x]))
>>> bool(np.abs(total).max() < 1e-3)          # x and -x cancel
True

execute_coherent_max_out

execute_coherent_max_out() -> int

Samples dp_correlator_execute_coherent() writes: n_out.

Returns:

Type Description
int

n_out, fixed at create().

Examples:

>>> import numpy as np
>>> from doppler.spectral import DelayDoppler
>>> rng = np.random.default_rng(0)
>>> ny, nx = 8, 32
>>> code = np.exp(2j * np.pi * rng.random(nx)).astype(np.complex64)
>>> dd = DelayDoppler(code, ny=ny)
>>> frames = np.concatenate([np.tile(np.roll(code, d), ny)
...                          for d in (3, 11)])   # delays 3 and 11
>>> dd.execute_coherent_max_out()
256

execute_power_sum

execute_power_sum(
    x: NDArray[complex64],
    out: NDArray[float32] | None = None,
) -> NDArray[np.float32]

The power of a batch's maps, summed cell by cell: each output cell is the sum over h of that cell's |map_h|^2, summed in h order in float.

Python: x is the H frames back to back, a flat array of H * ny * nx samples (H >= 1).

Unlike the coherent sum, phase does not cancel: frames x and -x sum to twice one map's power, where their coherent sum is zero.

Parameters:

Name Type Description Default
x NDArray[complex64]

H frames back to back (CF32).

required
out NDArray[float32] | None

n_out powers (float).

None

Returns:

Type Description
NDArray[float32]

Python: the (n_out,) float32 powers, a new array; a refused batch is a ValueError with nothing written. C: n_out, or SIZE_MAX for the refused batch.

Raises:

Type Description
ValueError

If the C call returns SIZE_MAX, its refusal value, in place of a count. The exception message is execute_power_sum: the input must be a whole, nonzero number of frames, and out= must hold one map.

Examples:

>>> import numpy as np
>>> from doppler.spectral import DelayDoppler
>>> rng = np.random.default_rng(0)
>>> ny, nx = 8, 32
>>> code = np.exp(2j * np.pi * rng.random(nx)).astype(np.complex64)
>>> dd = DelayDoppler(code, ny=ny)
>>> frames = np.concatenate([np.tile(np.roll(code, d), ny)
...                          for d in (3, 11)])   # delays 3 and 11
>>> x = np.tile(code, ny)
>>> power = dd.execute_power_sum(np.concatenate([x, -x]))
>>> one = np.abs(dd.execute_all(x)[0]) ** 2
>>> bool(np.allclose(power, 2 * one))        # x and -x do not cancel
True

execute_power_sum_max_out

execute_power_sum_max_out() -> int

Powers dp_correlator_execute_power_sum() writes: n_out.

Returns:

Type Description
int

n_out, fixed at create().

Examples:

>>> import numpy as np
>>> from doppler.spectral import DelayDoppler
>>> rng = np.random.default_rng(0)
>>> ny, nx = 8, 32
>>> code = np.exp(2j * np.pi * rng.random(nx)).astype(np.complex64)
>>> dd = DelayDoppler(code, ny=ny)
>>> frames = np.concatenate([np.tile(np.roll(code, d), ny)
...                          for d in (3, 11)])   # delays 3 and 11
>>> dd.execute_power_sum_max_out()
256

execute_power_max

execute_power_max(
    x: NDArray[complex64],
) -> tuple[NDArray[np.float32], NDArray[np.uint32]]

The largest power over a batch's maps, cell by cell, and the map it came from.

Python: x is the H frames back to back, a flat array of H * ny * nx samples (H >= 1).

Each cell of out is the largest |map_h|^2 that cell takes over the maps, and the same cell of out1 is the h it came from. A tie goes to the lowest h: a later map replaces the running maximum only when it is strictly greater.

Parameters:

Name Type Description Default
x NDArray[complex64]

H frames back to back (CF32); H at most 2^32.

required

Returns:

Type Description
tuple[NDArray[float32], NDArray[uint32]]

Python: the tuple (power, index), each (n_out,): float32 powers and uint32 map indices, always new arrays (this call takes no out=); a refused batch, or H above 2**32, is a ValueError. C: n_out, or SIZE_MAX with nothing written.

Raises:

Type Description
ValueError

If the C call returns SIZE_MAX, its refusal value, in place of a count. The exception message is execute_power_max: the input must be a whole, nonzero number of frames (at most 2**32), and out= must hold one map.

Examples:

>>> import numpy as np
>>> from doppler.spectral import DelayDoppler
>>> rng = np.random.default_rng(0)
>>> ny, nx = 8, 32
>>> code = np.exp(2j * np.pi * rng.random(nx)).astype(np.complex64)
>>> dd = DelayDoppler(code, ny=ny)
>>> frames = np.concatenate([np.tile(np.roll(code, d), ny)
...                          for d in (3, 11)])   # delays 3 and 11
>>> power, index = dd.execute_power_max(frames)
>>> int(index[4 * nx + 3]), int(index[4 * nx + 11])  # which map peaks
(0, 1)

execute_peaks

execute_peaks(
    x: NDArray[complex64],
) -> tuple[
    NDArray[np.float32],
    NDArray[np.uint32],
    NDArray[np.uint32],
    NDArray[np.float32],
]

One record per map of a batch: its peak power, where the peak is, and its mean power.

Python: x is the H frames back to back, a flat array of H * ny * nx samples (H >= 1).

Entry h of each output describes map h: out its largest |map|^2, out1 and out2 the row and column of that cell (the first in row-major order on a tie), and out3 the mean |map|^2 over its n_out cells. A map is ny_out x nx_out; with col_out it is ny x 1, and the column reported is col_out. The peak-to-mean of each map is out over out3, entry by entry: a statistic of each map, which the same statistic of a pointwise maximum over maps does not give.

Parameters:

Name Type Description Default
x NDArray[complex64]

H frames back to back (CF32).

required

Returns:

Type Description
tuple[NDArray[float32], NDArray[uint32], NDArray[uint32], NDArray[float32]]

Python: the tuple (peak, row, col, mean), each (H,): float32 peak power, uint32 row and column, float32 mean power, always new arrays (this call takes no out=); a refused batch is a ValueError. C: H, or SIZE_MAX with nothing written.

Raises:

Type Description
ValueError

If the C call returns SIZE_MAX, its refusal value, in place of a count. The exception message is execute_peaks: the input must be a whole, nonzero number of frames, and out= must hold one record per frame.

Examples:

>>> import numpy as np
>>> from doppler.spectral import DelayDoppler
>>> rng = np.random.default_rng(0)
>>> ny, nx = 8, 32
>>> code = np.exp(2j * np.pi * rng.random(nx)).astype(np.complex64)
>>> dd = DelayDoppler(code, ny=ny)
>>> frames = np.concatenate([np.tile(np.roll(code, d), ny)
...                          for d in (3, 11)])   # delays 3 and 11
>>> peak, row, col, mean = dd.execute_peaks(frames)
>>> row.tolist(), col.tolist()     # zero Doppler is row ny / 2
([4, 4], [3, 11])
>>> bool(np.all(peak / mean > 100))   # each map's own peak-to-mean
True

bank_power_max

bank_power_max(
    x: NDArray[complex64],
    L: int,
    g_min: int,
    g_max: int,
    rates: NDArray[complex64],
) -> tuple[NDArray[np.float32], NDArray[np.uint32]]

The strongest power at every cell over a bank, and which hypothesis it came from.

dp_correlator_bank_fold() with the POWER_MAX reducer: at each cell, out holds the largest |map_h|^2 over every hypothesis h, and out1 that h, (g - g_min) * R + r, the lowest h on a tie. Every argument and refusal is dp_correlator_bank_fold()'s; in addition max_out below n_out is refused with nothing written.

As dp_correlator_bank_fold().

Parameters:

Name Type Description Default
x NDArray[complex64]

Input.

required
L int

Input.

required
g_min int

Input.

required
g_max int

Input.

required
rates NDArray[complex64]

Input.

required

Returns:

Type Description
tuple[NDArray[float32], NDArray[uint32]]

Python: the tuple (power, index), each (n_out,): float32 powers and uint32 hypothesis indices, always new arrays (this call takes no out=); a refusal is a ValueError. C: n_out, or SIZE_MAX with nothing written.

Raises:

Type Description
ValueError

If the C call returns SIZE_MAX, its refusal value, in place of a count. The exception message is bank_power_max: needs one frame, L >= 1, g_min <= g_max, rates a whole number of ny-long vectors, and out= holding one map.

Examples:

>>> import numpy as np
>>> from doppler.spectral import DelayDoppler
>>> rng = np.random.default_rng(0)
>>> ny, nx, d = 8, 32, 5                  # code delayed by 5 samples
>>> code = rng.choice([-1.0, 1.0], nx).astype(np.complex64)
>>> m = np.arange(ny * nx)
>>> x = (np.tile(np.roll(code, d), ny)    # on a carrier 1.5 bins off
...      * np.exp(2j * np.pi * 1.5 / nx * m)).astype(np.complex64)
>>> dd = DelayDoppler(code, ny=ny)
>>> no_rates = np.empty(0, np.complex64)
>>> pw, h = dd.bank_power_max(x, 2, -4, 4, no_rates)  # g / (2 nx), -4..4
>>> cell = int(np.argmax(pw))
>>> cell % nx, int(h[cell]) - 4           # (delay, g = h + g_min)
(5, 3)

bank_detect

bank_detect(
    x: NDArray[complex64],
    L_d: int,
    L_r: int,
    g_min: int,
    g_max: int,
    t_candidate: float,
    t_confirm: float,
    rates: NDArray[complex64],
) -> tuple[
    NDArray[np.float32],
    NDArray[np.int32],
    NDArray[np.uint32],
    NDArray[np.uint32],
    NDArray[np.uint32],
]

Detect on a coarse carrier grid, refine each candidate at its own cell on a fine one, and keep what confirms.

The bank searches on L_d and resolves on L_r (docs/design/corr2d-hypotheses.md section 6):

  1. Detect. dp_correlator_bank_power_max() over carriers g_min .. g_max on the 1 / (L_d nx) grid, with every rate.
  2. Candidates. Every cell whose power exceeds t_candidate and is at least each of its 8 neighbors, both axes circular (a delay lag and a Doppler bin both wrap).
  3. Refine. For a candidate found by coarse carrier g_d and rate r, the map value at its cell under every fine carrier g_r from m g_d - (m - 1) to m g_d + (m - 1), m = L_r / L_d: the carriers between its coarse neighbors, on the 1 / (L_r nx) grid. On the fast path and DelayDoppler with a native delay axis each is a time-domain correlation at the cell's lag and a slow-time DFT at its Doppler row, in double, with no FFT: 2 ny nx multiply-adds a carrier. Any other object makes that hypothesis's map and reads the cell. The best g_r wins, the lowest on a tie.
  4. Confirm. A candidate whose refined power exceeds t_confirm is a record.

Records come out in cell order (row-major), one entry of each output per record: out the refined power, out1 g_r on the fine grid, out2 the rate r, out3 the row and out4 the column, the cell as execute() lays out the map. Measured at 64 x 512 against an oracle that corrects the exact offset, L_d = 2 with L_r = 4 holds L = 4's sensitivity within 0.16 dB, for about half the bank (the design page). L_d = L_r is the one-stage bank. Thresholds are powers in the map's own units; the caller sets them for its noise, as for any detector.

As dp_correlator_bank_fold(); g_min and g_max are on the coarse grid.

Parameters:

Name Type Description Default
x NDArray[complex64]

Input.

required
L_d int

The search grid; at least 1.

required
L_r int

The resolution grid; a multiple of L_d.

required
g_min int

Input.

required
g_max int

Input.

required
t_candidate float

Candidate threshold, on the coarse power.

required
t_confirm float

Confirm threshold, on the refined power.

required
rates NDArray[complex64]

Input.

required

Returns:

Type Description
tuple[NDArray[float32], NDArray[int32], NDArray[uint32], NDArray[uint32], NDArray[uint32]]

Python: the tuple (power, g, rate, row, col), one entry per confirmed record (possibly none): float32 refined power, int32 refined carrier, uint32 rate, row and column, always new arrays (this call takes no out=); anything below is a ValueError. C: the record count (0 included), or SIZE_MAX with nothing written for anything dp_correlator_bank_fold() refuses, L_d == 0, an L_r that L_d does not divide, a refined carrier past int32, or max_out below n_out.

Raises:

Type Description
ValueError

If the C call returns SIZE_MAX, its refusal value, in place of a count. The exception message is bank_detect: needs one frame, L_d >= 1 dividing L_r, g_min <= g_max, rates a whole number of ny-long vectors, and out= holding one record per map cell.

Examples:

>>> import numpy as np
>>> from doppler.spectral import DelayDoppler
>>> rng = np.random.default_rng(0)
>>> ny, nx, d = 8, 32, 5
>>> code = rng.choice([-1.0, 1.0], nx).astype(np.complex64)
>>> m = np.arange(ny * nx)
>>> x = (np.tile(np.roll(code, d), ny)    # 1.25 bins: between L_d = 2
...      * np.exp(2j * np.pi * 1.25 / nx * m)).astype(np.complex64)
>>> dd = DelayDoppler(code, ny=ny)
>>> no_rates = np.empty(0, np.complex64)
>>> coarse, _ = dd.bank_power_max(x, 2, -4, 4, no_rates)
>>> t = 0.5 * coarse.max()
>>> power, g, rate, row, col = dd.bank_detect(x, 2, 4, -4, 4, t, t,
...                                           no_rates)
>>> len(power), int(g[0]), int(col[0])    # g on the 1/(4 nx) grid
(1, 5, 5)
>>> round(float(power[0] / coarse.max()), 3)  # the straddle recovered
1.233

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 DelayDoppler 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 the state that evolves as the object runs, including any value a setter changed after construction, so a blob taken now and restored later resumes from this point. Construction parameters are not restored: restore into an object built the same way. A core may check some of them and refuse a blob built differently.

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 DelayDoppler 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, including any value a setter changed after construction. Construction parameters are not restored: restore into an object built the same way. Length is validated against state_bytes() before the blob is handed to the C core, which may reject it too: a core may check some construction parameters and refuse a blob built differently.

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

Parameters:

Name Type Description Default
blob bytes

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

required

Raises:

Type Description
MemoryError

If the C call returns DP_ERR_MEMORY. The exception message is set_state: the blob's carrier needs the delay-Doppler map rebuilt, and the rebuild could not allocate; the engine is unchanged.

ValueError

If the C core rejects the blob with any other non-zero status. The exception message is set_state rejected the blob.

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

Enter a context manager, returning this object.

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

Returns:

Type Description
DelayDoppler

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

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_pow_two(max(n, 512)) complex samples.

One push() has room for 1024 detections, so a push that completes at most 1024 frames, counting the carry, loses nothing. One that fills the room loses the rest of its input, every later frame whether or not it would have made a detection, unless the rest is shorter than a frame; the next push stays on the frame grid only when what was lost is a whole number of frames (#1992).

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)      # peak 12x the noise (linear)


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():                     # at most 1024 frames each
    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, with the same room: 1024 detections per push(), the same bound of 1024 frames counting the carry, and the same loss past it.

CorrDetector

Allocate a 1-D streaming detector: the 2-D detector at one row (docs/design/correlator.md section 2.1), so its hits are the 2-D one's at ny = 1, each with row 0 and the lag in col.

Parameters:

Name Type Description Default
ref NDArray[complex64]

Reference signal, CF32 ndarray of length ref_len.

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). A value at or beyond the window clamps to n - 1, so the default sentinel selects the full window.

...
noise_mode Literal['mean', 'median', 'min', 'max']

How the noise bins are aggregated into the noise floor the peak-to-noise statistic divides by. One of "mean", "median", "min", "max".

"mean"
threshold float

Test-stat gate; 0.0 = always emit.

0.0
nthreads int

Accepted for API compatibility; ignored.

1

Raises:

Type Description
ValueError

If construction fails. The exception message is CorrDetector: invalid parameter (need a non-empty ref and dwell >= 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: a detection statistic is dumped every dwell frames.

count property

count: int

Frames accumulated toward the next dump (0 ... dwell-1).

ring_cap property

ring_cap: int

Ring buffer capacity, complex samples.

noise_lo property

noise_lo: int

Noise bin range, first index (inclusive).

noise_hi property

noise_hi: int

Noise bin range, last index (inclusive).

threshold property

threshold: float

Test-stat gate: 0 fires on every dump, > 0 on test_stat above it.

last_corr property

last_corr: NDArray[complex64] | None

The correlation vector from the most recent push() that produced a result (None before that, and after reset() or set_state(): the vector is not part of the serialized state). 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

Restart detection: zero the dwell and drop the carry.

Zeroes the accumulator and the integration counter, drops the carry (the partial frame held between pushes) and clears the last dump's fields, so the detector afterwards matches a fresh one built with the same arguments. The reference, its spectrum, the noise range and the threshold are kept. last_corr reads None until the next dump.

Examples:

>>> from doppler.spectral import CorrDetector
>>> import numpy as np
>>> ref = np.zeros(4, dtype=np.complex64); ref[0] = 1.0
>>> det = CorrDetector(ref=ref, noise_lo=1)
>>> det.push(np.ones(2, dtype=np.complex64))
[]
>>> det.reset()
>>> len(det.push(np.ones(4, dtype=np.complex64)))
1
>>> det.count, det.last_corr is None
(0, False)
>>> det.reset()
>>> det.count, det.last_corr is None
(0, True)

push

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

Stream an arbitrary-length CF32 chunk through the detector.

Takes the input in order, runs each complete frame through the correlator, and on every dump computes the test statistic peak_mag / noise_est. Each dump that passes the threshold is one hit, a (lag, peak_mag, noise_est, test_stat) tuple, the lag being the hit's column. A chunk that ends mid-frame keeps the rest as the carry, so the hits do not depend on how the stream is split into calls.

push() has room for 1024 hits a call, so a push that completes at most 1024 frames, counting the carry, loses nothing. One that fills the room loses the rest of its input, every later frame whether or not it would have made a hit, unless the rest is shorter than a frame: that is kept as the carry. #1992 and just-buildit/just-makeit#2184 track sizing the list to the call.

Parameters:

Name Type Description Default
x NDArray[complex64]

The next chunk of the stream, any length.

required

Returns:

Type Description
list[tuple[int, float, float, float]]

The hits, in order; empty when no dump passed the gate.

Examples:

>>> from doppler.spectral import CorrDetector
>>> import numpy as np
>>> ref = np.zeros(4, dtype=np.complex64); ref[0] = 1.0
>>> det = CorrDetector(ref=ref, dwell=1, noise_lo=1, noise_hi=3)
>>> hits = det.push(np.ones(4, dtype=np.complex64))
>>> lag, peak, noise, stat = hits[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 the state that evolves as the object runs, including any value a setter changed after construction, so a blob taken now and restored later resumes from this point. Construction parameters are not restored: restore into an object built the same way. A core may check some of them and refuse a blob built differently.

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, including any value a setter changed after construction. Construction parameters are not restored: restore into an object built the same way. Length is validated against state_bytes() before the blob is handed to the C core, which may reject it too: a core may check some construction parameters and refuse a blob built differently.

Raises TypeError if blob is not bytes, ValueError if its length differs from state_bytes(), the exception listed under Raises if 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

Raises:

Type Description
MemoryError

If the C call returns DP_ERR_MEMORY. The exception message is set_state: the blob's carrier needs the delay-Doppler map rebuilt, and the rebuild could not allocate; the engine is unchanged.

ValueError

If the C core rejects the blob with any other non-zero status. The exception message is set_state rejected the blob.

destroy

destroy() -> None

Close the object; the memory goes with its last user.

Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this, or use the object as a context manager, to end its use at a definite point.

Views this object handed out keep the memory they point into, as does a call still running on another thread: it is freed when the last of them, and this object, are gone, not here (gh-2187). Idempotent: calling it again 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 detector: the 2-D correlator with the stream and detection layers.

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, < nynx). A value at or beyond the window clamps to nynx - 1, so the default sentinel selects the full window.

...
noise_mode Literal['mean', 'median', 'min', 'max']

How the noise bins are aggregated into the noise floor the peak-to-noise statistic divides by. One of "mean", "median", "min", "max".

"mean"
threshold float

Test-stat gate; 0.0 = always emit.

0.0
nthreads int

Workers for the single-row-reference fast path (1 serial, <= 0 every online core); the general 2-D path ignores it.

1

Raises:

Type Description
ValueError

If construction fails. The exception message is CorrDetector2D: invalid parameter (need a non-empty 2-D ref and dwell >= 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

Row count.

nx property

nx: int

Column count.

n property

n: int

ny * nx: the frame length push cuts the stream into.

dwell property

dwell: int

Integration depth: a detection statistic is dumped every dwell frames.

count property

count: int

Frames accumulated toward the next dump (0 ... dwell-1).

ring_cap property

ring_cap: int

Ring buffer capacity, complex samples.

noise_lo property

noise_lo: int

Noise bin range, first flat index (inclusive).

noise_hi property

noise_hi: int

Noise bin range, last flat index (inclusive).

threshold property

threshold: float

Test-stat gate: 0 fires on every dump, > 0 on test_stat above it.

last_corr property

last_corr: NDArray[complex64] | None

The correlation vector from the most recent push() that produced a result (None before that, and after reset() or set_state(): the vector is not part of the serialized state). 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

Restart detection: zero the dwell and drop the carry.

Zeroes the accumulator and the integration counter, drops the carry (the partial frame held between pushes) and clears the last dump's fields, so the detector afterwards matches a fresh one built with the same arguments. The reference, its spectrum, the noise range and the threshold are kept. last_corr reads None until the next dump.

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, noise_lo=1)
>>> det.push(np.ones(8, dtype=np.complex64))
[]
>>> det.reset()
>>> len(det.push(np.ones((4, 4), dtype=np.complex64)))
1
>>> det.count, det.last_corr is None
(0, False)
>>> det.reset()
>>> det.count, det.last_corr is None
(0, True)

push

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

Stream an arbitrary-length CF32 chunk through the detector.

Takes the input in order, runs each complete frame through the correlator, and on every dump computes the test statistic peak_mag / noise_est. Each dump that passes the threshold is one hit, a (row, col, peak_mag, noise_est, test_stat) tuple. A chunk that ends mid-frame keeps the rest as the carry, so the hits do not depend on how the stream is split into calls.

push() has room for 1024 hits a call, so a push that completes at most 1024 frames, counting the carry, loses nothing. One that fills the room loses the rest of its input, every later frame whether or not it would have made a hit, unless the rest is shorter than a frame: that is kept as the carry. #1992 and just-buildit/just-makeit#2184 track sizing the list to the call.

Parameters:

Name Type Description Default
x NDArray[complex64]

The next chunk of the stream, any length.

required

Returns:

Type Description
list[tuple[int, int, float, float, float]]

The hits, in order; empty when no dump passed the gate.

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)
>>> hits = det.push(np.ones((4, 4), dtype=np.complex64))
>>> row, col, peak, noise, stat = hits[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 the state that evolves as the object runs, including any value a setter changed after construction, so a blob taken now and restored later resumes from this point. Construction parameters are not restored: restore into an object built the same way. A core may check some of them and refuse a blob built differently.

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, including any value a setter changed after construction. Construction parameters are not restored: restore into an object built the same way. Length is validated against state_bytes() before the blob is handed to the C core, which may reject it too: a core may check some construction parameters and refuse a blob built differently.

Raises TypeError if blob is not bytes, ValueError if its length differs from state_bytes(), the exception listed under Raises if 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

Raises:

Type Description
MemoryError

If the C call returns DP_ERR_MEMORY. The exception message is set_state: the blob's carrier needs the delay-Doppler map rebuilt, and the rebuild could not allocate; the engine is unchanged.

ValueError

If the C core rejects the blob with any other non-zero status. The exception message is set_state rejected the blob.

destroy

destroy() -> None

Close the object; the memory goes with its last user.

Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this, or use the object as a context manager, to end its use at a definite point.

Views this object handed out keep the memory they point into, as does a call still running on another thread: it is freed when the last of them, and this object, are gone, not here (gh-2187). Idempotent: calling it again 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.

...

Streaming spectrograms

Spectrogram turns a stream into rows: one power spectrum every hop samples, each over the nfft samples that start there, however the stream is chopped into push() calls. The guide Streaming Spectrograms shows the call, and the gallery page draws the waterfall.

Spectrogram

A streaming short-time power spectrum: a stream in chunks of any size, one row of nfft bins per hop samples, each row the PSD of its frame.

import numpy as np from doppler.spectral import Spectrogram sp = Spectrogram(nfft=8, hop=8, window="rect") rows = sp.push(np.ones(16, np.complex64)) rows.shape (2, 8)

Parameters:

Name Type Description Default
nfft int

Samples per frame and bins per row, which must be the same number: an nfft that would be zero-padded to a longer transform (anything but a power of two >= 2) is refused rather than given rows wider than its frames.

256
hop int

Samples between row starts, 1 to nfft. 0 means nfft, which tiles the stream; the default. Less than nfft overlaps the frames. A hop above nfft would leave samples no row covers, so it is refused.

0
window Literal['hann', 'kaiser', 'blackman-harris', 'rect']

The window every frame is shaped by, as PSD takes them. Applied in its periodic form, as PSD applies it. A window whose taps do not sum to a positive finite number (a NaN or overflowing Kaiser beta) is refused. One of "hann", "kaiser", "blackman-harris", "rect".

  • "hann" — Hann: the default.
  • "kaiser" — Kaiser, shaped by beta.
  • "blackman-harris" — 4-term Blackman-Harris.
  • "rect" — Rectangular: no taper, best resolution, worst leakage.
"hann"
beta float

Kaiser beta (ignored for the other windows).

0.0
mode Literal['power', 'db']

The row units: power is linear against full scale (the default), db is dBFS. One of "power", "db".

"power"

Raises:

Type Description
ValueError

If construction fails. The exception message is Spectrogram: invalid parameter (need hop in 0..nfft, 0 meaning nfft; nfft within the largest transform, a window the PSD accepts, and mode power or db).

Examples:

Create with defaults:

>>> from doppler.spectral import Spectrogram
>>> obj = Spectrogram()

nfft property

nfft: int

Samples per frame, and bins per row: one number, because a frame the PSD would zero-pad to a longer transform is refused at construction.

hop property

hop: int

Samples between row starts, 1 to nfft: nfft tiles the stream, anything less overlaps the frames. A hop of 0 at construction means nfft, and reads back as nfft.

window property

window: Literal["hann", "kaiser", "blackman-harris", "rect"]

The window every frame is shaped by: "hann", "kaiser", "blackman-harris" or "rect".

beta property

beta: float

The Kaiser beta the Spectrogram was made with, as the float32 it keeps: 0.1 reads back 0.10000000149011612. It shapes the Kaiser window only; under the others it changes no row.

mode property

mode: Literal['power', 'db']

The row units: "power" (linear against full scale, the default) or "db" (dBFS).

consumed property

consumed: int

Input samples the last push took. From Python it is always the chunk's length: the binding sizes its output for every row the chunk completes, so no input is held back.

pending property

pending: int

Samples pushed that no row has covered yet: what flush() would turn into a row. 0 means the stream is complete as it stands.

push

push(
    x: NDArray[complex64],
    out: NDArray[float32] | None = None,
) -> NDArray[np.float32] | None

Push a chunk of samples, and return the rows it completes.

The rows come back as a (rows, nfft) float32 matrix, or None when the chunk completes none. A partial frame is carried to the next push. Without out=, consumed equals the input length: the binding allocates room for every row the chunk can complete, so no input is held back.

An object is not thread-safe: one Spectrogram must not be pushed from two threads at once, because the kernel uses the object's scratch. push releases the GIL, so other Python threads run meanwhile, but not on this object.

An out array may be any (k, nfft) float32 matrix, k = 0 included: push writes the whole rows that fit and stops before a row it has no room for, so consumed can then be less than len(x). Offer x[consumed:] again after draining the rows; nothing is lost and the rows resume bit-exact.

import numpy as np from doppler.spectral import Spectrogram sp = Spectrogram(nfft=8, hop=8, window="rect") rows = sp.push(np.ones(20, np.complex64)) rows.shape (2, 8) sp.consumed, sp.pending (20, 4)

Takes the input in order and produces each row as soon as a frame of nfft samples is complete, at every hop. A partial frame is carried to the next call, so the rows do not depend on how the input was split into calls. Input that completes no row is always taken whole.

Only whole rows are written. A C caller bounds the write with max_out and takes the rows that fit; the consumed count reports the input those rows used, and the caller offers the rest again. The Python face has no short buffer: its binding sizes the output for the whole chunk (see the Python method doc).

Parameters:

Name Type Description Default
x NDArray[complex64]

Complex baseband samples (cf32).

required
out NDArray[float32] | None

Rows, row-major, nfft floats each.

None

Returns:

Type Description
NDArray[float32] | None

Floats written, a multiple of nfft. From Python, the completed rows as a (rows, nfft) matrix, or None when none is completed. None when the C call writes no output.

push_max_out

push_max_out(n_in: int) -> int

Floats one push of n_in samples writes when out has room: the rows_for(n_in) count times nfft.

The capacity that makes a push take ALL of its input. Saturates at SIZE_MAX rather than wrapping.

Parameters:

Name Type Description Default
n_in int

Samples about to be pushed.

required

Returns:

Type Description
int

Floats: rows_for(n_in) * nfft, or SIZE_MAX on overflow.

flush

flush(
    out: NDArray[float32] | None = None,
) -> NDArray[np.float32] | None

End the stream: return the row a pending partial frame is owed.

The row comes back as a (1, nfft) float32 matrix, the shape push returns, or None when no row was owed. Either way the stream restarts, as after reset, so a second flush returns None.

An out array, if given, must be a float32 array of shape (1, nfft); any other shape raises ValueError, and the owed frame stays owed.

The example uses hop 4, so the owed row starts on the hop grid, at sample 4:

import numpy as np from doppler.spectral import Spectrogram sp = Spectrogram(nfft=8, hop=4, window="rect") _ = sp.push(np.ones(11, np.complex64)) sp.flush().shape (1, 8) sp.flush() is None True

The row sits on the hop grid: it starts at the next row start k*hop, never at the first sample no row covered, so it is the row a one-shot push of the input zero-padded to that row's end would have written. It is written if and only if the stream holds a sample no earlier row covered. Either way the spectrogram then restarts at sample 0, as after reset, so a second flush writes nothing.

Parameters:

Name Type Description Default
out NDArray[float32] | None

Optional pre-allocated output buffer: a writable, C-contiguous 2-D array of shape (rows, cols), where cols is this method's fixed row width. When given, the result is written into it and the returned array is a (rows, cols) view of exactly the rows produced; when omitted, a fresh array is allocated.

None

Returns:

Type Description
NDArray[float32] | None

Floats written: nfft, or 0 if no row was owed; -1 if the row is not exactly nfft floats long. From Python, the owed row as a (1, nfft) matrix, or None when none was owed. None when the C call writes no output.

Raises:

Type Description
ValueError

If the C call returns a negative value. The exception message is flush: out must hold exactly nfft floats, with the return code appended (gh-869).

flush_max_out

flush_max_out() -> int

Floats dp_spectrogram_flush() writes: nfft, the one row it can owe.

Sized by the object alone, since a flush owes at most one row. It is also the only row length dp_spectrogram_flush() accepts.

Returns:

Type Description
int

Floats: nfft.

rows_for

rows_for(n_in: int) -> int

How many rows a push of n_in more samples would complete, from the current carry.

Takes no input. Use it to size chunks that bound memory.

from doppler.spectral import Spectrogram sp = Spectrogram(nfft=8, hop=2) sp.rows_for(7), sp.rows_for(8), sp.rows_for(100) (0, 1, 47)

The carry plus n_in samples, cut into frames of nfft at the hop. A push with room for this many rows writes exactly this many and takes all of n_in.

Parameters:

Name Type Description Default
n_in int

Samples about to be pushed.

required

Returns:

Type Description
int

Rows (not floats) that push would complete.

reset

reset() -> None

Forget the stream: drop the carry and restart at sample 0.

The configuration is kept. consumed and pending read 0 afterwards.

import numpy as np from doppler.spectral import Spectrogram sp = Spectrogram(nfft=8, hop=8) _ = sp.push(np.ones(5, np.complex64)) sp.pending 5 sp.reset() sp.pending 0

The next row covers samples [0, nfft) of whatever is pushed next. The consumed count and the pending count read 0. Configuration is kept.

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 Spectrogram 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 the state that evolves as the object runs, including any value a setter changed after construction, so a blob taken now and restored later resumes from this point. Construction parameters are not restored: restore into an object built the same way. A core may check some of them and refuse a blob built differently.

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 Spectrogram 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, including any value a setter changed after construction. Construction parameters are not restored: restore into an object built the same way. Length is validated against state_bytes() before the blob is handed to the C core, which may reject it too: a core may check some construction parameters and refuse a blob built differently.

Raises TypeError if blob is not bytes, ValueError if its length differs from state_bytes() or the core rejects it, and RuntimeError if the Spectrogram 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__() -> Spectrogram

Enter a context manager, returning this object.

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

Returns:

Type Description
Spectrogram

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

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 is cut into segments of n samples, one every hop samples (default hop = n, non-overlapping); each complete segment is windowed, zero-padded to nfft = next_pow_two(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_pow_two(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 whose frames are spaced by a hop.

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); finite and > 0.

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

The window each segment is shaped by. rect is no taper: best resolution, worst leakage. One of "hann", "kaiser", "blackman-harris", "rect".

  • "hann" — Hann: the default.
  • "kaiser" — Kaiser, shaped by beta.
  • "blackman-harris" — 4-term Blackman-Harris.
  • "rect" — Rectangular: no taper, best resolution, worst leakage.
"hann"
beta float

Kaiser beta (ignored by every other window). One that makes the window non-finite -- NaN, or large enough to overflow it -- is refused.

0.0
pad int

Zero-pad factor (>= 1; 0 is refused); nfft = next_pow_two(n * pad).

1
full_scale float

Amplitude that reads 0 dBFS in the dB getters; finite and > 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. At most 64.

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

How successive traces are reduced: the running mean, an exponential average, or a max or min hold. One of "mean", "exp", "maxhold", "minhold".

  • "mean" — Linear running mean of all frames.
  • "exp" — Exponential moving average (alpha).
  • "maxhold" — Per-bin running maximum.
  • "minhold" — Per-bin running minimum.
"mean"
alpha float

EMA smoothing factor, exp mode only, where it must lie in (0, 1]: the rule is the AccTrace averager's, whose create refuses anything else, so this does too. Ignored, and so not checked, in the other modes.

0.1
hop int

Samples between frame starts, 1..n. 0 means n, which gives non-overlapping frames; the default. A hop above n would leave samples between frames that are never kept, so it is refused.

0

Raises:

Type Description
ValueError

If construction fails. The exception message is PSD: invalid parameter (need n >= 2, pad >= 1 with n * pad within the largest FFT, finite fs > 0, bits <= 64, hop in 1..n (0 = n), a finite full_scale > 0 when bits = 0, a window whose taps sum to a finite positive value (not a NaN or overflowing kaiser beta), and in exp mode 0 < alpha <= 1).

Examples:

>>> from doppler.spectral import PSD
>>> w = PSD(n=1024, fs=1.0e6, window="kaiser", beta=8.0, mode="mean", hop=512)
>>> 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 (the kernel's, copied at create).

rbw property

rbw: float

Rbw.

count property

count: int

Frames folded into the averaged trace so far.

mode property

mode: int

Trace reduction mode, as the enum index: 0 mean, 1 exp, 2 maxhold, 3 minhold.

accumulate

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

Window, FFT and fold every cf32 frame the stream completes into the average; a partial frame is carried to the next call.

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 the whole real frames of a block into the average, at hop = n. A trailing partial frame is dropped. It neither reads nor writes the carry of the complex accumulate, so mixing the two on one object mixes two streams.

This call stays on whole frames at hop = n: it folds floor(n_in / n) frames and drops a trailing partial frame. It neither reads nor writes the carry of the complex accumulate call, so mixing the two on one object mixes two streams, and a hop below n is not used by this call.

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

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

Parameters:

Name Type Description Default
out NDArray[float32] | None

Destination, at least nfft float32 elements.

None

Returns:

Type Description
NDArray[float32] | None

min(nfft, max_out), or 0 if empty. None when the C call writes no output.

psd_db_max_out

psd_db_max_out() -> int

Output capacity hint for dp_psd_psd_db().

Returns:

Type Description
int

nfft.

psd_dbhz

psd_dbhz(
    out: NDArray[float32] | None = None,
) -> NDArray[np.float32] | None

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

Parameters:

Name Type Description Default
out NDArray[float32] | None

Destination, at least nfft float32 elements.

None

Returns:

Type Description
NDArray[float32] | None

min(nfft, max_out), or 0 if empty. None when the C call writes no 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 dp_psd_psd_dbhz().

Returns:

Type Description
int

nfft.

power_twosided

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

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

Parameters:

Name Type Description Default
out NDArray[float32] | None

Destination, at least nfft float32 elements.

None

Returns:

Type Description
NDArray[float32] | None

min(nfft, max_out), or 0 if empty. None when the C call writes no output.

power_twosided_max_out

power_twosided_max_out() -> int

Output capacity hint for dp_psd_power_twosided().

Returns:

Type Description
int

nfft, the zero-padded transform length.

power_onesided

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

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

Parameters:

Name Type Description Default
out NDArray[float32] | None

Destination, at least nfft/2 + 1 float32 elements.

None

Returns:

Type Description
NDArray[float32] | None

min(nfft/2 + 1, max_out), or 0 if empty. None when the C call writes no output.

power_onesided_max_out

power_onesided_max_out() -> int

Output capacity hint for dp_psd_power_onesided().

Returns:

Type Description
int

nfft/2 + 1.

band_power

band_power(
    bands: NDArray[float64],
    out: NDArray[float32] | None = None,
) -> NDArray[np.float32] | None

Integrated power per band in dB; bands = [lo0,hi0,lo1,hi1,...] Hz. None when there is nothing to report: before any accumulate, or no complete lo/hi pair.

Parameters:

Name Type Description Default
bands NDArray[float64]

Flat [lo,hi,...] band edges, Hz.

required
out NDArray[float32] | None

Destination, at least n_bands float32 elements.

None

Returns:

Type Description
NDArray[float32] | None

The number of bands written, min(n_bands, max_out); 0 (Python None) when there is nothing to report. None when the C call writes no output.

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

The output length depends on the bands argument, which this cannot see, so the binding sizes the buffer from the bands itself.

Returns:

Type Description
int

Always 0 (the "caller sizes it" sentinel).

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.

Calls obw_from_power on the averaged spectrum, so the search and its domain are that function's and the two cannot disagree.

Parameters:

Name Type Description Default
fraction float

Power fraction in the OPEN interval (0, 1), e.g. 0.99. 1 is excluded: all of the power in float is every bin that rounding residue reached, which has no stable answer.

required

Returns:

Type Description
float

Occupied bandwidth in Hz (0 if empty or no power); NaN when fraction is outside (0, 1), before any frame too, rather than a silently clamped answer.

Examples:

>>> import numpy as np
>>> from doppler.spectral import PSD
>>> w = PSD(n=64, fs=64.0, window="rect")
>>> w.accumulate(np.ones(64, dtype=np.complex64))  # all power at DC
>>> w.occupied_bw(0.99)                            # one bin, fs / 64
1.0
>>> w.occupied_bw(1.0)                             # outside (0, 1)
nan

noise_floor

noise_floor() -> float

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

Fills the object's internal dB scratch (the psd_db() trace over all nfft bins) and takes its median.

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 the state that evolves as the object runs, including any value a setter changed after construction, so a blob taken now and restored later resumes from this point. Construction parameters are not restored: restore into an object built the same way. A core may check some of them and refuse a blob built differently.

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, including any value a setter changed after construction. Construction parameters are not restored: restore into an object built the same way. Length is validated against state_bytes() before the blob is handed to the C core, which may reject it too: a core may check some construction parameters and refuse a blob built differently.

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. This is the symmetric form, which filter design wants; PSD and the objects built on it apply the periodic form (the window of N + 1 points, the last dropped), see dp_psd_kernel_init().

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

Occupied bandwidth: the width of the central interval holding frac of the total power, with (1 - frac)/2 excluded at each end.

A cumulative walk over a DC-centred linear-power spectrum; the span is whole bins, inclusive, times fs / pwr_len. Any constant per-bin scale cancels, so the power need not be referenced.

Parameters:

Name Type Description Default
pwr NDArray[float64]

Linear power, DC-centred, non-negative.

required
fs float

Sample rate, Hz.

required
frac float

Power fraction in the OPEN interval (0, 1), e.g. 0.99. 1 is excluded: all of the power in floating point is every bin rounding residue reached, which has no stable answer.

required

Returns:

Type Description
float

Occupied bandwidth in Hz; 0 for no bins or no power; NaN when frac is outside (0, 1), whatever the data.

Examples:

>>> import numpy as np
>>> from doppler.spectral import obw_from_power
>>> pwr = np.zeros(64)
>>> pwr[32] = 1.0                    # one bin holds all the power
>>> obw_from_power(pwr, 64.0, 0.99)  # one bin wide, fs / 64
1.0
>>> obw_from_power(pwr, 64.0, 1.0)   # outside (0, 1)
nan

noise_floor_db

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

Robust noise-floor estimate: the median of a dB spectrum.

Tones and spurs occupy few bins and sit well above the median, so the median tracks the noise pedestal rather than the signal. The median is taken on a sorted heap copy; db is not modified. For an even length it is the mean of the two middle values.

Parameters:

Name Type Description Default
db NDArray[float32]

Spectrum in dB (any reference: dBFS, dBc, ...).

required

Returns:

Type Description
float

Median of db, in the same dB units; 0.0 when db_len is 0 or the scratch copy cannot be allocated.

power_to_db_f32

power_to_db_f32(
    lin: NDArray[float32],
) -> NDArray[np.float32]

Linear power to dB: 10·log10, with PSD's floor, fast.

Each value of out is 10·log10 of the matching value of lin when that is at or above 1e-20, and exactly -200 below it: 0, a subnormal and a negative value all read -200. That is PSD's floor, the one definition, so there is no floor argument. A power of two converts exactly, to the correctly rounded value (1.0 reads exactly 0 dB), and every other value lies within 0.01 dB of double-precision 10·log10. The bound is MEASURED over every positive finite float32 (native/validation/power_to_db_sweep.c, in make test-sweep); the achieved worst, 3.25e-4 dB, is in the measurement record (docs/design/spectrogram-measurements.md, "The fast dB conversion").

It is the library's dB conversion for power: every dB reading PSD takes goes through it, so a PSD or Spectrogram dB value is this function of the matching linear value, bit for bit, and a display that converts only the power bins it draws gets exactly the dB rows. Branchless, so it vectorizes. NaN and Inf in give an unspecified value (PSD produces neither from finite input). lin_len 0 writes nothing.

Parameters:

Name Type Description Default
lin NDArray[float32]

Linear power (a power ratio), float32.

required

Returns:

Type Description
NDArray[float32]

Output.

Examples:

>>> import numpy as np
>>> from doppler.spectral import power_to_db_f32
>>> x = np.array([1.0, 2.0, 0.5, 0.0, 1e-30], dtype=np.float32)
>>> power_to_db_f32(x).tolist()     # 2^k exactly; the floor below 1e-20
[0.0, 3.0102999210357666, -3.0102999210357666, -200.0, -200.0]
>>> d = power_to_db_f32(np.array([100.0], dtype=np.float32))[0]
>>> bool(abs(d - 20.0) < 0.01)     # off a power of two: within 0.01 dB
True

Window and mode names

PSD_WINDOW and SPECTROGRAM_MODE map each C enum value to the name the window= and mode= arguments take, so a configuration can be checked against the values the library actually has:

>>> from doppler.spectral import PSD_WINDOW, SPECTROGRAM_MODE
>>> sorted(PSD_WINDOW.values())
['blackman-harris', 'hann', 'kaiser', 'rect']
>>> SPECTROGRAM_MODE
{0: 'power', 1: 'db'}

Gallery — CarrierAcquisition: RRC Pulse Shaping, Correlation and Detection, Delay-Doppler Search, 2-D Acquisition Grid, Gallery, A Streaming Spectrogram, Four WCDMA Carriers — PSD, band_power, AccTrace Guides — Delay-Doppler Search, DSSS Burst Acquisition, Guides, Power Spectra & Measurements, Streaming Spectrograms Design — The acquisition surface — the measurement record, The acquisition surface: the DSSS search engine, API taxonomy: the DSP building-block hierarchy and its naming axis, Corr2D batches: many maps in one call, Corr2D hypothesis bank: operation order, Corr2D: decoupled (interpolated) inverse length, One Python class: the step-4 design, One correlator: the step-3 design, DsssBurstReceiver: the burst chain, composed in C, Design, Measurement Suite — single-tone ADC / spectral metrics, Spectrum Analyzer, Spectral & Measurement API Map, The Spectrogram — the measurement record, The Spectrogram — chunks in, rows out Contributing — Adding an algorithm — the lifecycle, DSSS Primary Use Cases for Code Acquisition Design, C Return-Code Convention, Validation log, Code coverage