Skip to content

Python Acquire API

The doppler.acquire module holds the searches that find a signal before anything tracks it:

  • Acquisition — the streaming code-phase × Doppler engine.
  • BurstAcquisition and BurstCapture — find a burst of any repeated complex preamble (a PN code, Zadoff-Chu, a chirp or a QPSK sequence), with PersistentBurstCapture keeping its ring in a file.
  • CarrierAcquisition — a PSDMF (power-spectral-density matched-filter) residual-carrier frequency estimator. It runs after Acquisition's coarse Doppler search as a one-shot refinement stage: non-coherently average the incoming stream's power spectrum, then circularly correlate that average against a known power spectrum shape (the default is the average PSD of a random rectangular-pulse BPSK stream, a sinc²; psd_template overrides it for a different pulse shape or modulation) to find the residual carrier offset.
  • bin_to_signed — the Doppler-bin fold convention the searches and their hand-offs share.

The DSSS receivers in the DSSS API compose these objects.

Source: src/doppler/acquire/__init__.py

See the CarrierAcquisition: RRC Pulse Shaping gallery page for a worked example showing why the template matters.


CarrierAcquisition — PSDMF residual-carrier estimation

Composes doppler.spectral.PSD (FFT + window + non-coherent power averaging), doppler.spectral.CorrDetector (FFT-based correlation of the averaged power against the known template, plus a noise-referenced test statistic), and doppler.detection's Pfa/Pd statistics (the same ones Acquisition itself is built on) for the detection gate. sequential (test every block, adaptive) vs. non-sequential (a fixed dwell_target wait) mirror the two modes of the FrequencyAcquisition reference design — max_n_blocks is sequential mode's own give-up cap, deliberately independent of dwell_target.

CarrierAcquisition

Create a PSD-template carrier acquisition (residual-carrier detector and estimator).

Parameters:

Name Type Description Default
sample_rate_hz float

Sample rate of the input stream, Hz (required).

required
symbol_rate_hz float

Symbol rate, Hz -- builds the default template (required).

required
resolution_hz float

Desired FFT frequency resolution, Hz. <= 0.0 is a sentinel meaning "auto": symbol_rate_hz/10.0. The block is round(sample_rate_hz/resolution_hz) samples, floored at 3 -- a block of 1 or 2 gives no usable estimate (#1959).

0.0
zero_pad int

PSD zero-pad factor (>= 1); see psd_core.h.

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

The search window. hann is the default and the window the detection threshold was fit at. One of "hann", "kaiser", "blackman-harris".

  • "hann" — Hann: the default.
  • "kaiser" — Kaiser, shaped by beta.
  • "blackman-harris" — 4-term Blackman-Harris.
"hann"
beta float

Kaiser beta (ignored for hann/blackman-harris).

0.0
psd_template NDArray[float32]

Known PSD-shape template override, length must equal nfft = next_pow_two(block * zero_pad), block as above; NULL/length-0 means "not supplied" -- the default rectangular-pulse sinc^2 template (from symbol_rate_hz) is used.

...
pfa float

Target per-test false-alarm probability.

1e-3
pd float

Target detection probability.

0.9
design_snr float

Assumed per-sample amplitude SNR used ONLY to precompute dwell_target via dp_det_n_noncoh(); not a live measurement. An optimistic guess only affects NON-sequential mode (which trusts this one-shot wait count outright) -- sequential mode's own give-up bound is max_n_blocks, not dwell_target, precisely so a wrong design_snr can't stop it from trying more blocks once real data shows it needs to.

2.0
sequential bool

True: test for a detection after EVERY block (the per-block CFAR ratio threshold -- see carrier_acq_ratio_threshold() in carrier_acq_core.c -- tightens as more looks accumulate), stopping the moment one fires or max_n_blocks is reached. False: accumulate silently and test once, at dwell_target.

True
max_n_blocks int

Sequential mode's own give-up cap (ignored by non-sequential mode, which stops at dwell_target instead) -- deliberately a SEPARATE, generous bound from dwell_target; capping sequential mode at design_snr's own point estimate would defeat the reason to test every block in the first place.

100000

Examples:

>>> import numpy as np
>>> from doppler.acquire import CarrierAcquisition
>>> rng = np.random.default_rng(12345)
>>> bits = np.where(rng.integers(0, 2, 4000), 1.0, -1.0)
>>> data = np.repeat(bits, 8)                 # 8 samples/symbol BPSK
>>> t = np.arange(len(data))
>>> x = (data * np.exp(2j * np.pi * 123.0 * t / 8000.0)).astype(
...     np.complex64)  # residual carrier at 123 Hz
>>> ca = CarrierAcquisition(
...     sample_rate_hz=8000.0, symbol_rate_hz=1000.0,
...     psd_template=np.array([], dtype=np.float32))
>>> ca.steps(x)                   # fold the stream, testing each block
>>> ca.ready                      # detection fired
True
>>> round(ca.residual_hz, 0)      # recovered residual carrier, Hz
123.0

ready property

ready: bool

True once a detection has fired (or the dwell_target give-up cap was reached) -- residual_hz is only meaningful once this is true.

residual_hz property

residual_hz: float

Sub-bin-refined residual carrier frequency estimate, Hz. Valid only when ready is true.

n_blocks property

n_blocks: int

Number of n_fft-length blocks actually folded into the PSD average so far.

dwell_target property

dwell_target: int

Non-sequential mode's precomputed fixed wait count, from det_n_noncoh(design_snr, ...) at construction. Ignored by sequential mode's own give-up bound -- see max_n_blocks.

max_n_blocks property

max_n_blocks: int

Sequential mode's own give-up cap (independent of dwell_target) -- the max_n_blocks constructor argument, echoed back.

nfft property

nfft: int

PSD transform length (next_pow_two(n_fft*zero_pad)) -- the length any caller-supplied template array must match.

steps

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

Fold raw complex samples into the running PSD average and test for a detection; any chunk size across repeated calls (a partial trailing block carries to the next call).

Parameters:

Name Type Description Default
x NDArray[complex64]

Raw complex input samples (cf32).

required

Examples:

>>> import numpy as np
>>> from doppler.acquire import CarrierAcquisition
>>> rng = np.random.default_rng(12345)
>>> bits = np.where(rng.integers(0, 2, 4000), 1.0, -1.0)
>>> data = np.repeat(bits, 8)                 # 8 samples/symbol BPSK
>>> t = np.arange(len(data))
>>> x = (data * np.exp(2j * np.pi * 123.0 * t / 8000.0)).astype(
...     np.complex64)  # residual carrier at 123 Hz
>>> ca = CarrierAcquisition(
...     sample_rate_hz=8000.0, symbol_rate_hz=1000.0,
...     psd_template=np.array([], dtype=np.float32))
>>> ca.steps(x)                   # fold the stream, testing each block
>>> ca.ready
True
>>> round(ca.residual_hz, 0)      # recovered residual carrier, Hz
123.0

reset

reset() -> None

Discard the running PSD average and detection state; counters return to zero.

Use it to reuse one detector across successive captures: after a detection (or a give-up) the running average and counters are cleared, so the next steps() starts folding a fresh stream from zero.

Examples:

>>> import numpy as np
>>> from doppler.acquire import CarrierAcquisition
>>> ca = CarrierAcquisition(
...     sample_rate_hz=8000.0, symbol_rate_hz=1000.0,
...     psd_template=np.array([], dtype=np.float32))
>>> ca.steps(np.zeros(2048, dtype=np.complex64))  # accumulate looks
>>> ca.n_blocks > 0
True
>>> ca.reset()                    # discard the running PSD average
>>> ca.n_blocks
0

state_bytes

state_bytes() -> int

Size in bytes of this object's serialized state.

The exact length get_state returns and set_state requires. It depends on how the object was constructed (state arrays are sized at construction), so read it from the instance rather than assuming a constant.

Raises RuntimeError if the CarrierAcquisition has already been destroyed.

Returns:

Type Description
int

Byte length of one serialized state blob.

get_state

get_state() -> bytes

Serialize this object's mutable state to bytes.

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

The blob is opaque and always state_bytes() long. Its layout is an implementation detail of the C core and is not a stable format across builds.

Raises RuntimeError if the CarrierAcquisition has already been destroyed.

Returns:

Type Description
bytes

Opaque snapshot, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

Restore mutable state from a get_state() blob.

Overwrites the live state in place, including any value a setter changed after construction. Construction parameters are not restored: restore into an object built the same way. Length is validated against state_bytes() before the blob is handed to the C core, which may reject it too: a core may check some construction parameters and refuse a blob built differently.

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

Parameters:

Name Type Description Default
blob bytes

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

required

destroy

destroy() -> None

Release the underlying C resources immediately.

Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this to release them at a definite point instead, or use the object as a context manager, which calls it on exit.

Idempotent: calling it again on an already-released object does nothing. Every other method raises RuntimeError once it has run.

__enter__

__enter__() -> CarrierAcquisition

Enter a context manager, returning this object.

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

Returns:

Type Description
CarrierAcquisition

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

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.

...

Acquisition — continuous acquisition

Acquisition searches a continuous spread-spectrum signal (a code that repeats forever, data riding on it) over the joint (Doppler × code-phase) grid. It takes the code, chip_rate and the sample_rate it is fed at, always tiles the Doppler uncertainty with frequency windows, and sizes its non-coherent looks from (cn0_dbhz, pfa, pd) using doppler.detection; its coherent depth is set by the code-only epochs you declare (code_only_epochs) and capped by doppler_rate, never grown to meet pd. Push arbitrary-length blocks; it yields one 9-tuple per detection — (doppler_bin, code_phase, peak_mag, noise_est, test_stat, cn0_dbhz_est, samples_consumed, chip_phase, doppler_rate_hz_s), chip_phase being the Dll seed in chips and doppler_rate_hz_s the winning Doppler-rate hypothesis (0 unless the search was built with rate_hypotheses). See the DSSS acquisition guide for when to use it.

Acquisition

Create a continuous-mode acquisition engine: always wideband window-tiling, allowing a block-coherent depth inside the tiles to accommodate waveforms with code-only windows.

Parameters:

Name Type Description Default
code NDArray[uint8]

PN chips (0/1), length code_len.

required
sample_rate float

The rate of the samples pushed, Hz. One code period is then n = sample_rate * code_len / chip_rate samples, which must be a whole number of at least code_len: the search's transforms are n points long, so a smooth n (a power of two, dp_correlator_period_samples()) is what makes them fast. When n is a multiple of code_len each chip is held n / code_len samples; otherwise each sample takes the chip its centre falls in, and the correlation shape is measured from that replica.

4000000.0
chip_rate float

Chip rate in Hz (> 0).

1000000.0
symbol_rate float

Continuous data-symbol rate in Hz; <= 0 means no known clock. Diagnostic only (exposed via dp_correlator_search_t::epochs_per_symbol), doesn't feed sizing: this engine never coherently combines regardless of the data-modulation clock.

1000.0
cn0_dbhz float

Carrier-to-noise density in dB-Hz: any finite value. A continuous engine needs one -- its non-coherent looks cannot be chosen without a target.

50.0
doppler_uncertainty float

One-sided Doppler search half-range in Hz; 0 uses the full native span +/- chip_rate/(2*sf) (still window-tiled, at window_bins=1).

0.0
pfa float

Target system (max-of-N) false-alarm probability (0,1).

1e-3
pd float

Target detection probability (0,1).

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

The CFAR reference: the statistic the noise floor is estimated with, which the detection threshold scales. One of "mean", "median", "min", "max".

"mean"
code_only_epochs int

Whole code-only epochs a waveform's code-only window holds at any chip phase (floor(W_symbols * chips_per_symbol / sf) - 1; docs/design/async-dsss-receiver.md §2.1) -- the engine allows a coherent depth to accommodate such waveforms. 1 = no window: a coherent depth of 1.

1
doppler_rate float

Doppler rate in Hz/s the coherent depth is bounded against (the drift over one block stays inside half a slow-time row); 0 leaves the window as the only bound.

0.0

Raises:

Type Description
ValueError

If construction fails. The exception message is ``Acquisition: invalid parameter (need chip_rate > 0 and a sample_rate whose code period, sample_rate * len(code) / chip_rate, is a whole number of samples, at least len(code); a finite cn0_dbhz; 0 < pfa < 1 and 0 < pd < 1; a finite doppler_uncertainty >= 0 and doppler_rate >= 0; code_only_epochs

= 1)``.

Warns:

Type Description
UserWarning

Emitted after construction when underpowered holds: Acquisition is under-powered: pd_predicted < pd at this cn0_dbhz. Raise cn0_dbhz or narrow doppler_uncertainty..

Examples:

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(PN(poly=mls_poly(5), seed=1,
...                      length=5).generate(31)).astype(np.uint8)
>>> s0 = np.repeat(np.where(code & 1, -1.0, 1.0), 4).astype(
...     np.complex64)
>>> burst = np.tile(np.roll(s0, 17), 23).astype(np.complex64)
>>> a = Acquisition(code, sample_rate=4e6, chip_rate=1e6, cn0_dbhz=50.0)
>>> a.push(burst)[0][:2]    # detects (Doppler-window bin, code phase)
(0, 18)
>>> (a.code_bins, a.lag_bins)  # 124 samples, read on 128 columns
(124, 128)
>>> round(18 * a.code_bins / a.lag_bins, 2)  # column 18, in samples
17.44
>>> a.coherent_bins            # no window given: one epoch
1
>>> b = Acquisition(code, sample_rate=4e6, chip_rate=1e6, cn0_dbhz=50.0,
...                 code_only_epochs=7)
>>> b.coherent_bins            # (7 + 1) // 2: a whole block fits
4

max_peaks property

max_peaks: int

The peak list's capacity per dwell (1 = the classic gated maximum); set with set_max_peaks().

carrier_freq_hz property

carrier_freq_hz: float

RF carrier the Doppler is physically coupled to, Hz (0.0 = uncoupled); set with set_carrier_freq_hz().

code_bins property

code_bins: int

One code period in samples, sample_rate * sf / chip_rate: the length of every forward transform and of a raw epoch.

lag_bins property

lag_bins: int

Code-phase columns of the surface. Equal to code_bins unless code_bins is not 7-smooth; then the correlation is read on the next power of two, an exact interpolation that keeps the inverse transforms fast. Column c is a lag of c * code_bins / lag_bins samples; surface_chip_phase() gives each column in chips.

doppler_bins property

doppler_bins: int

Native Doppler bins this engine searches: the window-tile count times the block-coherent depth inside each tile (coherent_bins), one uniform grid of doppler_res_hz over the tiled span in FFT-bin order -- what a hit's doppler_bin indexes.

coherent_bins property

coherent_bins: int

The block-coherent depth D inside every window tile (docs/design/acq-surface.md §4): epochs per block and Doppler rows per tile. 1 without a code-only window.

sf property

sf: int

Chips per PN segment, inferred from len(code).

n_noncoh property

n_noncoh: int

Non-coherent looks per detection (1 = pure coherent).

ring_cap property

ring_cap: int

Input ring capacity in complex samples.

noise_lo property

noise_lo: int

First CFAR reference bin (inclusive).

noise_hi property

noise_hi: int

Last CFAR reference bin (inclusive).

threshold property

threshold: float

CFAR gate on the test statistic (coherent path).

eta property

eta: float

Raw per-cell Rayleigh amplitude threshold.

eta_nc property

eta_nc: float

Non-coherent CFAR threshold (order-N_nc Marcum).

pfa_cell property

pfa_cell: float

Šidák per-cell false-alarm probability over the searched cells.

pd_predicted property

pd_predicted: float

Predicted Pd at cn0_dbhz and the chosen grid: the average Pd over the straddle priors (slow-time scalloping, intra-segment rotation, code-phase sample offset - quadrature over uniform priors), matching what the Monte-Carlo characterization measures rather than the on-grid best case.

straddle_loss property

straddle_loss: float

Mean amplitude derating of the correlation peak from grid straddle (slow-time Doppler scalloping x intra-segment rotation x code-phase sample offset, each averaged over a uniform prior) - a diagnostic summary; 20*log10(straddle_loss) is the loss in dB. Sizing and pd_predicted average Pd itself over the priors (Pd at this mean amplitude would overstate the mean Pd).

fs property

fs: float

Sample rate (Hz) the engine searches at: chip_rate * code_bins / sf.

chip_rate property

chip_rate: float

Chip rate (Hz).

cn0_dbhz property

cn0_dbhz: float

Carrier-to-noise density used to size the search (dB-Hz).

doppler_span_hz property

doppler_span_hz: float

Native unambiguous Doppler half-range = +/- chip_rate/(2*sf) Hz.

doppler_res_hz property

doppler_res_hz: float

Doppler bin width = chip_rate/(sf*doppler_bins) Hz.

pd property

pd: float

Target detection probability.

underpowered property

underpowered: bool

True when pd_predicted < pd -- the search cannot meet the target pd at this cn0_dbhz and geometry. The engine still builds a best-effort grid rather than failing; because C cannot raise a Python warning from a successful create, construction also emits a UserWarning in this case.

symbol_rate property

symbol_rate: float

Continuous data-symbol rate (Hz) this engine was built with -- diagnostic only, doesn't feed sizing (this engine never coherently combines regardless).

epochs_per_symbol property

epochs_per_symbol: float

(chip_rate/sf)/symbol_rate -- code epochs per data symbol; 0 when symbol_rate is 0.

threads property

threads: int

Workers the searcher fans its tiles across, the calling thread included (docs/design/acq-surface.md §13); 1 = serial. Set with set_threads(); a continuous engine with more than one tile starts at the machine's online core count.

keep_surface property writable

keep_surface: int

1 keeps every decided dwell's surface for surface() (normalised into the gate's units at each decision); 0 (the default) costs nothing. set_surface_sink() in C sets it.

surface_rows property

surface_rows: int

Rows of the surface surface() returns: the Doppler axis in surface units (tiles x interpolated slow-time rows); its columns are lag_bins.

surface_at property

surface_at: int

samples_consumed of the dwell whose surface surface() returns (0 until one has been captured).

n_peaks property

n_peaks: int

Picks in the last decided dwell, held twins included.

n_held property

n_held: int

Picks of the last decided dwell held as same-code-phase twins rather than listed (docs/design/async-dsss-receiver.md §7.1).

peak_conc property

peak_conc: float

Concentration of the last dwell's strongest peak: the power of its main lobe (its row and one either side, the exclusion zone's width) over the total power of its code-phase column across every Doppler row and tile. Near 1 for a clean single emitter, even one straddling two tiles; about 0.5 when a data transition splits it into twins two or more tiles away; lower when a coherent block straddles data (docs/design/async-dsss-receiver.md §2.4).

reset

reset() -> None

Drain the input ring and reset the coherent accumulator.

Discards any buffered samples that have not yet completed a frame and clears the non-coherent power accumulator and dwell bookkeeping, so the next push() begins a fresh search from an empty ring. The construction parameters — grid, thresholds, and PN reference — are untouched; only the in-flight streaming state is dropped.

Examples:

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(PN(poly=mls_poly(5), seed=1,
...                      length=5).generate(31)).astype(np.uint8)
>>> s0 = np.repeat(np.where(code & 1, -1.0, 1.0), 4).astype(
...     np.complex64)
>>> burst = np.tile(np.roll(s0, 17), 23).astype(np.complex64)
>>> a = Acquisition(code, sample_rate=4e6, chip_rate=1e6, cn0_dbhz=50.0)
>>> _ = a.push(burst[:100])   # a partial frame, buffered mid-stream
>>> a.reset()                 # drop it before it can bias a detection
>>> a.push(burst)[0][:2]      # (Doppler bin, code phase column)
(0, 18)

push

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

Stream raw samples; emit one event per CFAR dump above threshold.

Buffers x, then for every complete frame applies the slow-time Doppler FFT, correlates against the PN reference, dumps the coherent surface (or, when n_noncoh > 1, accumulates |·|² over n_noncoh looks first), gates the peak on the auto-configured threshold, and appends an dp_correlator_hit_t. Each event carries the peak's Doppler bin and code phase (the two search axes), its CFAR statistic, and an estimated C/N0 — see dp_correlator_hit_t.

Python's push() has room for 1024 events a call, so a push that ends at most 1024 / max_peaks dwells, counting the carry, loses nothing. Once fewer than max_peaks slots are left, the call stops before the next frame that would end a dwell -- and once all 1024 are used, before the next frame of any kind -- and the rest of its input is lost, unless the rest completes no frame, counting the carry already held: that is kept as the carry. The next push stays on the frame grid only when what was lost is a whole number of frames. Before v0.66 the room was 64, and a push past it kept up to ring_cap/frame_n - 1 frames for the next call, dropped the rest, and could cut a dwell's list short. #1992 and just-buildit/just-makeit#2184 track sizing the list to the call.

Parameters:

Name Type Description Default
x complex

Raw input, interleaved CF32, n_in complex samples.

required

Returns:

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

Number of events written (0 … max_results).

Examples:

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(PN(poly=mls_poly(5), seed=1,
...                      length=5).generate(31)).astype(np.uint8)
>>> s0 = np.repeat(np.where(code & 1, -1.0, 1.0), 4).astype(
...     np.complex64)
>>> a = Acquisition(code, sample_rate=4e6, chip_rate=1e6, cn0_dbhz=50.0,
...                 doppler_uncertainty=40e3)
>>> fs = 4e6                        # the sample_rate passed above
>>> t = np.arange(a.code_bins * a.n_noncoh)
>>> carrier = np.exp(2j * np.pi * (a.doppler_res_hz / fs) * t)
>>> sig = (np.tile(np.roll(s0, 17), a.n_noncoh)
...        * carrier).astype(np.complex64)
>>> a.push(sig)[0][:2]              # (Doppler-window bin, lag column)
(1, 18)

configure_search_raw

configure_search_raw(
    doppler_bins: int, n_noncoh: int
) -> None

Pin the search grid directly, bypassing both auto-sizing searches -- the advanced escape hatch (mirrors Dll.configure_lock_raw/Costas.configure_lock). Resizes every buffer/plan that depends on the grid (the slow-time FFT, the code correlator, the reference, and every per-frame scratch buffer), re-derives the threshold ladder for the pinned grid from the same physics init used, and clears in-flight accumulation (ring contents, the non-coherent power accumulator, dwell bookkeeping) -- call between push() calls, never a substitute for one. Raises ValueError if doppler_bins is outside [1, reps] or n_noncoh is outside [1, 256] (the internal non-coherent-look safety-valve ceiling).

Resizes every buffer/plan that depends on the grid (the slow-time FFT, the code correlator, the reference, and every per-frame scratch buffer), re-derives the threshold ladder for the pinned grid from the same physics dp_correlator_create_burst()/dp_correlator_create_continuous() used, and clears in-flight accumulation (ring contents, the non-coherent power accumulator, dwell bookkeeping) — call between push() calls, never a substitute for one.

Parameters:

Name Type Description Default
doppler_bins int

Coherent depth to pin, in [1, reps].

required
n_noncoh int

Non-coherent look count to pin, in [1, DP_CORRELATOR_N_NONCOH_SAFETY_CEILING].

required

Raises:

Type Description
ValueError

If the C call returns a non-zero status. The exception message is configure_search_raw failed, with the return code appended (gh-869).

Examples:

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(PN(poly=mls_poly(5), seed=1,
...                      length=5).generate(31)).astype(np.uint8)
>>> s0 = np.repeat(np.where(code & 1, -1.0, 1.0), 4).astype(
...     np.complex64)
>>> a = Acquisition(code, sample_rate=4e6, chip_rate=1e6, cn0_dbhz=50.0)
>>> a.configure_search_raw(doppler_bins=1, n_noncoh=4)  # pin the grid
>>> a.doppler_bins, a.n_noncoh
(1, 4)
>>> burst = np.tile(np.roll(s0, 17), 4).astype(np.complex64)
>>> a.push(burst)[0][:2]      # detects at the pinned grid
(0, 18)

set_max_peaks

set_max_peaks(n: int) -> None

How many peaks a dwell may report -- the peak list's capacity (docs/design/async-dsss-receiver.md section 7.1). One (the default) is the classic gated maximum. More lists every peak above the same gate, strongest first, with an exclusion zone of one Doppler bin by one chip around each (one emitter's main lobe, so its own shoulders are not the next peak) and the two-epoch rule for a peak at an already-listed code phase (a data transition inside the epoch splits one emitter into twins at its own code phase on other tiles; such a peak is held for one dwell and listed only if it is still there, at the same tile, on the next). Each listed peak is one record from push(), all of a dwell's sharing samples_consumed and noise_est; a held twin takes one of the n slots that dwell but is not reported. The threshold does not change with n. Raises ValueError outside 1..64. Clears the held candidates.

One (the default) is the classic detector -- the maximum of the surface, gated. More is the list of docs/design/async-dsss-receiver.md §7.1: every peak above the same gate, strongest first, each with an exclusion zone of one Doppler bin by the reference's first autocorrelation null around it (one chip for a PN code; one emitter's main lobe, so its own shoulders are not the next peak), and the two-epoch rule for a peak at an already-listed code phase -- a data transition inside the epoch splits one emitter into twins at its own code phase on other tiles, so such a peak is held for one dwell and listed only if it was there, at the same tile, on the previous one. Each listed peak is one dp_correlator_hit_t from dp_correlator_push(), all of a dwell's sharing its samples_consumed and noise_est. A held twin takes a slot of the n for that dwell but is not reported. The threshold does not change: a second peak is another draw from the same cells against the same union bound. Clears the held candidates.

Parameters:

Name Type Description Default
n int

1 … DP_CORRELATOR_MAX_PEAKS.

required

Raises:

Type Description
ValueError

If the C call returns a non-zero status. The exception message is set_max_peaks failed, with the return code appended (gh-869).

Examples:

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> code = (np.arange(31) * 5 % 2).astype(np.uint8)
>>> a = Acquisition(code, sample_rate=2e6, chip_rate=1e6, symbol_rate=1e3,
...                 cn0_dbhz=50.0, doppler_uncertainty=50e3)
>>> a.max_peaks
1
>>> a.set_max_peaks(8)
>>> a.max_peaks
8

set_carrier_freq_hz

set_carrier_freq_hz(carrier_freq_hz: float) -> None

Couple the code clock to the carrier: the chip rate dilates by doppler_hz / carrier_freq_hz, and the engine accounts for it -- every window tile's epochs are shifted along the code axis by the drift the tile's own frequency implies, aligned to the block's middle (inside a coherent block of D epochs), and the hand-off advances the hit's code phase by the drift over half the dwell (docs/design/async-dsss-receiver-measurements.md §12.11, §12.12). It rides in the state blob, so a resumed engine carries it on, and setting it rebuilds the one delay-Doppler map's ramp tables (docs/design/acq-surface.md §18.2). 0.0 (the default) = uncoupled, the engine as it ran without it. Raises ValueError for a carrier that is neither 0 nor finite and above half the sample rate (fs / 2): every Doppler the engine can report is below fs / 2, so above it |doppler / carrier| < 1 and the dilation stays finite.

A physically-coupled Doppler moves the code as well as the carrier -- 100 chips/s at 20 ppm of 5 Mcps -- and the engine's two long integrations both smear over it (doppler#1256, #1254):

  • Inside a coherent block of D epochs every tile's epoch correlations are shifted along the code axis by the drift the tile's own frequency implies, f_tile / carrier chips per chip, aligned to the block's middle (a linear phase on each epoch's product). The one delay-Doppler map applies it at the block's end, read on a slow-time grid 4 times finer and rounded. The bound is 0.09 dB, the worst over 33 tones and three dilations (docs/design/acq-surface.md §18.1); the engine measures 0.020-0.026 dB on its equivalence test's carrier scenes. dp_correlator_block_prompt() applies it exactly. A change of carrier mid-block applies to the whole block. Measured at the specification's 20 ppm with D = 154 (3.1 chips of drift across the block): without it the block's peak is 13 dB down and 3 chips wide and the depth detects nothing at 34 dB-Hz; with it the block reads as a still one.
  • The hand-off (dp_correlator_build_handoff()) advances the hit's code phase by the drift over half the dwell -- the non-coherent sum's peak is the phase at the dwell's middle, the seed is wanted at its end: 0.9 chip at the 40 dB-Hz floor, past a refine loop's pull-in.

A mutator's value (#2022): it rides in the state blob (v8), and a restore takes it through this setter's rebuild, so a resumed engine carries it on. Setting it rebuilds the map's fine grid and every tile's ramp tables when they move (the regrid that re-applies the thread request). Default 0.0 (uncoupled) is the engine exactly as it ran without it.

Parameters:

Name Type Description Default
carrier_freq_hz float

RF carrier, Hz; 0.0 = uncoupled.

required

Raises:

Type Description
MemoryError

If the C call returns DP_ERR_MEMORY. The exception message is set_carrier_freq_hz: rebuilding the delay-Doppler map for the new carrier could not allocate; the engine is unchanged.

ValueError

If the C call returns any other non-zero status. The exception message is set_carrier_freq_hz failed, with the return code appended (gh-869).

Examples:

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> a = Acquisition(code, sample_rate=1e7, chip_rate=5e6, symbol_rate=2700.0,
...                 cn0_dbhz=45.0, doppler_uncertainty=50e3)
>>> a.carrier_freq_hz
0.0
>>> a.set_carrier_freq_hz(2.5e9)
>>> a.carrier_freq_hz
2500000000.0

set_threads

set_threads(n: int) -> None

Set how many threads the searcher fans its tiles across (docs/design/acq-surface.md §13: a roll per thread on persistent workers).

A continuous engine is created with a pool of the machine's online cores when it has more than one tile; a burst engine, and a single-tile one, run serially. This sets the count: 0 auto-selects the online core count, 1 runs everything on the calling thread, n runs on n workers (the caller included) -- but never more than window_bins: every fan hands out one tile a task, so a worker past that only wakes and waits. The request is kept, and every regrid re-applies it, so an engine narrowed to one tile and widened again runs on min(n, tiles), not on the 1 the narrow grid ran on. The workers are created here, once, and parked between pushes; nothing is created per push. The surface is bit-identical at every count -- the tiles are independent after the one forward transform and each writes its own rows -- so this changes the cost of a push and nothing about its result. Setup path, never hot; not while another thread is inside push().

Parameters:

Name Type Description Default
n int

Thread count; 0 = online cores, 1 = serial.

required

Raises:

Type Description
ValueError

If the C call returns a non-zero status. The exception message is set_threads failed, with the return code appended (gh-869).

Examples:

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(
...     PN(poly=mls_poly(9), seed=1, length=9).generate(511), np.uint8)
>>> a = Acquisition(code, sample_rate=2e6, chip_rate=1e6, cn0_dbhz=50.0,
...                 doppler_uncertainty=4000.0)
>>> a.threads >= 1               # at most one worker a tile
True
>>> a.set_threads(1)
>>> a.threads
1

set_telemetry

set_telemetry(
    tlm: object | None, prefix: str, decim: int = 1
) -> None

Attach (or detach) a telemetry context and register the engine's probes on it (docs/design/async-dsss-receiver.md §2.4).

Registers ten probes, emitted once per DECIDED dwell (a coherent dump, or the dwell that completes n_noncoh looks) and further thinned by decim: ".stat" (the dwell's test statistic — the strongest cell against the CFAR reference, in the units the gate is set in), ".gate" (that gate: threshold on the coherent path, eta_nc on the non-coherent one — plotted together they show exactly where a hit fired), ".noise" (the CFAR reference noise_est), ".peak" (the strongest cell's raw value), ".row" and ".col" (its native Doppler row and code-phase column — a surface coordinate, not a physical unit; dp_correlator_surface_doppler_hz() and dp_correlator_surface_chip_phase() convert), ".n_peaks" (picks in the dwell, held twins included), ".n_held" (picks held as same-code-phase twins rather than listed, §7.1), ".conc" (the strongest pick's concentration — see peak_conc: its main lobe's power over its whole column's, near 1 for one clean emitter even when it straddles two tiles, about 0.5 when a data transition splits it into twins two or more tiles away, lower still when a coherent block straddles data — the discriminator between one emitter's splatter and a second emitter) and ".hit" (1 when the gate fired). Passing NULL detaches. Setup path, never hot; the context is borrowed and must outlive the attachment (SPSC rules in dp_tlm/dp_tlm_core.h).

Parameters:

Name Type Description Default
tlm object | None

Telemetry context to attach, or NULL to detach.

required
prefix str

Probe-name prefix, e.g. "acq" or "ch0.acq".

required
decim int

Emit every decim-th decided dwell; >= 1.

1

Raises:

Type Description
ValueError

If the C call returns a non-zero status. The exception message is set_telemetry failed, with the return code appended (gh-869).

Examples:

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.telemetry import Telemetry
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(
...     PN(poly=mls_poly(9), seed=1, length=9).generate(511), np.uint8)
>>> a = Acquisition(code, sample_rate=2e6, chip_rate=1e6, cn0_dbhz=50.0)
>>> tlm = Telemetry(1 << 12)
>>> a.set_telemetry(tlm, "acq")
>>> sorted(tlm.probe_names)[:3]
['acq.col', 'acq.conc', 'acq.gate']
>>> x = np.zeros(a.n_noncoh * 511 * 2 * 3, dtype=np.complex64)
>>> _ = a.push(x)
>>> len(tlm.read()) % 10      # ten records per decided dwell
0

surface

surface(out: NDArray[float32]) -> int

The last decided dwell's surface, in the gate's own units.

Copies the surface the last dwell was decided on into out, row-major surface_rows (Doppler: tiles, or interpolated slow-time rows) by lag_bins (code phase), every cell divided by the same CFAR reference the gate used — so a cell reads as its own test statistic, to a float rounding (the SIMD build's fast-math may take a reciprocal in this loop and a divide in the gate's), and the gate (threshold, or eta_nc on the non-coherent path) is a flat plane on a plot. The engine keeps this only while keep_surface is set (a caller sets it, or dp_correlator_set_surface_sink() does): set it, push, then read. surface_at says which dwell it is; a time-decimated record is the caller reading every k-th dwell, or a sink with decim.

Parameters:

Name Type Description Default
out NDArray[float32]

At least surface_rows * lag_bins floats.

required

Returns:

Type Description
int

Cells written (surface_rows * lag_bins), or 0 when no dwell has been decided with keep_surface set, or out is too small.

Examples:

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(
...     PN(poly=mls_poly(9), seed=1, length=9).generate(511), np.uint8)
>>> a = Acquisition(code, sample_rate=2e6, chip_rate=1e6, cn0_dbhz=50.0)
>>> a.keep_surface = 1
>>> x = np.zeros(a.n_noncoh * 511 * 2, dtype=np.complex64)
>>> _ = a.push(x)
>>> s = np.empty(a.surface_rows * a.lag_bins, dtype=np.float32)
>>> a.surface(s) == s.size
True
>>> a.code_bins, a.lag_bins  # 1022 is 2 x 7 x 73: read on 1024 lags
(1022, 1024)
>>> s.reshape(a.surface_rows, a.lag_bins).shape == (a.surface_rows, 1024)
True

surface_doppler_hz

surface_doppler_hz(out: NDArray[float64]) -> int

The surface's Doppler axis: the frequency of each row, in Hz.

One value per surface row, the fold and scale a hit's doppler_hz_est uses (dp_fftfreq_index() times doppler_res_hz, on the interpolated grid where the slow-time axis is interpolated), so a plot of dp_correlator_surface() carries the same axis a DetectionEvent reports on.

Parameters:

Name Type Description Default
out NDArray[float64]

At least surface_rows doubles.

required

Returns:

Type Description
int

Values written (surface_rows), or 0 if out is too small.

Examples:

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(
...     PN(poly=mls_poly(9), seed=1, length=9).generate(511), np.uint8)
>>> a = Acquisition(code, sample_rate=2e6, chip_rate=1e6, cn0_dbhz=50.0,
...                 doppler_uncertainty=4000.0)
>>> f = np.empty(a.surface_rows, dtype=np.float64)
>>> a.surface_doppler_hz(f) == a.surface_rows
True
>>> bool(f[0] == 0.0 and f.min() < 0.0 < f.max())
True

surface_chip_phase

surface_chip_phase(out: NDArray[float64]) -> int

The surface's code-phase axis: the chip phase of each column.

One value per surface column, in chips, the same mapping dp_correlator_build_handoff() applies to a hit's col — so a plotted peak sits at the chip phase the DetectionEvent would carry.

Parameters:

Name Type Description Default
out NDArray[float64]

At least lag_bins doubles.

required

Returns:

Type Description
int

Values written (lag_bins), or 0 if out is too small.

Examples:

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(
...     PN(poly=mls_poly(9), seed=1, length=9).generate(511), np.uint8)
>>> a = Acquisition(code, sample_rate=2e6, chip_rate=1e6, cn0_dbhz=50.0)
>>> c = np.empty(a.lag_bins, dtype=np.float64)
>>> a.surface_chip_phase(c) == a.lag_bins
True
>>> bool(c[0] == 0.0 and c[1] == 511 - 511 / 1024)
True

surface_complex

surface_complex(out: NDArray[complex64]) -> int

The last decided dwell's surface, complex: amplitude and carrier phase per cell, before the magnitude the gate reads.

Copies the coherent sum the last dwell was decided on into out, row-major surface_rows x lag_bins like dp_correlator_surface(), in the correlation's own units rather than the gate's. A cell's phase is the carrier at the block's middle, relative to its tile's centre; its neighbours along the code axis are complex early and late arms, so a tracker can form the coherent discriminator Re(conj(P) (L - E)) / |P|^2, which the magnitude surface cannot (docs/design/async-dsss-receiver-measurements.md §12.21). Coherent path only: a non-coherent dwell (n_noncoh > 1) is a power sum with no phase, and reads 0.

Parameters:

Name Type Description Default
out NDArray[complex64]

At least surface_rows * lag_bins complex floats.

required

Returns:

Type Description
int

Cells written (surface_rows * lag_bins), or 0 when no dwell has been decided, the path is non-coherent, or out is too small.

Examples:

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(PN(poly=mls_poly(5), seed=1,
...                      length=5).generate(31)).astype(np.uint8)
>>> s0 = np.repeat(np.where(code & 1, -1.0, 1.0), 4).astype(
...     np.complex64)
>>> a = Acquisition(code, sample_rate=4e6, chip_rate=1e6, cn0_dbhz=70.0)
>>> a.n_noncoh                 # one look: the dwell is the coherent dump
1
>>> _ = a.push(np.roll(s0, 17).astype(np.complex64))
>>> sc = np.empty(a.surface_rows * a.lag_bins, dtype=np.complex64)
>>> a.surface_complex(sc) == sc.size
True
>>> int(np.argmax(np.abs(sc)) % a.lag_bins)   # the peak's lag column
18

block_prompt

block_prompt(
    tile: int, col: int, out: NDArray[complex64]
) -> int

One cell's column of the last whole block: the per-epoch complex correlations at a code phase, the despread stream at epoch rate.

The block-coherent engine gathers every tile's correlation row for coherent_bins epochs before its slow-time transform (file doc, docs/design/acq-surface.md §4). This copies the coherent_bins values at column col of tile tile into out, in epoch order: each epoch's complex prompt at that code phase, rolled to the tile's centre frequency and shifted to the block's middle by the tile's code-rate hypothesis, so the phase is continuous along the column for an emitter at the tile's centre and rotates at its offset from it. At an emitter's cell this is what a despreader produces, one value per epoch, phase included (docs/design/async-dsss-receiver-measurements.md §12.21). Valid once a block is whole, until the next epoch is pushed.

Parameters:

Name Type Description Default
tile int

Tile index, 0 … window_bins-1 (native FFT order, the order the surface's rows are cut in).

required
col int

Code-phase column, 0 … lag_bins-1.

required
out NDArray[complex64]

At least coherent_bins complex floats.

required

Returns:

Type Description
int

Values written (coherent_bins), or 0 at coherent_bins == 1 (no block is gathered), while a block is partial, for an index out of range, or when out is too small.

Examples:

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(PN(poly=mls_poly(5), seed=1,
...                      length=5).generate(31)).astype(np.uint8)
>>> s0 = np.repeat(np.where(code & 1, -1.0, 1.0), 4).astype(
...     np.complex64)
>>> b = Acquisition(code, sample_rate=4e6, chip_rate=1e6, cn0_dbhz=60.0,
...                 doppler_uncertainty=40e3, code_only_epochs=7)
>>> b.coherent_bins                    # (7 + 1) // 2
4
>>> blk = np.tile(np.roll(s0, 17), b.coherent_bins).astype(np.complex64)
>>> _ = b.push(blk)
>>> p = np.empty(b.coherent_bins, dtype=np.complex64)
>>> b.block_prompt(0, 17, p) == b.coherent_bins
True
>>> bool(np.abs(p).min() > 0.99 * np.abs(p).max())  # every epoch's prompt
True
>>> b.block_prompt(0, 17, np.empty(1, dtype=np.complex64))  # too small
0

block_raw

block_raw(out: NDArray[complex64]) -> int

The last whole block's raw samples, as pushed.

Copies the coherent_bins * code_bins samples the block-coherent engine gathered for its last whole block into out, epoch by epoch in stream order — the samples dp_correlator_push() consumed, untouched. Kept for the tile-edge re-ask (search_resolve_tile_alias()); exposed so a tracker can re-correlate them at any code phase, rate or symbol boundary the engine's own grid does not have — a symbol-rate despreader at the tracked timing runs on exactly this (docs/design/async-dsss-receiver-measurements.md §12.21). Valid once a block is whole, until the next epoch is pushed.

Parameters:

Name Type Description Default
out NDArray[complex64]

At least coherent_bins * code_bins complex floats.

required

Returns:

Type Description
int

Samples written (coherent_bins * code_bins), or 0 at coherent_bins == 1, while a block is partial, or when out is too small.

Examples:

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(PN(poly=mls_poly(5), seed=1,
...                      length=5).generate(31)).astype(np.uint8)
>>> s0 = np.repeat(np.where(code & 1, -1.0, 1.0), 4).astype(
...     np.complex64)
>>> b = Acquisition(code, sample_rate=4e6, chip_rate=1e6, cn0_dbhz=60.0,
...                 doppler_uncertainty=40e3, code_only_epochs=7)
>>> blk = np.tile(np.roll(s0, 17), b.coherent_bins).astype(np.complex64)
>>> _ = b.push(blk)
>>> raw = np.empty(b.coherent_bins * b.code_bins, dtype=np.complex64)
>>> b.block_raw(raw) == raw.size
True
>>> bool(np.array_equal(raw, blk))      # the samples as pushed
True

state_bytes

state_bytes() -> int

Size in bytes of this object's serialized state.

The exact length get_state returns and set_state requires. It depends on how the object was constructed (state arrays are sized at construction), so read it from the instance rather than assuming a constant.

Raises RuntimeError if the Acquisition has already been destroyed.

Returns:

Type Description
int

Byte length of one serialized state blob.

get_state

get_state() -> bytes

Serialize this object's mutable state to bytes.

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

The blob is opaque and always state_bytes() long. Its layout is an implementation detail of the C core and is not a stable format across builds.

Raises RuntimeError if the Acquisition has already been destroyed.

Returns:

Type Description
bytes

Opaque snapshot, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

Restore mutable state from a get_state() blob.

Overwrites the live state in place, including any value a setter changed after construction. Construction parameters are not restored: restore into an object built the same way. Length is validated against state_bytes() before the blob is handed to the C core, which may reject it too: a core may check some construction parameters and refuse a blob built differently.

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

Parameters:

Name Type Description Default
blob bytes

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

required

Raises:

Type Description
MemoryError

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

ValueError

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

destroy

destroy() -> None

Release the underlying C resources immediately.

Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this to release them at a definite point instead, or use the object as a context manager, which calls it on exit.

Idempotent: calling it again on an already-released object does nothing. Every other method raises RuntimeError once it has run.

__enter__

__enter__() -> Acquisition

Enter a context manager, returning this object.

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

Returns:

Type Description
Acquisition

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

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.

...

BurstAcquisition — the burst front door to acquisition

BurstAcquisition is the burst front door to the same engine. It takes one period of any repeated complex preamble as samples, plus their rate fs, and sizes the coherent depth: the smallest number of repetitions whose Pd for the whole burst meets pd, with no non-coherent looks. Every dwell whose statistic clears the gate reports a hit, so one burst typically produces several; each is the same 9-tuple as Acquisition's, except that chip_phase counts preamble samples, one chip per sample. The two front doors differ in what they take (preamble samples against a code) and in how they size the search. See the acquisition guide for a worked example on a non-PN preamble.

BurstAcquisition

Create a burst-mode acquisition engine for any repeated preamble, given as its samples (forwards to dp_correlator_create_burst() -- see its doc comment in correlator_core.h for the full physics).

Parameters:

Name Type Description Default
preamble NDArray[complex64]

One period of the preamble, preamble_len samples; not all zero, every sample finite.

required
reps int

Max coherent repetitions (>= 1).

1
fs float

Sample rate in Hz (> 0); 1 for normalized units.

1.0
cn0_dbhz float

Design carrier-to-noise density in dB-Hz, of the preamble's mean power: any finite value, or NaN (DP_CORRELATOR_CN0_NONE) for no design point -- size for the whole preamble.

...
doppler_uncertainty float

One-sided Doppler search half-range in Hz.

0.0
pfa float

Target system false-alarm probability (0,1).

1e-3
pd float

Target detection probability (0,1).

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

The CFAR reference: the statistic the noise floor is estimated with, which the detection threshold scales. One of "mean", "median", "min", "max".

"mean"
doppler_rate float

Doppler rate in Hz/s (>= 0) that caps the coherent depth at f_epoch/sqrt(2*doppler_rate) repetitions (doppler#1482); 0 is no bound.

0.0
rate_hypotheses int

Doppler-rate hypotheses to search (0 = auto, else 1 to DP_CORRELATOR_RATE_HYPS_MAX); 1 searches none. Above 1 the engine COMPENSATES the rate rather than only capping the depth: doppler_rate becomes the span [-R, +R], the coherent block is de-chirped at this many rates across it, and the depth is bounded by the residual R/(n - 1) -- see dp_correlator_create_burst(), which owns the semantics, including 0 = auto (the engine chooses the count; read it back from engine->rate_hyps). Needs a non-zero doppler_rate.

1

Raises:

Type Description
ValueError

If construction fails. The exception message is ``BurstAcquisition: invalid parameter (need a non-empty preamble with finite, non-zero energy, reps >= 1, fs > 0, cn0_dbhz finite or NaN, doppler_uncertainty

= 0, doppler_rate >= 0, 0 <= rate_hypotheses <= 64 (0 = auto; doppler_rate > 0 unless 1), 0 < pfa < 1, 0 < pd < 1)``.

Warns:

Type Description
UserWarning

Emitted after construction when underpowered holds: BurstAcquisition is under-powered: pd_burst < pd at this reps/cn0_dbhz. Raise reps or cn0_dbhz, or narrow doppler_uncertainty..

Examples:

>>> import numpy as np
>>> from doppler.cvt import bin_to_nrz
>>> from doppler.acquire import BurstAcquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(PN(poly=mls_poly(5), seed=1,
...                      length=5).generate(31)).astype(np.uint8)
>>> nrz = np.zeros(31, np.float32)
>>> _ = bin_to_nrz(code & 1, nrz)
>>> s0 = np.repeat(nrz, 4).astype(np.complex64)    # 4 samples a chip
>>> burst = np.tile(np.roll(s0, 17), 24)
>>> b = BurstAcquisition(s0, reps=8, fs=4e6, cn0_dbhz=60.0)
>>> b.push(burst)[0][:2]      # detects (Doppler bin, delay in samples)
(0, 17)

Any repeated preamble is searched the same way -- here a 127-sample Zadoff-Chu sequence, in normalized units:

>>> k = np.arange(127)
>>> zc = np.exp(-1j * np.pi * 5 * k * (k + 1) / 127).astype(
...     np.complex64)
>>> z = BurstAcquisition(zc, reps=8)
>>> z.code_bins                # samples per repetition
127
>>> z.push(np.tile(np.roll(zc, 40), 10))[0][:2]
(0, 40)

A carrier that sweeps caps the coherent depth; searching Doppler-rate hypotheses compensates it instead, and buys the depth back:

>>> import warnings
>>> def make(**kw):
...     with warnings.catch_warnings():
...         warnings.simplefilter("ignore")   # under-powered, below
...         return BurstAcquisition(zc, reps=16, fs=1.0, cn0_dbhz=-12.5,
...                                 doppler_rate=9e-7, **kw)
>>> make().doppler_bins, make().underpowered      # the rate caps it
(5, True)
>>> c = make(rate_hypotheses=9)
>>> c.rate_hypotheses, c.doppler_bins, c.underpowered
(9, 9, False)

max_peaks property

max_peaks: int

The peak list's capacity per dwell (1 = the classic gated maximum); set with set_max_peaks().

code_bins property

code_bins: int

Delay hypotheses searched: one repetition of the preamble, in samples (len(preamble)).

doppler_bins property

doppler_bins: int

Doppler hypotheses searched: the coherent depth chosen (the slow-time FFT length in code reps, <= reps), times the window-tile count when doppler_uncertainty exceeds the native span -- a tile keeps its own coherent depth when a design cn0_dbhz asks for one, and a hit's doppler_bin indexes all of window_bins * depth rows. This is the count to fold a hit's bin over (bin_to_signed(bin, doppler_bins) * doppler_res_hz is the signed Doppler); it is dp_correlator_grid_bins(), and the same number BurstCapture.doppler_bins reads.

reps property

reps: int

Max coherent code repetitions (the coherence ceiling).

n_noncoh property

n_noncoh: int

Non-coherent looks per detection (1 = pure coherent).

ring_cap property

ring_cap: int

Input ring capacity in complex samples.

noise_lo property

noise_lo: int

First CFAR reference bin (inclusive).

noise_hi property

noise_hi: int

Last CFAR reference bin (inclusive).

threshold property

threshold: float

CFAR gate on the test statistic (coherent path).

eta property

eta: float

Raw per-cell Rayleigh amplitude threshold.

eta_nc property

eta_nc: float

Non-coherent CFAR threshold (order-N_nc Marcum).

pfa_cell property

pfa_cell: float

Šidák per-cell false-alarm probability over the searched cells.

pd_predicted property

pd_predicted: float

Predicted Pd at cn0_dbhz and the chosen grid: the average Pd over the straddle priors (slow-time scalloping, intra-segment rotation, code-phase sample offset - quadrature over uniform priors), matching what the Monte-Carlo characterization measures rather than the on-grid best case.

pd_burst property

pd_burst: float

Predicted Pd of one burst of reps repetitions at cn0_dbhz: the dwells are aligned to the stream, so the preamble lands at a uniform offset and spans about reps/doppler_bins of them, whole or partial, and the burst is detected when any one is. The number the sizer meets pd with and underpowered is set from; pd_predicted is one aligned dwell. NaN with no design cn0_dbhz or with n_noncoh > 1.

psl_db property

psl_db: float

The preamble's peak sidelobe level, dB: the largest lag of its periodic autocorrelation OUTSIDE the mainlobe, relative to the peak. A detection's sidelobes sit this far below it, at delays the peak list's exclusion does not cover, so a burst clearing the threshold by more than -psl_db also lists its own sidelobe (with max_peaks > 1), and a strong burst's sidelobe can mask a weak one there. -29.8 dB for a 31-chip m-sequence (1/31); -inf for a perfect sequence such as Zadoff-Chu. Fixed at construction.

straddle_loss property

straddle_loss: float

Mean amplitude derating of the correlation peak from grid straddle (slow-time Doppler scalloping x intra-segment rotation x code-phase sample offset, each averaged over a uniform prior) - a diagnostic summary; 20*log10(straddle_loss) is the loss in dB. Sizing and pd_predicted average Pd itself over the priors (Pd at this mean amplitude would overstate the mean Pd).

fs property

fs: float

Sample rate (Hz) the preamble was given at.

doppler_rate property

doppler_rate: float

Doppler rate (Hz/s) the coherent depth is bounded against: coherent_bins <= f_epoch/sqrt(2*doppler_rate). 0 is no bound.

rate_hypotheses property

rate_hypotheses: int

Doppler-rate hypotheses the engine searches, as given at construction, or the count the engine chose when it was built with 0 (auto: the fewest whose residual stops limiting the depth); 1 is none -- doppler_rate then only caps the coherent depth. Above 1 doppler_rate is the span of the hypotheses and the depth is bounded by the residual doppler_rate/(rate_hypotheses - 1).

cn0_dbhz property

cn0_dbhz: float

Carrier-to-noise density used to size the search (dB-Hz).

doppler_span_hz property

doppler_span_hz: float

Native unambiguous Doppler half-range = +/- fs/(2*len(preamble)) Hz.

doppler_res_hz property

doppler_res_hz: float

Doppler bin width = fs/(len(preamble)*doppler_bins) Hz.

pd property

pd: float

Target detection probability.

underpowered property

underpowered: bool

True when pd_burst < pd -- the search cannot meet the target pd at this cn0_dbhz and geometry. The engine still builds a best-effort grid rather than failing; because C cannot raise a Python warning from a successful create, construction also emits a UserWarning in this case.

reset

reset() -> None

Drain the input ring and reset the coherent accumulator.

Forwards to dp_correlator_reset() on the embedded engine: discards any buffered samples that have not yet completed a frame and clears the non-coherent power accumulator and dwell bookkeeping, so the next push() begins a fresh search from an empty ring. Construction parameters are untouched.

Examples:

>>> import numpy as np
>>> from doppler.acquire import BurstAcquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(PN(poly=mls_poly(5), seed=1,
...                      length=5).generate(31)).astype(np.uint8)
>>> s0 = np.repeat(np.where(code & 1, -1.0, 1.0), 4).astype(
...     np.complex64)
>>> burst = np.tile(np.roll(s0, 17), 24).astype(np.complex64)
>>> b = BurstAcquisition(s0, reps=8, fs=4e6, cn0_dbhz=60.0)
>>> _ = b.push(burst[:100])   # a partial frame, buffered mid-stream
>>> b.reset()                 # drop it before it can bias a detection
>>> b.push(burst)[0][:2]      # (Doppler bin, delay in samples)
(0, 17)

push

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

Stream raw samples; emit one event per CFAR dump above threshold.

Forwards to dp_correlator_push() on the embedded engine (see its doc comment in correlator_core.h for the framing/CFAR mechanics). Each event carries the peak's Doppler bin and code phase (the two search axes), its CFAR statistic, and an estimated C/N0 — see dp_correlator_hit_t.

Python's push() has room for 1024 events a call, with dp_correlator_push()'s rule for what a push that fills it loses.

Parameters:

Name Type Description Default
x complex

Raw input, interleaved CF32, n_in complex samples.

required

Returns:

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

Number of events written (0 … max_results).

Examples:

>>> import numpy as np
>>> from doppler.acquire import BurstAcquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(PN(poly=mls_poly(5), seed=1,
...                      length=5).generate(31)).astype(np.uint8)
>>> s0 = np.repeat(np.where(code & 1, -1.0, 1.0), 4).astype(
...     np.complex64)
>>> burst = np.tile(np.roll(s0, 17), 24).astype(np.complex64)
>>> b = BurstAcquisition(s0, reps=8, fs=4e6, cn0_dbhz=60.0)
>>> b.push(burst)[0][:2]      # (Doppler bin, delay in samples)
(0, 17)

configure_search_raw

configure_search_raw(
    doppler_bins: int, n_noncoh: int
) -> None

Pin the search grid directly, bypassing both auto-sizing searches -- the advanced escape hatch (mirrors Dll.configure_lock_raw/Costas.configure_lock). Resizes every buffer/plan that depends on the grid (the slow-time FFT, the code correlator, the reference, and every per-frame scratch buffer), re-derives the threshold ladder for the pinned grid from the same physics init used, and clears in-flight accumulation (ring contents, the non-coherent power accumulator, dwell bookkeeping) -- call between push() calls, never a substitute for one. Raises ValueError if doppler_bins is outside [1, reps] or n_noncoh is outside [1, 256] (the internal non-coherent-look safety-valve ceiling).

Forwards to dp_correlator_configure_search_raw() on the embedded engine (see its doc comment in correlator_core.h): resizes every grid-dependent buffer/plan, re-derives the threshold ladder for the pinned grid, and clears in-flight accumulation — call between push() calls, never a substitute for one.

Parameters:

Name Type Description Default
doppler_bins int

Coherent depth to pin, in [1, reps].

required
n_noncoh int

Non-coherent look count to pin, in [1, DP_CORRELATOR_N_NONCOH_SAFETY_CEILING].

required

Raises:

Type Description
ValueError

If the C call returns a non-zero status. The exception message is configure_search_raw failed, with the return code appended (gh-869).

Examples:

>>> import numpy as np
>>> from doppler.acquire import BurstAcquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(PN(poly=mls_poly(5), seed=1,
...                      length=5).generate(31)).astype(np.uint8)
>>> s0 = np.repeat(np.where(code & 1, -1.0, 1.0), 4).astype(
...     np.complex64)
>>> b = BurstAcquisition(s0, reps=8, fs=4e6, cn0_dbhz=60.0)
>>> b.configure_search_raw(doppler_bins=4, n_noncoh=2)  # pin the grid
>>> b.doppler_bins, b.n_noncoh
(4, 2)
>>> burst = np.tile(np.roll(s0, 17), 8).astype(np.complex64)
>>> b.push(burst)[0][:2]      # detects at the pinned grid
(0, 17)

set_max_peaks

set_max_peaks(n: int) -> None

How many peaks a dwell may report -- the peak list's capacity (docs/design/async-dsss-receiver.md section 7.1). One (the default) is the classic gated maximum. More lists every peak above the same gate, strongest first, with an exclusion zone of one Doppler bin by one chip around each (one emitter's main lobe, so its own shoulders are not the next peak) and the two-epoch rule for a peak at an already-listed code phase (a data transition inside the epoch splits one emitter into twins at its own code phase on other tiles; such a peak is held for one dwell and listed only if it is still there, at the same tile, on the next). Each listed peak is one record from push(), all of a dwell's sharing samples_consumed and noise_est; a held twin takes one of the n slots that dwell but is not reported. The threshold does not change with n. Raises ValueError outside 1..64. Clears the held candidates.

Forwards to dp_correlator_set_max_peaks() on the embedded engine (see its doc comment in correlator_core.h): one is the classic gated maximum; more is the list of docs/design/async-dsss-receiver.md §7.1 -- every peak above the same gate, strongest first, an exclusion zone of one Doppler bin by the reference's first autocorrelation null (one chip for a PN code) around each, and the two-epoch rule for a peak at an already-listed code phase. Each listed peak is one result from push().

Parameters:

Name Type Description Default
n int

1 … DP_CORRELATOR_MAX_PEAKS.

required

Raises:

Type Description
ValueError

If the C call returns a non-zero status. The exception message is set_max_peaks failed, with the return code appended (gh-869).

Examples:

>>> import numpy as np
>>> from doppler.acquire import BurstAcquisition
>>> code = (np.arange(31) * 5 % 2).astype(np.uint8)
>>> s0 = np.repeat(np.where(code & 1, -1.0, 1.0), 4).astype(
...     np.complex64)
>>> b = BurstAcquisition(s0, reps=8, fs=4e6, cn0_dbhz=60.0)
>>> b.set_max_peaks(4)
>>> b.max_peaks
4

state_bytes

state_bytes() -> int

Size in bytes of this object's serialized state.

The exact length get_state returns and set_state requires. It depends on how the object was constructed (state arrays are sized at construction), so read it from the instance rather than assuming a constant.

Raises RuntimeError if the BurstAcquisition has already been destroyed.

Returns:

Type Description
int

Byte length of one serialized state blob.

get_state

get_state() -> bytes

Serialize this object's mutable state to bytes.

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

The blob is opaque and always state_bytes() long. Its layout is an implementation detail of the C core and is not a stable format across builds.

Raises RuntimeError if the BurstAcquisition has already been destroyed.

Returns:

Type Description
bytes

Opaque snapshot, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

Restore mutable state from a get_state() blob.

Overwrites the live state in place, including any value a setter changed after construction. Construction parameters are not restored: restore into an object built the same way. Length is validated against state_bytes() before the blob is handed to the C core, which may reject it too: a core may check some construction parameters and refuse a blob built differently.

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

Parameters:

Name Type Description Default
blob bytes

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

required

destroy

destroy() -> None

Release the underlying C resources immediately.

Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this to release them at a definite point instead, or use the object as a context manager, which calls it on exit.

Idempotent: calling it again on an already-released object does nothing. Every other method raises RuntimeError once it has run.

__enter__

__enter__() -> BurstAcquisition

Enter a context manager, returning this object.

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

Returns:

Type Description
BurstAcquisition

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

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.

...

BurstCapture — acquisition's output, turned into bursts

BurstCapture is the stage between a detector and whatever consumes a burst. BurstAcquisition reports an END anchor and a code phase that is a lag modulo one code period (for a Zadoff-Chu preamble, shifted by any Doppler the search did not resolve first), so it names the alignment within a preamble repetition and never which one — and a burst has a frame that begins in one specific repetition. BurstCapture resolves that (the refine stage), keeps the look-back needed to reach a start that has already gone past, and emits the burst's samples.

It stops there. Demodulating is BurstDemod's job, and DsssBurstReceiver is this plus that. Reach for BurstCapture directly when you want the bursts themselves: a recorder, an offline corpus, or a second consumer fanned out from one stream.

push() returns windows concatenated — burst i occupies burst_len samples at i*burst_len — and events() returns the matching record for each. It owns its acquisition engine rather than accepting detections from elsewhere, because a hit's samples_consumed is stream-absolute only for an engine fed continuously and never reset; see the design note for why that invariant cannot be delegated to a caller.

BurstCapture

Create a burst capture: acquisition, refine and retention behind one push().

Parameters:

Name Type Description Default
preamble NDArray[complex64]

One period of the preamble, preamble_len samples; not all zero, every sample finite. Read, not kept.

required
burst_len int

Samples in one burst -- what gets captured.

8192
reps int

Preamble repetitions (>= 1).

5
fs float

Sample rate, Hz (> 0); 1 for normalized units.

1.0
cn0_dbhz float

C/N0 the search is sized for, dB-Hz, of the preamble's mean power: any finite value, or NaN (DP_CORRELATOR_CN0_NONE) for no design point.

...
doppler_uncertainty float

Doppler search half-range, Hz (0 = native).

0.0
pfa float

Target false-alarm probability, in (0, 1).

1e-3
pd float

Target detection probability, in (0, 1).

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

The CFAR reference: the statistic the noise floor is estimated with, which the detection threshold scales. One of "mean", "median", "min", "max".

"mean"
doppler_rate float

Doppler rate, Hz/s (>= 0), that caps the acquisition's coherent depth at f_epoch/sqrt(2*doppler_rate) repetitions (doppler#1482); 0 is no bound. With rate_hypotheses > 1 it is the span instead.

0.0
rate_hypotheses int

Doppler-rate hypotheses to search (0 = auto, else 1 to DP_CORRELATOR_RATE_HYPS_MAX; auto picks the fewest the span needs, see dp_correlator_create_burst(), and dp_burst_capture_get_rate_hypotheses() reads the choice); 1 searches none and doppler_rate only caps the depth. Above 1 the capture COMPENSATES the rate: doppler_rate is the span [-R, +R], the acquisition de-chirps its coherent block at this many rates across it, and the depth is bounded by the RESIDUAL, R/(n - 1) -- deeper blocks, more sensitivity, about n times the per-dwell cost, and a false-alarm budget split over the hypotheses (dp_correlator_create_burst() owns the semantics). Refine follows the engine: its coherent sum over the preamble's periods tries the rates too, on a finer grid because it integrates the whole preamble, and its Doppler cells widen by the shift a rate puts between the detecting dwell's middle and the preamble's -- so a burst the engine found by rate is placed by rate. Needs a non-zero doppler_rate (the span). Measured: validate_acq_rate_pd.

1

Raises:

Type Description
ValueError

If construction fails. The exception message is ``BurstCapture: invalid parameter (need a non-empty preamble with finite, non-zero energy, reps

= 1, fs > 0, burst_len >= 1, cn0_dbhz finite or NaN, doppler_rate >= 0, 0 <= rate_hypotheses <= 64 (0 = auto; doppler_rate nonzero unless 1), 0 < pfa < 1, 0 < pd < 1)``.

Warns:

Type Description
UserWarning

Emitted after construction when underpowered holds: BurstCapture: the search cannot meet the requested pd at this cn0_dbhz and geometry (pd_burst < pd). It still builds a best-effort grid, so the symptom is bursts that are never captured rather than an error. Lower pd, raise cn0_dbhz, or give the preamble more repetitions..

Examples:

>>> import numpy as np
>>> from doppler.acquire import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> pre = np.repeat(np.where(code, -1.0, 1.0), 2).astype(np.complex64)
>>> cap = BurstCapture(pre, burst_len=512, reps=4, fs=2e6)
>>> cap.burst_len
512
>>> cap.retain_span == cap.refine_span + cap.burst_len
True

A carrier that sweeps caps the coherent depth at its rate; searching Doppler-rate hypotheses compensates it, and buys the depth back:

>>> import warnings
>>> k = np.arange(127)
>>> zc = np.exp(-1j * np.pi * 5 * k * (k + 1) / 127).astype(np.complex64)
>>> def make(**kw):
...     with warnings.catch_warnings():
...         warnings.simplefilter("ignore")   # under-powered, below
...         return BurstCapture(zc, burst_len=2432, reps=16, fs=1.0,
...                             cn0_dbhz=-12.5, doppler_rate=9e-7, **kw)
>>> make().doppler_bins, make().underpowered      # the rate caps it
(5, True)
>>> c = make(rate_hypotheses=9)
>>> c.rate_hypotheses, c.doppler_bins, c.underpowered
(9, 9, False)

preamble_start property

preamble_start: int

Stream-absolute sample index the most recent window's preamble starts at; 0 before the first window and after reset(). Never late is refine's design goal, not a guarantee (a window that began after the preamble has destroyed the burst): refine takes the strongest whole-period position over a range reaching both sides of the anchor, so at low C/N0 it can pick a wrong repetition, early or late.

doppler_hz_est property

doppler_hz_est: float

Folded/signed coarse Doppler estimate of the most recent window, Hz. Acquisition's own bin, mapped through dp_fftfreq -- the ONE home for that fold, because a consumer seeded on the wrong side of it is off by the full search span.

doppler_res_hz property

doppler_res_hz: float

Acquisition's native Doppler bin width = fs/(len(preamble)*coherent_bins), Hz. The width of doppler_hz_est: the estimate is that value +/- half of this.

cn0_dbhz_est property

cn0_dbhz_est: float

Estimated carrier-to-noise density of the most recent window (dB-Hz), backed out of the hit's test statistic. A LOWER BOUND: it tracks the true C/N0 while receiver noise dominates the CFAR estimate, then saturates at the code's own autocorrelation-sidelobe floor once the true C/N0 exceeds what this code and geometry can resolve -- a real ceiling, not a fault.

doppler_rate_hz_s property

doppler_rate_hz_s: float

Refine's winning Doppler-rate hypothesis for the most recent window, Hz/s: the rate whose de-chirp made the whole preamble's coherent sum largest at its start, from refine's grid over [-doppler_rate, +doppler_rate] (at least as dense as the acquisition's, and searched whenever a rate is declared, even with one acquisition hypothesis). 0 without a declared rate. A hypothesis, not a fit: it names the grid point nearest the carrier's rate well above the design point, and less often near it (validate_acq_rate_pd's event-rate rows).

burst_len property

burst_len: int

Samples in one emitted window -- the burst length this capture was built for, and the stride of a row in push()'s return.

refine_span property

refine_span: int

Coalescing window, in samples -- the reach over which two detections are ONE preamble.

Both sides of that test are burst STARTS (resolved code epochs), so this bounds start-to-start separation, NOT the dead air between bursts. The two differ by a whole burst, and reading it as dead air costs a caller real airtime for nothing: the gap actually required is max(0, refine_span - burst_len), which is 0 whenever a burst is longer than the refine reach (doppler#1085).

min_gap property

min_gap: int

Dead air to leave BETWEEN bursts, in samples — edge to edge, not start to start.

Derived rather than documented as a rule the caller has to apply: a detection's anchor is the code epoch of whichever frame detected, and acquisition's framing is not aligned to the preamble, so the last frame that can detect sits up to reps * code_period past the true start. CLAIM merges two anchors closer than refine_span, so a pair survives only when gap >= refine_span + reps*code_period - burst_len.

Zero is a real answer — a burst longer than refine_span + reps*P needs no gap for the claim rule's sake — but it does not mean zero is wise: a zero gap is a continuous stream rather than a burst link, and it measures 88% at a geometry where this reads 0.

Replaces the prose max(0, refine_span - burst_len), which was short by the whole detection-lag term: 32 samples against 528 at the C suite's geometry (doppler#1172).

retain_span property

retain_span: int

History kept per anchor, in samples -- the MINIMUM TRAILING CONTEXT.

refine_span plus one whole burst. A burst closer than this to the end of what has been pushed is held rather than emitted, because refine cannot yet see the samples it needs. Feed at least this many more, or the last burst of a capture never comes out.

underpowered property

underpowered: bool

True when the search cannot meet the requested pd at this cn0_dbhz and geometry — pd_burst < pd. The grid is still built, best-effort, so the symptom is bursts that are never captured rather than a failure. Construction also emits a UserWarning; this is the same fact as a value, for a caller that would rather ask than catch.

doppler_rate property

doppler_rate: float

Doppler rate (Hz/s) the acquisition's coherent depth is bounded against: doppler_bins <= f_epoch/sqrt(2*doppler_rate), so the carrier drifts less than half a slow-time row per block. 0 is no bound.

rate_hypotheses property

rate_hypotheses: int

Doppler-rate hypotheses the acquisition searches, as given at construction, or the count the acquisition chose when it was built with 0 (auto: the fewest whose residual stops limiting the depth); 1 is none -- doppler_rate then only caps the coherent depth. Above 1 doppler_rate is the span of the hypotheses, the depth is bounded by the residual doppler_rate/(rate_hypotheses - 1), and refine tries the rates too.

pd_predicted property

pd_predicted: float

Detection probability of ONE dwell of the sized grid lying wholly inside the preamble, at cn0_dbhz. A burst gets about reps/doppler_bins dwells at an alignment it does not choose, so compare pd_burst, not this, against the pd that was asked for.

pd_burst property

pd_burst: float

Detection probability of one burst at cn0_dbhz: every dwell its preamble spans, at a uniform alignment against the stream, any one detecting. The number behind underpowered, and the one to compare against the pd that was asked for. It models the ENGINE; what refine loses afterwards is not in the model. Measured on a Zadoff-Chu 127 x 8 preamble, the capture delivers at least pd_burst at a 0.6 design point and at 0.9, the default pd (validation report §2.8). NaN with no design cn0_dbhz.

psl_db property

psl_db: float

The preamble's peak sidelobe level, dB: the largest lag of its periodic autocorrelation OUTSIDE the mainlobe, relative to the peak. A detection's sidelobes sit this far below it, at delays the peak list's exclusion does not cover, so a burst clearing the threshold by more than -psl_db also lists its own sidelobe (with max_peaks > 1), and a strong burst's sidelobe can mask a weak one there. -29.8 dB for a 31-chip m-sequence (1/31); -inf for a perfect sequence such as Zadoff-Chu. Fixed at construction.

eta property

eta: float

Coherent detection gate: the normalised statistic a single-look decision must clear, from pfa spread across the search surface. In force when n_noncoh == 1.

eta_nc property

eta_nc: float

Non-coherent detection gate — the one in force when n_noncoh > 1, which a burst search never chooses on its own (it reads 0 unless configure_search_raw pins looks). Higher than eta for the same pfa, because combining looks costs the threshold what it buys in sensitivity.

straddle_loss property

straddle_loss: float

Correlation kept, worst case, by a burst landing BETWEEN grid points rather than on one. The search is a finite grid in Doppler and code phase, so a real burst almost never sits on a hypothesis exactly; this is what that costs, and it is already priced into pd_predicted and pd_burst.

doppler_bins property

doppler_bins: int

Doppler hypotheses searched: the coherent depth chosen (the slow-time FFT length in code reps, <= reps), times the window-tile count when doppler_uncertainty exceeds the native span -- a tile keeps its own coherent depth when a design cn0_dbhz asks for one, and a hit's doppler_bin indexes all of window_bins * depth rows. This is the count to fold a hit's bin over (bin_to_signed(bin, doppler_bins) * doppler_res_hz is the signed Doppler); it is dp_correlator_grid_bins(), and the same number BurstAcquisition.doppler_bins reads. configure_search_raw pins it.

n_noncoh property

n_noncoh: int

Non-coherent looks combined per decision. Above 1 the object needs that many frames before it can decide at all, which is why a caller sweeping in short dwells has to pin it.

code_bins property

code_bins: int

Delay hypotheses per Doppler row: one repetition of the preamble, in samples (len(preamble)).

doppler_span_hz property

doppler_span_hz: float

Unambiguous Doppler half-range, ± this. Beyond it the per-segment integrate-and-dump's sinc rolloff suppresses the correlation, so a burst outside the span is not merely harder to find — it is nulled.

pending property

pending: int

Detections held because their burst window has NOT fully arrived.

push() deliberately emits nothing for these: a window is returned when it is complete, not when it is guessed at. What this exists for is the other end -- a caller closing a file or a socket while this is non-zero is discarding a burst that would have been captured, and every other read-back looks identical to "nothing was ever there".

dropped property

dropped: int

Samples of look-back discarded while a queued detection still needed them, lifetime: the part of a dead detection's span -- one whose history is gone, so it can never be emitted -- behind the ring's tail. A shadowed detection is not counted. On a live stream this stays 0: push() never refuses input, a moved anchor is a later one, and a re-armed refine reads no further back than the history kept for an unarrived detection at or ahead of it. A resume carries all the look-back, so what makes it non-zero is a blob that names history it does not hold. A LOST BURST each, not a statistic -- it survives reset().

n_bursts property

n_bursts: int

Windows emitted, lifetime.

push

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

Stream raw cf32 samples and get back the SAMPLES of every burst whose window has fully arrived, concatenated: burst i occupies burst_len samples starting at i*burst_len, and events() returns the matching record for each. Samples feed the embedded BurstAcquisition and are retained in a history ring; when a detection fires, the refine stage correlates one code period at each preamble position to recover the exact preamble start -- the one quantity acquisition structurally cannot report, since its code_phase is a lag modulo one code period -- and the window is emitted the moment its last sample has arrived. It stops there: what to DO with a burst (demodulate it, write it to a file, ship it to another process) is the caller's. An empty return is normal, not an error: it means no burst completed in this call. Accepts any block size -- the history ring is a contiguous window over the stream and is never reset between bursts, so a burst whose tail falls outside one call is completed by a later one.

Windows are concatenated: burst i occupies burst_len samples starting at i*burst_len, and events() returns the matching record for each. Every sample of x is consumed. An empty return is normal -- it means no burst completed in this call.

It aborts the process in two places, both a defect in this object and neither reachable from any input or from any blob set_state() accepts: a history ring with no room after trim (the bound in the implementation's trim proves room), and an acquisition call that took nothing while input was left (each call starts with its result empty, and a push with its result empty takes at least the next frame, or the rest as carry). Each stops rather than spinning forever -- and in a Python host it ends the interpreter.

Parameters:

Name Type Description Default
x NDArray[complex64]

Input samples, x_len long.

required
out NDArray[complex64] | None

Written with the completed windows; may be NULL to drop.

None

Returns:

Type Description
NDArray[complex64]

Samples written -- always a multiple of burst_len.

Examples:

>>> import numpy as np
>>> from doppler.acquire import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> pre = np.repeat(np.where(code, -1.0, 1.0), 2).astype(np.complex64)
>>> cap = BurstCapture(pre, burst_len=512, reps=4, fs=2e6)
>>> win = cap.push(np.zeros(4096, dtype=np.complex64))
>>> win.size % cap.burst_len        # whole windows, never a partial
0
>>> win.size                        # silence, so no burst completed
0

push_max_out

push_max_out(x_len: int) -> int

Upper bound on samples push() can return for x_len input.

Distinct bursts cannot overlap, so x_len samples complete at most x_len/burst_len + 1 of them, plus whatever is already queued.

Parameters:

Name Type Description Default
x_len int

Samples the next push() will be given.

required

Returns:

Type Description
int

(x_len/burst_len + 1 + queue capacity) * burst_len samples.

detections

detections(
    count: int = 1, out: NDArray[Any] | None = None
) -> NDArray[Any]

Every hit the search made in the last push(), unfiltered — before the claim rule coalesced the several detections of one preamble, and before the suppression window dropped the ones inside a burst already captured. So several rows can name one burst and a row can be a false alarm; that is the point. Each carries the STREAM-ABSOLUTE code epoch, which acquisition's own code_phase is not (it is a lag modulo one code period), plus the folded Doppler, the C/N0 lower bound and the CFAR statistic that gated it. Read events() instead for the bursts that survived and whose windows arrived. Valid until the next push(), reset() or set_state().

BEFORE the claim rule and the suppression window: several rows can name one preamble, and a row can be a false alarm. That is the point -- this is what acquisition FOUND, and events() is what survived. Valid until the next push(), reset() or set_state().

Parameters:

Name Type Description Default
count int

How many output samples to ask for. The call may return fewer; size an out= buffer with the matching _max_out() when you need the worst case.

1
out NDArray[Any] | None

Destination rows; may be NULL to copy nothing (the count is still returned).

None

Returns:

Type Description
NDArray[Any]

min(detections held, max_out): the rows copied, or with a NULL out the rows that would have been.

One row per burst_capture_detection_t record, with fields:

epoch : int Stream-absolute code epoch of the hit. doppler_hz : float Signed coarse Doppler, folded, Hz. cn0_dbhz : float C/N0 estimate from the hit, dB-Hz. test_stat : float The CFAR gating statistic, peak over noise. peak_mag : float Raw CFAR peak magnitude.

Examples:

>>> import numpy as np
>>> from doppler.acquire import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> pre = np.repeat(np.where(code, -1.0, 1.0), 2).astype(np.complex64)
>>> cap = BurstCapture(pre, burst_len=512, reps=4, fs=2e6)
>>> _ = cap.push(np.zeros(4096, dtype=np.complex64))
>>> # what the search found, against what became a burst
>>> len(cap.detections()) >= len(cap.events())
True

detections_max_out

detections_max_out(n: int) -> int

Raw detections available from the last push(). n is ignored.

Parameters:

Name Type Description Default
n int

Ignored: the count is the last push's, not a request.

required

Returns:

Type Description
int

Rows detections() can copy out now (0 before any push).

events

events(
    count: int = 1, out: NDArray[Any] | None = None
) -> NDArray[Any]

The event record for each burst the last push() returned. Row i describes the window at samples[i*burst_len ...] of that push. Valid until the next push(), reset() or set_state().

Row i describes the window at i*burst_len. Valid until the next push(), reset() or set_state().

Parameters:

Name Type Description Default
count int

How many output samples to ask for. The call may return fewer; size an out= buffer with the matching _max_out() when you need the worst case.

1
out NDArray[Any] | None

Destination rows; may be NULL to copy nothing (the count is still returned).

None

Returns:

Type Description
NDArray[Any]

min(windows of the last push, max_out): the rows copied, or with a NULL out the rows that would have been.

One row per burst_capture_event_t record, with fields:

preamble_start : int Refined stream position of the preamble. doppler_hz_est : float Signed coarse Doppler, Hz. doppler_res_hz : float Acquisition's native bin width, Hz. cn0_dbhz_est : float C/N0 estimate from the hit, dB-Hz. doppler_rate_hz_s : float Refine's winning rate hypothesis, Hz/s: the rate whose de-chirp made the whole preamble's coherent sum largest at preamble_start. One of refine's R grid points from -doppler_rate to +doppler_rate, spaced 2*doppler_rate/(R - 1); 0 without a declared rate (doppler_rate 0). Refine searches the grid whenever a rate is declared, with one acquisition hypothesis or many (doppler#2219). A hypothesis, not a fit, from the whole preamble rather than one dwell: it is the grid point nearest the carrier's rate in every event 23 dB over the design point, on or off the grid. At the design point it is in 82% to 89% of events whose rate is ON a grid point, but in 51% of those 0.4 of a step off one, where the neighbouring point is only 0.6 of a step away (validate_acq_rate_pd's event-rate rows; docs/design/burst-capture.md). Appended last, so positional initialisers do not shift (doppler#2219).

Examples:

>>> import numpy as np
>>> from doppler.acquire import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> pre = np.repeat(np.where(code, -1.0, 1.0), 2).astype(np.complex64)
>>> cap = BurstCapture(pre, burst_len=512, reps=4, fs=2e6)
>>> win = cap.push(np.zeros(4096, dtype=np.complex64))
>>> len(cap.events()) == win.size // cap.burst_len
True

events_max_out

events_max_out(n: int) -> int

Records available from the last push(). n is ignored.

Parameters:

Name Type Description Default
n int

Ignored: the count is the last push's, not a request.

required

Returns:

Type Description
int

Event rows events() can copy out now: one per window returned.

configure_search_raw

configure_search_raw(
    doppler_bins: int, n_noncoh: int
) -> None

Pin the embedded BurstAcquisition's search grid directly, bypassing the auto-sizing -- the escape hatch for a caller who wants a specific (doppler_bins, n_noncoh). Forwards to the engine unchanged.

The escape hatch for a caller who wants a specific (doppler_bins, n_noncoh). Forwards to the engine, with one refusal of this object's own: a grid whose anchor can lag the preamble by more than refine reaches -- n_noncoh * doppler_bins code periods against k_lo -- is rejected rather than accepted and silently mis-refined. Acquisition stamps a hit at the end of the LAST accumulated look, so every look past the one holding the preamble moves the anchor a whole frame later; a burst has one frame of preamble, so n_noncoh = 1 is the grid a capture wants and the sizer now always picks (doppler#1181).

Parameters:

Name Type Description Default
doppler_bins int

Coherent depth to pin (see dp_correlator_configure_search_raw()).

required
n_noncoh int

Non-coherent looks to pin; n_noncoh * doppler_bins must not exceed the refine reach.

required

Raises:

Type Description
ValueError

If the C call returns a non-zero status. The exception message is configure_search_raw failed, with the return code appended (gh-869).

Examples:

>>> import numpy as np
>>> from doppler.acquire import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> pre = np.repeat(np.where(code, -1.0, 1.0), 2).astype(np.complex64)
>>> cap = BurstCapture(pre, burst_len=512, reps=4, fs=2e6)
>>> cap.configure_search_raw(4, 1)   # 4 Doppler bins, coherent only

release

release(i: int) -> None

Give back the span window i of the last push() claimed. An emitted window owns its whole span: detections inside it are the payload firing against the acquisition code, so they are HELD rather than reported. A consumer that knows better -- a demodulator whose CRC failed -- calls this for that window, and the held detections are searched again on the next push(). Unreleased, they are dropped when the next push() begins. Raises ValueError if i is not a window of the last push().

An emitted window owns its whole span: a detection inside it is the payload firing against the acquisition code, not a new burst, so it is HELD rather than reported. Whether the window WAS a burst is a verdict this object cannot reach -- it stops at samples; error detection, whatever form the frame gives it, is the consumer's -- so a consumer that knows better calls this for that window, and the held detections are searched again on the next push(). Unreleased, they are dropped when the next push() begins, which is exactly the behaviour a consumer with no verdict always had.

What it prevents (doppler#1181): a spurious window ending just after a real burst begins used to swallow that burst's first detections -- the receiver's own design says only a DECODED burst may own a span (§10.3, doppler#1004), and the capture underneath had been owning it on emission.

Must be called BEFORE the next push(): i indexes THIS push's windows.

Parameters:

Name Type Description Default
i int

Window index from the last push().

required

Raises:

Type Description
ValueError

If the C call returns a non-zero status. The exception message is release failed, with the return code appended (gh-869).

Examples:

>>> import numpy as np
>>> from doppler.acquire import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> pre = np.repeat(np.where(code, -1.0, 1.0), 2).astype(np.complex64)
>>> cap = BurstCapture(pre, burst_len=512, reps=4, fs=2e6)
>>> _ = cap.push(np.zeros(4096, dtype=np.complex64))
>>> cap.release(0)   # no window 0 in a quiet push
Traceback (most recent call last):
  ...
ValueError: release failed (rc=-4)

reset

reset() -> None

Return to the searching state: resets the embedded acquisition, drops the history ring's contents, clears every queued detection and every read-back, so a fresh stream cannot inherit the previous one's position. Construction parameters are untouched.

Resets the embedded acquisition, rewinds the history ring, clears every queued detection and every read-back. Construction parameters are untouched; dropped and n_bursts deliberately survive, because a lost burst stays lost and a lifetime count is a lifetime count.

Examples:

>>> import numpy as np
>>> from doppler.acquire import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> pre = np.repeat(np.where(code, -1.0, 1.0), 2).astype(np.complex64)
>>> cap = BurstCapture(pre, burst_len=512, reps=4, fs=2e6)
>>> cap.push(np.zeros(4096, dtype=np.complex64)).size
0
>>> cap.reset()
>>> cap.pending
0

state_bytes

state_bytes() -> int

Size in bytes of this object's serialized state.

The exact length get_state returns and set_state requires. It depends on how the object was constructed (state arrays are sized at construction), so read it from the instance rather than assuming a constant.

Raises RuntimeError if the BurstCapture has already been destroyed.

Returns:

Type Description
int

Byte length of one serialized state blob.

get_state

get_state() -> bytes

Serialize this object's mutable state to bytes.

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

The blob is opaque and always state_bytes() long. Its layout is an implementation detail of the C core and is not a stable format across builds.

Raises RuntimeError if the BurstCapture has already been destroyed.

Returns:

Type Description
bytes

Opaque snapshot, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

Restore mutable state from a get_state() blob.

Overwrites the live state in place, including any value a setter changed after construction. Construction parameters are not restored: restore into an object built the same way. Length is validated against state_bytes() before the blob is handed to the C core, which may reject it too: a core may check some construction parameters and refuse a blob built differently.

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

Parameters:

Name Type Description Default
blob bytes

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

required

Raises:

Type Description
MemoryError

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

ValueError

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

destroy

destroy() -> None

Release the underlying C resources immediately.

Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this to release them at a definite point instead, or use the object as a context manager, which calls it on exit.

Idempotent: calling it again on an already-released object does nothing. Every other method raises RuntimeError once it has run.

__enter__

__enter__() -> BurstCapture

Enter a context manager, returning this object.

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

Returns:

Type Description
BurstCapture

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

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.

...

PersistentBurstCapture — the same capture, with the ring in a file

The look-back is essentially the whole checkpoint: at a 511-chip code, 5 repetitions and an 8029-symbol frame, BurstCapture.state_bytes() is 16.84 MB, and all but 135 kB of it is retained history. PersistentBurstCapture takes a path and backs the ring's pages with that file (MAP_SHARED), so the samples are the file's contents — there is no mirror buffer and no flush path. Two things follow: the blob drops to 135 kB (the acquisition engine's own state) because the history is already durable, and the history outlives the process, so a capture restored over the same file reaches back across a restart into a burst that began before it.

It is a view over the same core — the constructor differs and nothing else does — so every method and read-back is shared verbatim. A blob does not travel between the two flavours in either direction: state_bytes() differs, and accepting one for the other would resume a capture whose history was somewhere else.

PersistentBurstCapture

Create a capture whose look-back lives in a FILE.

Parameters:

Name Type Description Default
path str | PathLike

File to back the ring with; not NULL and not empty.

required
preamble NDArray[complex64]

One period of the preamble, preamble_len samples.

required
burst_len int

Samples in one burst -- what gets captured.

8192
reps int

Preamble repetitions (>= 1).

5
fs float

Sample rate, Hz (> 0).

1.0
cn0_dbhz float

C/N0 the search is sized for, dB-Hz: any finite value, or NaN (DP_CORRELATOR_CN0_NONE) for no design point.

...
doppler_uncertainty float

Doppler search half-range, Hz (0 = native).

0.0
pfa float

Target false-alarm probability, in (0, 1).

1e-3
pd float

Target detection probability, in (0, 1).

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

The CFAR reference: the statistic the noise floor is estimated with, which the detection threshold scales. One of "mean", "median", "min", "max".

"mean"
doppler_rate float

Doppler rate, Hz/s (>= 0), that caps the acquisition's coherent depth at f_epoch/sqrt(2*doppler_rate) repetitions (doppler#1482); 0 is no bound. With rate_hypotheses > 1 it is the span instead.

0.0
rate_hypotheses int

Doppler-rate hypotheses to search; see dp_burst_capture_create().

1

Raises:

Type Description
ValueError

If construction fails. The exception message is ``BurstCapture: invalid parameter (need a non-empty preamble with finite, non-zero energy, reps

= 1, fs > 0, burst_len >= 1, cn0_dbhz finite or NaN, doppler_rate >= 0, 0 <= rate_hypotheses <= 64 (0 = auto; doppler_rate nonzero unless 1), 0 < pfa < 1, 0 < pd < 1)``.

Examples:

>>> import numpy as np, tempfile, os
>>> from doppler.acquire import BurstCapture, PersistentBurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> pre = np.repeat(np.where(code, -1.0, 1.0), 2).astype(np.complex64)
>>> path = os.path.join(tempfile.mkdtemp(), "ring.cf32")
>>> cap = PersistentBurstCapture(path, pre, burst_len=512,
...                             reps=4, fs=2e6)
>>> ram = BurstCapture(pre, burst_len=512, reps=4, fs=2e6)
>>> _ = cap.push(np.zeros(4096, dtype=np.complex64))
>>> # the look-back is in the file, so the blob stops carrying the
>>> # ring: twice retain_span rounded up to a power of two, 8 B a sample
>>> ring = 1 << (2 * ram.retain_span - 1).bit_length()
>>> ram.state_bytes() - cap.state_bytes() == ring * 8
True
>>> os.path.getsize(path) >= ring * 8   # rounded up to a page
True

preamble_start property

preamble_start: int

Stream-absolute sample index the most recent window's preamble starts at; 0 before the first window and after reset(). Never late is refine's design goal, not a guarantee (a window that began after the preamble has destroyed the burst): refine takes the strongest whole-period position over a range reaching both sides of the anchor, so at low C/N0 it can pick a wrong repetition, early or late.

doppler_hz_est property

doppler_hz_est: float

Folded/signed coarse Doppler estimate of the most recent window, Hz. Acquisition's own bin, mapped through dp_fftfreq -- the ONE home for that fold, because a consumer seeded on the wrong side of it is off by the full search span.

doppler_res_hz property

doppler_res_hz: float

Acquisition's native Doppler bin width = fs/(len(preamble)*coherent_bins), Hz. The width of doppler_hz_est: the estimate is that value +/- half of this.

cn0_dbhz_est property

cn0_dbhz_est: float

Estimated carrier-to-noise density of the most recent window (dB-Hz), backed out of the hit's test statistic. A LOWER BOUND: it tracks the true C/N0 while receiver noise dominates the CFAR estimate, then saturates at the code's own autocorrelation-sidelobe floor once the true C/N0 exceeds what this code and geometry can resolve -- a real ceiling, not a fault.

doppler_rate_hz_s property

doppler_rate_hz_s: float

Refine's winning Doppler-rate hypothesis for the most recent window, Hz/s: the rate whose de-chirp made the whole preamble's coherent sum largest at its start, from refine's grid over [-doppler_rate, +doppler_rate] (at least as dense as the acquisition's, and searched whenever a rate is declared, even with one acquisition hypothesis). 0 without a declared rate. A hypothesis, not a fit: it names the grid point nearest the carrier's rate well above the design point, and less often near it (validate_acq_rate_pd's event-rate rows).

burst_len property

burst_len: int

Samples in one emitted window -- the burst length this capture was built for, and the stride of a row in push()'s return.

refine_span property

refine_span: int

Coalescing window, in samples -- the reach over which two detections are ONE preamble.

Both sides of that test are burst STARTS (resolved code epochs), so this bounds start-to-start separation, NOT the dead air between bursts. The two differ by a whole burst, and reading it as dead air costs a caller real airtime for nothing: the gap actually required is max(0, refine_span - burst_len), which is 0 whenever a burst is longer than the refine reach (doppler#1085).

min_gap property

min_gap: int

Dead air to leave BETWEEN bursts, in samples — edge to edge, not start to start.

Derived rather than documented as a rule the caller has to apply: a detection's anchor is the code epoch of whichever frame detected, and acquisition's framing is not aligned to the preamble, so the last frame that can detect sits up to reps * code_period past the true start. CLAIM merges two anchors closer than refine_span, so a pair survives only when gap >= refine_span + reps*code_period - burst_len.

Zero is a real answer — a burst longer than refine_span + reps*P needs no gap for the claim rule's sake — but it does not mean zero is wise: a zero gap is a continuous stream rather than a burst link, and it measures 88% at a geometry where this reads 0.

Replaces the prose max(0, refine_span - burst_len), which was short by the whole detection-lag term: 32 samples against 528 at the C suite's geometry (doppler#1172).

retain_span property

retain_span: int

History kept per anchor, in samples -- the MINIMUM TRAILING CONTEXT.

refine_span plus one whole burst. A burst closer than this to the end of what has been pushed is held rather than emitted, because refine cannot yet see the samples it needs. Feed at least this many more, or the last burst of a capture never comes out.

underpowered property

underpowered: bool

True when the search cannot meet the requested pd at this cn0_dbhz and geometry — pd_burst < pd. The grid is still built, best-effort, so the symptom is bursts that are never captured rather than a failure. Construction also emits a UserWarning; this is the same fact as a value, for a caller that would rather ask than catch.

doppler_rate property

doppler_rate: float

Doppler rate (Hz/s) the acquisition's coherent depth is bounded against: doppler_bins <= f_epoch/sqrt(2*doppler_rate), so the carrier drifts less than half a slow-time row per block. 0 is no bound.

rate_hypotheses property

rate_hypotheses: int

Doppler-rate hypotheses the acquisition searches, as given at construction, or the count the acquisition chose when it was built with 0 (auto: the fewest whose residual stops limiting the depth); 1 is none -- doppler_rate then only caps the coherent depth. Above 1 doppler_rate is the span of the hypotheses, the depth is bounded by the residual doppler_rate/(rate_hypotheses - 1), and refine tries the rates too.

pd_predicted property

pd_predicted: float

Detection probability of ONE dwell of the sized grid lying wholly inside the preamble, at cn0_dbhz. A burst gets about reps/doppler_bins dwells at an alignment it does not choose, so compare pd_burst, not this, against the pd that was asked for.

pd_burst property

pd_burst: float

Detection probability of one burst at cn0_dbhz: every dwell its preamble spans, at a uniform alignment against the stream, any one detecting. The number behind underpowered, and the one to compare against the pd that was asked for. It models the ENGINE; what refine loses afterwards is not in the model. Measured on a Zadoff-Chu 127 x 8 preamble, the capture delivers at least pd_burst at a 0.6 design point and at 0.9, the default pd (validation report §2.8). NaN with no design cn0_dbhz.

psl_db property

psl_db: float

The preamble's peak sidelobe level, dB: the largest lag of its periodic autocorrelation OUTSIDE the mainlobe, relative to the peak. A detection's sidelobes sit this far below it, at delays the peak list's exclusion does not cover, so a burst clearing the threshold by more than -psl_db also lists its own sidelobe (with max_peaks > 1), and a strong burst's sidelobe can mask a weak one there. -29.8 dB for a 31-chip m-sequence (1/31); -inf for a perfect sequence such as Zadoff-Chu. Fixed at construction.

eta property

eta: float

Coherent detection gate: the normalised statistic a single-look decision must clear, from pfa spread across the search surface. In force when n_noncoh == 1.

eta_nc property

eta_nc: float

Non-coherent detection gate — the one in force when n_noncoh > 1, which a burst search never chooses on its own (it reads 0 unless configure_search_raw pins looks). Higher than eta for the same pfa, because combining looks costs the threshold what it buys in sensitivity.

straddle_loss property

straddle_loss: float

Correlation kept, worst case, by a burst landing BETWEEN grid points rather than on one. The search is a finite grid in Doppler and code phase, so a real burst almost never sits on a hypothesis exactly; this is what that costs, and it is already priced into pd_predicted and pd_burst.

doppler_bins property

doppler_bins: int

Doppler hypotheses searched: the coherent depth chosen (the slow-time FFT length in code reps, <= reps), times the window-tile count when doppler_uncertainty exceeds the native span -- a tile keeps its own coherent depth when a design cn0_dbhz asks for one, and a hit's doppler_bin indexes all of window_bins * depth rows. This is the count to fold a hit's bin over (bin_to_signed(bin, doppler_bins) * doppler_res_hz is the signed Doppler); it is dp_correlator_grid_bins(), and the same number BurstAcquisition.doppler_bins reads. configure_search_raw pins it.

n_noncoh property

n_noncoh: int

Non-coherent looks combined per decision. Above 1 the object needs that many frames before it can decide at all, which is why a caller sweeping in short dwells has to pin it.

code_bins property

code_bins: int

Delay hypotheses per Doppler row: one repetition of the preamble, in samples (len(preamble)).

doppler_span_hz property

doppler_span_hz: float

Unambiguous Doppler half-range, ± this. Beyond it the per-segment integrate-and-dump's sinc rolloff suppresses the correlation, so a burst outside the span is not merely harder to find — it is nulled.

pending property

pending: int

Detections held because their burst window has NOT fully arrived.

push() deliberately emits nothing for these: a window is returned when it is complete, not when it is guessed at. What this exists for is the other end -- a caller closing a file or a socket while this is non-zero is discarding a burst that would have been captured, and every other read-back looks identical to "nothing was ever there".

dropped property

dropped: int

Samples of look-back discarded while a queued detection still needed them, lifetime: the part of a dead detection's span -- one whose history is gone, so it can never be emitted -- behind the ring's tail. A shadowed detection is not counted. On a live stream this stays 0: push() never refuses input, a moved anchor is a later one, and a re-armed refine reads no further back than the history kept for an unarrived detection at or ahead of it. A resume carries all the look-back, so what makes it non-zero is a blob that names history it does not hold. A LOST BURST each, not a statistic -- it survives reset().

n_bursts property

n_bursts: int

Windows emitted, lifetime.

push

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

Stream raw cf32 samples and get back the SAMPLES of every burst whose window has fully arrived, concatenated: burst i occupies burst_len samples starting at i*burst_len, and events() returns the matching record for each. Samples feed the embedded BurstAcquisition and are retained in a history ring; when a detection fires, the refine stage correlates one code period at each preamble position to recover the exact preamble start -- the one quantity acquisition structurally cannot report, since its code_phase is a lag modulo one code period -- and the window is emitted the moment its last sample has arrived. It stops there: what to DO with a burst (demodulate it, write it to a file, ship it to another process) is the caller's. An empty return is normal, not an error: it means no burst completed in this call. Accepts any block size -- the history ring is a contiguous window over the stream and is never reset between bursts, so a burst whose tail falls outside one call is completed by a later one.

Windows are concatenated: burst i occupies burst_len samples starting at i*burst_len, and events() returns the matching record for each. Every sample of x is consumed. An empty return is normal -- it means no burst completed in this call.

It aborts the process in two places, both a defect in this object and neither reachable from any input or from any blob set_state() accepts: a history ring with no room after trim (the bound in the implementation's trim proves room), and an acquisition call that took nothing while input was left (each call starts with its result empty, and a push with its result empty takes at least the next frame, or the rest as carry). Each stops rather than spinning forever -- and in a Python host it ends the interpreter.

Parameters:

Name Type Description Default
x NDArray[complex64]

Input samples, x_len long.

required
out NDArray[complex64] | None

Written with the completed windows; may be NULL to drop.

None

Returns:

Type Description
NDArray[complex64]

Samples written -- always a multiple of burst_len.

Examples:

>>> import numpy as np
>>> from doppler.acquire import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> pre = np.repeat(np.where(code, -1.0, 1.0), 2).astype(np.complex64)
>>> cap = BurstCapture(pre, burst_len=512, reps=4, fs=2e6)
>>> win = cap.push(np.zeros(4096, dtype=np.complex64))
>>> win.size % cap.burst_len        # whole windows, never a partial
0
>>> win.size                        # silence, so no burst completed
0

push_max_out

push_max_out(x_len: int) -> int

Upper bound on samples push() can return for x_len input.

Distinct bursts cannot overlap, so x_len samples complete at most x_len/burst_len + 1 of them, plus whatever is already queued.

Parameters:

Name Type Description Default
x_len int

Samples the next push() will be given.

required

Returns:

Type Description
int

(x_len/burst_len + 1 + queue capacity) * burst_len samples.

detections

detections(
    count: int = 1, out: NDArray[Any] | None = None
) -> NDArray[Any]

Every hit the search made in the last push(), unfiltered — before the claim rule coalesced the several detections of one preamble, and before the suppression window dropped the ones inside a burst already captured. So several rows can name one burst and a row can be a false alarm; that is the point. Each carries the STREAM-ABSOLUTE code epoch, which acquisition's own code_phase is not (it is a lag modulo one code period), plus the folded Doppler, the C/N0 lower bound and the CFAR statistic that gated it. Read events() instead for the bursts that survived and whose windows arrived. Valid until the next push(), reset() or set_state().

BEFORE the claim rule and the suppression window: several rows can name one preamble, and a row can be a false alarm. That is the point -- this is what acquisition FOUND, and events() is what survived. Valid until the next push(), reset() or set_state().

Parameters:

Name Type Description Default
count int

How many output samples to ask for. The call may return fewer; size an out= buffer with the matching _max_out() when you need the worst case.

1
out NDArray[Any] | None

Destination rows; may be NULL to copy nothing (the count is still returned).

None

Returns:

Type Description
NDArray[Any]

min(detections held, max_out): the rows copied, or with a NULL out the rows that would have been.

One row per burst_capture_detection_t record, with fields:

epoch : int Stream-absolute code epoch of the hit. doppler_hz : float Signed coarse Doppler, folded, Hz. cn0_dbhz : float C/N0 estimate from the hit, dB-Hz. test_stat : float The CFAR gating statistic, peak over noise. peak_mag : float Raw CFAR peak magnitude.

Examples:

>>> import numpy as np
>>> from doppler.acquire import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> pre = np.repeat(np.where(code, -1.0, 1.0), 2).astype(np.complex64)
>>> cap = BurstCapture(pre, burst_len=512, reps=4, fs=2e6)
>>> _ = cap.push(np.zeros(4096, dtype=np.complex64))
>>> # what the search found, against what became a burst
>>> len(cap.detections()) >= len(cap.events())
True

detections_max_out

detections_max_out(n: int) -> int

Raw detections available from the last push(). n is ignored.

Parameters:

Name Type Description Default
n int

Ignored: the count is the last push's, not a request.

required

Returns:

Type Description
int

Rows detections() can copy out now (0 before any push).

events

events(
    count: int = 1, out: NDArray[Any] | None = None
) -> NDArray[Any]

The event record for each burst the last push() returned. Row i describes the window at samples[i*burst_len ...] of that push. Valid until the next push(), reset() or set_state().

Row i describes the window at i*burst_len. Valid until the next push(), reset() or set_state().

Parameters:

Name Type Description Default
count int

How many output samples to ask for. The call may return fewer; size an out= buffer with the matching _max_out() when you need the worst case.

1
out NDArray[Any] | None

Destination rows; may be NULL to copy nothing (the count is still returned).

None

Returns:

Type Description
NDArray[Any]

min(windows of the last push, max_out): the rows copied, or with a NULL out the rows that would have been.

One row per burst_capture_event_t record, with fields:

preamble_start : int Refined stream position of the preamble. doppler_hz_est : float Signed coarse Doppler, Hz. doppler_res_hz : float Acquisition's native bin width, Hz. cn0_dbhz_est : float C/N0 estimate from the hit, dB-Hz. doppler_rate_hz_s : float Refine's winning rate hypothesis, Hz/s: the rate whose de-chirp made the whole preamble's coherent sum largest at preamble_start. One of refine's R grid points from -doppler_rate to +doppler_rate, spaced 2*doppler_rate/(R - 1); 0 without a declared rate (doppler_rate 0). Refine searches the grid whenever a rate is declared, with one acquisition hypothesis or many (doppler#2219). A hypothesis, not a fit, from the whole preamble rather than one dwell: it is the grid point nearest the carrier's rate in every event 23 dB over the design point, on or off the grid. At the design point it is in 82% to 89% of events whose rate is ON a grid point, but in 51% of those 0.4 of a step off one, where the neighbouring point is only 0.6 of a step away (validate_acq_rate_pd's event-rate rows; docs/design/burst-capture.md). Appended last, so positional initialisers do not shift (doppler#2219).

Examples:

>>> import numpy as np
>>> from doppler.acquire import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> pre = np.repeat(np.where(code, -1.0, 1.0), 2).astype(np.complex64)
>>> cap = BurstCapture(pre, burst_len=512, reps=4, fs=2e6)
>>> win = cap.push(np.zeros(4096, dtype=np.complex64))
>>> len(cap.events()) == win.size // cap.burst_len
True

events_max_out

events_max_out(n: int) -> int

Records available from the last push(). n is ignored.

Parameters:

Name Type Description Default
n int

Ignored: the count is the last push's, not a request.

required

Returns:

Type Description
int

Event rows events() can copy out now: one per window returned.

configure_search_raw

configure_search_raw(
    doppler_bins: int, n_noncoh: int
) -> None

Pin the embedded BurstAcquisition's search grid directly, bypassing the auto-sizing -- the escape hatch for a caller who wants a specific (doppler_bins, n_noncoh). Forwards to the engine unchanged.

The escape hatch for a caller who wants a specific (doppler_bins, n_noncoh). Forwards to the engine, with one refusal of this object's own: a grid whose anchor can lag the preamble by more than refine reaches -- n_noncoh * doppler_bins code periods against k_lo -- is rejected rather than accepted and silently mis-refined. Acquisition stamps a hit at the end of the LAST accumulated look, so every look past the one holding the preamble moves the anchor a whole frame later; a burst has one frame of preamble, so n_noncoh = 1 is the grid a capture wants and the sizer now always picks (doppler#1181).

Parameters:

Name Type Description Default
doppler_bins int

Coherent depth to pin (see dp_correlator_configure_search_raw()).

required
n_noncoh int

Non-coherent looks to pin; n_noncoh * doppler_bins must not exceed the refine reach.

required

Raises:

Type Description
ValueError

If the C call returns a non-zero status. The exception message is configure_search_raw failed, with the return code appended (gh-869).

Examples:

>>> import numpy as np
>>> from doppler.acquire import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> pre = np.repeat(np.where(code, -1.0, 1.0), 2).astype(np.complex64)
>>> cap = BurstCapture(pre, burst_len=512, reps=4, fs=2e6)
>>> cap.configure_search_raw(4, 1)   # 4 Doppler bins, coherent only

release

release(i: int) -> None

Give back the span window i of the last push() claimed. An emitted window owns its whole span: detections inside it are the payload firing against the acquisition code, so they are HELD rather than reported. A consumer that knows better -- a demodulator whose CRC failed -- calls this for that window, and the held detections are searched again on the next push(). Unreleased, they are dropped when the next push() begins. Raises ValueError if i is not a window of the last push().

An emitted window owns its whole span: a detection inside it is the payload firing against the acquisition code, not a new burst, so it is HELD rather than reported. Whether the window WAS a burst is a verdict this object cannot reach -- it stops at samples; error detection, whatever form the frame gives it, is the consumer's -- so a consumer that knows better calls this for that window, and the held detections are searched again on the next push(). Unreleased, they are dropped when the next push() begins, which is exactly the behaviour a consumer with no verdict always had.

What it prevents (doppler#1181): a spurious window ending just after a real burst begins used to swallow that burst's first detections -- the receiver's own design says only a DECODED burst may own a span (§10.3, doppler#1004), and the capture underneath had been owning it on emission.

Must be called BEFORE the next push(): i indexes THIS push's windows.

Parameters:

Name Type Description Default
i int

Window index from the last push().

required

Raises:

Type Description
ValueError

If the C call returns a non-zero status. The exception message is release failed, with the return code appended (gh-869).

Examples:

>>> import numpy as np
>>> from doppler.acquire import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> pre = np.repeat(np.where(code, -1.0, 1.0), 2).astype(np.complex64)
>>> cap = BurstCapture(pre, burst_len=512, reps=4, fs=2e6)
>>> _ = cap.push(np.zeros(4096, dtype=np.complex64))
>>> cap.release(0)   # no window 0 in a quiet push
Traceback (most recent call last):
  ...
ValueError: release failed (rc=-4)

reset

reset() -> None

Return to the searching state: resets the embedded acquisition, drops the history ring's contents, clears every queued detection and every read-back, so a fresh stream cannot inherit the previous one's position. Construction parameters are untouched.

Resets the embedded acquisition, rewinds the history ring, clears every queued detection and every read-back. Construction parameters are untouched; dropped and n_bursts deliberately survive, because a lost burst stays lost and a lifetime count is a lifetime count.

Examples:

>>> import numpy as np
>>> from doppler.acquire import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> pre = np.repeat(np.where(code, -1.0, 1.0), 2).astype(np.complex64)
>>> cap = BurstCapture(pre, burst_len=512, reps=4, fs=2e6)
>>> cap.push(np.zeros(4096, dtype=np.complex64)).size
0
>>> cap.reset()
>>> cap.pending
0

state_bytes

state_bytes() -> int

Size in bytes of this object's serialized state.

The exact length get_state returns and set_state requires. It depends on how the object was constructed (state arrays are sized at construction), so read it from the instance rather than assuming a constant.

Raises RuntimeError if the PersistentBurstCapture has already been destroyed.

Returns:

Type Description
int

Byte length of one serialized state blob.

get_state

get_state() -> bytes

Serialize this object's mutable state to bytes.

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

The blob is opaque and always state_bytes() long. Its layout is an implementation detail of the C core and is not a stable format across builds.

Raises RuntimeError if the PersistentBurstCapture has already been destroyed.

Returns:

Type Description
bytes

Opaque snapshot, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

Restore mutable state from a get_state() blob.

Overwrites the live state in place, including any value a setter changed after construction. Construction parameters are not restored: restore into an object built the same way. Length is validated against state_bytes() before the blob is handed to the C core, which may reject it too: a core may check some construction parameters and refuse a blob built differently.

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

Parameters:

Name Type Description Default
blob bytes

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

required

Raises:

Type Description
MemoryError

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

ValueError

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

destroy

destroy() -> None

Release the underlying C resources immediately.

Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this to release them at a definite point instead, or use the object as a context manager, which calls it on exit.

Idempotent: calling it again on an already-released object does nothing. Every other method raises RuntimeError once it has run.

__enter__

__enter__() -> PersistentBurstCapture

Enter a context manager, returning this object.

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

Returns:

Type Description
PersistentBurstCapture

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

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.

...

bin_to_signed — read an FFT grid the way numpy does

Maps a reported Doppler bin index to its signed frequency index — numpy.fft.fftfreq(n) * n, exactly. Multiply by doppler_res_hz for Hz:

import numpy as np

from doppler.cvt import bin_to_nrz
from doppler.acquire import BurstAcquisition, bin_to_signed
from doppler.wfm import PN, mls_poly

code = np.asarray(
    PN(poly=mls_poly(5), seed=1, length=5).generate(31)
).astype(np.uint8)
# The preamble is its samples: chips by bin_to_nrz, held 4 samples a chip.
nrz = np.zeros(code.size, np.float32)
bin_to_nrz(code, nrz)
preamble = np.repeat(nrz, 4).astype(np.complex64)
acq = BurstAcquisition(preamble, reps=8, fs=4e6, cn0_dbhz=60.0)

# One repeated-code burst, so push() reports a hit to read the bin off.
burst = np.tile(preamble, 8)
hit_bin = acq.push(burst)[0][0]

f0_hz = bin_to_signed(hit_bin, acq.doppler_bins) * acq.doppler_res_hz
print(f"bin {hit_bin} -> {f0_hz:+.0f} Hz")

Call it rather than writing the fold out. The search and its hand-off must agree on the convention, and a consumer seeded on the wrong side of it is off by the full search span — a failure that once surfaced here as a receiver reporting tracking == 1 while decoding noise. It is a thin wrapper over dp_fftfreq_index() in clib_common.h, so C callers inline the same code.

Two things worth knowing. An even grid's Nyquist bin is -n/2, following numpy; this engine reported +n/2 there until the burst-chain certification, so a formula ported in from numpy now agrees with it. And the C companion dp_fftfreq(bin, n, fs) returns the bin's frequency directly, taking the sample rate where numpy takes the sample spacing.

bin_to_signed

bin_to_signed(bin: int, n_bins: int) -> int

Map an FFT bin index to its SIGNED frequency index -- numpy.fft.fftfreq(n) * n, exactly: 0 = DC, ascending positive to (n-1)/2, then wrapping negative, so an even grid's Nyquist bin is -n/2. Multiply by doppler_res_hz for Hz. Call this rather than writing the fold out: the search and its hand-off must agree on the convention, and a consumer seeded on the wrong side of it is off by the full search span -- a failure that once surfaced here as a receiver reporting tracking while decoding noise. A thin wrapper over dp_fftfreq_index() in clib_common.h, so C callers inline the same code.

Parameters:

Name Type Description Default
bin int

Bin index in [0, n_bins).

required
n_bins int

Grid size.

required

Returns:

Type Description
int

Signed index in [-(n_bins/2), +((n_bins-1)/2)].

Examples:

>>> import numpy as np
>>> from doppler.acquire import bin_to_signed
>>> [bin_to_signed(b, 8) for b in range(8)]
[0, 1, 2, 3, -4, -3, -2, -1]
>>> (np.fft.fftfreq(8) * 8).astype(int).tolist()   # same convention
[0, 1, 2, 3, -4, -3, -2, -1]
>>> bin_to_signed(4, 7)                         # odd grid: no ambiguity
-3

Gallery — CarrierAcquisition: RRC Pulse Shaping, Correlation and Detection, DSSS Acquisition — Pd / Pfa vs Es/N0, DsssBurstReceiver — the Composed Burst Chain, DsssReceiver — the Composed Continuous DSSS Receiver, Gallery Guides — Acquiring a Repeated Preamble, DSSS Burst Acquisition, Guides, Checkpoint & Resume, Waveforms — what you can generate Design — Design — pure-functional acquisition kernel (elastic fleet), The acquisition surface: the DSSS search engine, API taxonomy: the DSP building-block hierarchy and its naming axis, AsyncDsssReceiver and AsyncDsssPool — continuous DSSS, from spec to object, BurstBank — the coarse-Doppler bank as one C object, BurstCapture: acquisition's output, turned into bursts, CoarseChannel — is a channel an object, or a slice of the bank?, Corr2D: decoupled (interpolated) inverse length, One Python class: the step-4 design, One correlator: the step-3 design, Detection Sizing — the four laws behind one prefix, DsssBurstReceiver: the burst chain, composed in C, Design, The Spectrogram — the measurement record, State Serialization — the standard bytes interface Contributing — DSSS Primary Use Cases for Code Acquisition Design, Validation log, Contributing