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 |
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)
reset
¶
Zero the accumulator and reset the integration counter to 0. Equivalent to starting a fresh dwell cycle without rebuilding FFT plans or recomputing ref_spec.
Examples:
execute
¶
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 |
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
¶
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
¶
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
¶
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, |
set_state
¶
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 |
required |
Raises:
| Type | Description |
|---|---|
MemoryError
|
If the C call returns |
ValueError
|
If the C core rejects the blob with any other non-zero status. The
exception message is |
destroy
¶
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 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 |
-1
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If construction fails. The exception message is |
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)
reset
¶
Zero the accumulator and reset the integration counter to 0. Equivalent to starting a fresh dwell cycle without rebuilding FFT plans or recomputing ref_spec.
Examples:
execute
¶
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 |
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
¶
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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the C call returns |
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
¶
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 |
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 |
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
¶
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 |
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
¶
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
¶
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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the C call returns |
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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the C call returns |
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
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the C call returns |
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):
- Detect. dp_correlator_bank_power_max() over carriers g_min ..
g_max on the
1 / (L_d nx)grid, with every rate. - 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).
- 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)tom g_d + (m - 1), m =L_r / L_d: the carriers between its coarse neighbors, on the1 / (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 nxmultiply-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. - 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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the C call returns |
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
¶
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
¶
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, |
set_state
¶
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 |
required |
Raises:
| Type | Description |
|---|---|
MemoryError
|
If the C call returns |
ValueError
|
If the C core rejects the blob with any other non-zero status. The
exception message is |
destroy
¶
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 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, |
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 |
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
doppler_step
property
¶
Doppler spacing between adjacent output rows, cycles/sample: 1 / (ny_out * nx). 0 for Corr2D.
reset
¶
Zero the accumulator and reset the integration counter to 0. Equivalent to starting a fresh dwell cycle without rebuilding FFT plans or recomputing ref_spec.
Examples:
execute
¶
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 |
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
¶
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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the C call returns |
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
¶
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 |
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 |
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
¶
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 |
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
¶
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
¶
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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the C call returns |
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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the C call returns |
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
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the C call returns |
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):
- Detect. dp_correlator_bank_power_max() over carriers g_min ..
g_max on the
1 / (L_d nx)grid, with every rate. - 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).
- 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)tom g_d + (m - 1), m =L_r / L_d: the carriers between its coarse neighbors, on the1 / (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 nxmultiply-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. - 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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the C call returns |
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
¶
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
¶
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, |
set_state
¶
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 |
required |
Raises:
| Type | Description |
|---|---|
MemoryError
|
If the C call returns |
ValueError
|
If the C core rejects the blob with any other non-zero status. The
exception message is |
destroy
¶
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 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"
|
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 |
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)
threshold
property
¶
Test-stat gate: 0 fires on every dump, > 0 on test_stat above it.
last_corr
property
¶
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
¶
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
¶
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
¶
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
¶
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, |
set_state
¶
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 |
required |
Raises:
| Type | Description |
|---|---|
MemoryError
|
If the C call returns |
ValueError
|
If the C core rejects the blob with any other non-zero status. The
exception message is |
destroy
¶
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 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"
|
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 |
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)
threshold
property
¶
Test-stat gate: 0 fires on every dump, > 0 on test_stat above it.
last_corr
property
¶
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
¶
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
¶
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
¶
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
¶
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, |
set_state
¶
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 |
required |
Raises:
| Type | Description |
|---|---|
MemoryError
|
If the C call returns |
ValueError
|
If the C core rejects the blob with any other non-zero status. The
exception message is |
destroy
¶
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 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"
|
beta
|
float
|
Kaiser beta (ignored for the other windows). |
0.0
|
mode
|
Literal['power', 'db']
|
The row units: |
"power"
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If construction fails. The exception message is |
Examples:
Create with defaults:
nfft
property
¶
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
¶
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
¶
The window every frame is shaped by: "hann", "kaiser", "blackman-harris" or "rect".
beta
property
¶
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
¶
The row units: "power" (linear against full scale, the default) or "db" (dBFS).
consumed
property
¶
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
¶
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 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
¶
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
¶
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_max_out
¶
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
¶
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
¶
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
¶
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
¶
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, |
set_state
¶
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 |
required |
destroy
¶
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 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.
|
"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"
|
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 |
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
mode
property
¶
Trace reduction mode, as the enum index: 0 mean, 1 exp, 2 maxhold, 3 minhold.
accumulate
¶
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
¶
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 |
psd_db
¶
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
¶
Output capacity hint for dp_psd_psd_db().
Returns:
| Type | Description |
|---|---|
int
|
nfft. |
psd_dbhz
¶
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:
psd_dbhz_max_out
¶
Output capacity hint for dp_psd_psd_dbhz().
Returns:
| Type | Description |
|---|---|
int
|
nfft. |
power_twosided
¶
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
¶
Output capacity hint for dp_psd_power_twosided().
Returns:
| Type | Description |
|---|---|
int
|
nfft, the zero-padded transform length. |
power_onesided
¶
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
¶
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 |
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:
band_power_max_out
¶
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 integrated power across all bands in dB.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bands
|
NDArray[float64]
|
Flat |
required |
Returns:
| Type | Description |
|---|---|
float
|
Total band power in dB (dB floor if empty). |
occupied_bw
¶
Occupied bandwidth in Hz holding the given fraction of total power.
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:
noise_floor
¶
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
¶
Peak-in-band level minus noise floor, in dB.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lo_hz
|
float
|
Band lower edge, Hz. |
required |
hi_hz
|
float
|
Band upper edge, Hz. |
required |
Returns:
| Type | Description |
|---|---|
float
|
SNR in dB (0 if empty). |
sfdr
¶
Spurious-free dynamic range in dB from the top two peaks.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
min_db
|
float
|
Minimum peak level considered, dB. |
required |
Returns:
| Type | Description |
|---|---|
float
|
Carrier-minus-highest-spur level in dB (0 if fewer than two peaks). |
state_bytes
¶
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
¶
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, |
set_state
¶
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 |
required |
destroy
¶
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 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
¶
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:
kaiser_window
¶
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:
kaiser_enbw
¶
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:
kaiser_beta_for_sidelobe
¶
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:
blackman_harris_window
¶
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:
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:
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:
find_peaks_f32
¶
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:
obw_from_power
¶
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:
noise_floor_db
¶
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
¶
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'}
Related pages¶
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