Skip to content

Python DSSS API

The doppler.dsss module provides a full DSSS receiver: Acquisition — the streaming burst-acquisition engine that finds an unknown code phase and Doppler — and two despreaders that track and despread once acquired: Despreader, the continuous DLL+Costas receiver (GPS-like, always tracking), and BurstDespreader, the preamble-aware payload tracker for latency-bound bursts.

Source: src/doppler/dsss/__init__.py


Acquisition — streaming burst acquisition

Acquisition searches a streamed cf32 signal for a repeated BPSK PN burst over the joint (Doppler × code-phase) grid, sizing its own search grid — coherent depth, CFAR threshold, non-coherent looks — from the physics (chip_rate, cn0_dbhz, pfa, pd) using doppler.detection. Push arbitrary-length blocks; it yields one record per detection — (doppler_bin, code_phase, peak_mag, noise_est, test_stat, snr_est) — whose (doppler_bin, code_phase) seed the BurstDespreader. See the DSSS Burst Acquisition guide for the search-space sizing and a worked example.

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

Samples per chip (>= 1).

4
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 acq_state_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 (> 0).

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

CFAR mode index: 0=mean, 1=median, 2=min, 3=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; design §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

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.dsss 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, spc=4, chip_rate=1e6, cn0_dbhz=50.0)
>>> a.push(burst)[0][:2]    # detects (Doppler-window bin, code phase)
(0, 17)
>>> a.coherent_bins            # no window given: one epoch
1
>>> b = Acquisition(code, spc=4, 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

Code-phase hypotheses searched (= sf*spc, one code period).

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 (design §2.3): 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).

spc property

spc: int

Samples per chip (chip-rate oversample factor).

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

Bonferroni 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) = chip_rate * spc.

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 (design §2.3); 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 code_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 (design §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 (design §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.dsss 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, spc=4, 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)
(0, 17)

push

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

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 acq_result_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 acq_result_t.

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

Number of events written (0 … max_results).

Examples:

>>> import numpy as np
>>> from doppler.dsss 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, spc=4, chip_rate=1e6, cn0_dbhz=50.0,
...                 doppler_uncertainty=40e3)
>>> fs = 1e6 * 4                    # sample rate = chip_rate * spc
>>> 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, code phase)
(1, 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).

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 acq_create_burst()/acq_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, ACQ_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.dsss 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, spc=4, 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, 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.

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 one chip around it (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 acq_result_t from acq_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 … ACQ_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.dsss import Acquisition
>>> code = (np.arange(31) * 5 % 2).astype(np.uint8)
>>> a = Acquisition(code, spc=2, 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 before the slow-time transform (inside a coherent block of D epochs), and the hand-off advances the hit's code phase by the drift over half the dwell (design section 12.11, 12.12). Config, not running state: not in the state blob, so a resumed engine wants it set again. 0.0 (the default) = uncoupled, the engine as it ran without it. Raises ValueError for a negative or non-finite value.

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, before the slow-time transform (a linear phase on each epoch's product, exact to a fraction of a sample). Measured at SPEC'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 (acq_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.

Config, not running state: it is not in the state blob, so a resumed engine wants it set again by its holder, as at create. 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
ValueError

If the C call returns a 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.dsss import Acquisition
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> a = Acquisition(code, spc=2, 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 (design §2.3: 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). 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.dsss 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, spc=2, chip_rate=1e6, cn0_dbhz=50.0,
...                 doppler_uncertainty=4000.0)
>>> a.threads >= 1               # a pool, sized to the machine
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 (design §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; acq_surface_doppler_hz() and acq_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.dsss 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, spc=2, 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 code_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 acq_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 * code_bins floats.

required

Returns:

Type Description
int

Cells written (surface_rows * code_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.dsss 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, spc=2, 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.code_bins, dtype=np.float32)
>>> a.surface(s) == s.size
True
>>> s.reshape(a.surface_rows, a.code_bins).shape == (a.surface_rows, 1022)
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 acq_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.dsss 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, spc=2, 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 acq_build_handoff() applies to a hit's code_phase — so a plotted peak sits at the chip phase the DetectionEvent would carry.

Parameters:

Name Type Description Default
out NDArray[float64]

At least code_bins doubles.

required

Returns:

Type Description
int

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

Examples:

>>> import numpy as np
>>> from doppler.dsss 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, spc=2, chip_rate=1e6, cn0_dbhz=50.0)
>>> c = np.empty(a.code_bins, dtype=np.float64)
>>> a.surface_chip_phase(c) == a.code_bins
True
>>> bool(c[0] == 0.0 and c[1] == 510.5)
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 exactly the state that evolves as the object runs, so a blob taken now and restored later resumes from this point. Construction parameters are not included: restore into an object built the same way.

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

Raises RuntimeError if the 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; the object keeps the parameters it was constructed with. Length is validated against state_bytes() before the blob is handed to the C core, and the core may reject it as well.

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

...

PolynomialPhaseEstimator — feedforward frequency + chirp-rate estimator

PolynomialPhaseEstimator recovers the frequency and chirp rate (Doppler and Doppler rate) of a complex sequence in one shot — no tracking loop — via a coherent (chirp-rate × frequency) matched-filter surface: for each rate hypothesis it dechirps the sequence and FFTs it, and the surface's global peak (parabola-interpolated in both axes) gives (r, f). Being fully coherent it is the matched-filter-optimal estimator, so it holds at low SNR. The single max_rate knob spans both regimes: max_rate = 0 collapses to one FFT — pure Doppler, near-static — while max_rate > 0 searches a ±max_rate dechirp bank for a severe LEO chirp (cost scales with the rate span). The caller strips modulation first (data-aided wipe, or square an M-PSK stream for the non-data-aided case). estimate(x) returns a PolynomialPhaseEstimate(freq_norm, rate_norm, snr_db) record in normalized units (cycles/sample and cycles/sample²); scale by the sequence's sample rate for Hz. It is the feedforward front-end for chirping-burst demodulation.

PolynomialPhaseEstimator

Create a polynomial-phase estimator.

Parameters:

Name Type Description Default
max_len int

Maximum input sequence length (>= 4).

4096
max_rate float

Chirp-rate search half-span (cycles/sample^2); 0 searches frequency only (a single FFT — near-static Doppler).

0.0

Examples:

Create with defaults:

>>> from doppler.dsss import PolynomialPhaseEstimator
>>> obj = PolynomialPhaseEstimator(max_len=4096, max_rate=0.0)

max_len property

max_len: int

max input length (sizes the plan/scratch).

nfft property

nfft: int

zero-padded transform length: 4 * next_pow2 (max_len). The 4x is deliberate -- a finer frequency grid before the parabolic peak refinement, which matters because the input is often short (preamble partials, symbol streams). It also sizes buf, spec and mag, so the footprint is 4x what a bare next-pow2 would suggest.

max_rate property

max_rate: float

chirp-rate search half-span (cycles/sample^2).

n_rate property

n_rate: int

number of chirp-rate hypotheses (1 if max_rate=0).

reset

reset() -> None

Do nothing — the estimator keeps no running state between calls.

A feedforward analyzer computes each estimate purely from the segment it is handed, so there is nothing to clear. The method exists only to satisfy the common object protocol; calling it is always safe and has no effect.

Examples:

>>> from doppler.dsss import PolynomialPhaseEstimator
>>> p = PolynomialPhaseEstimator(max_len=512, max_rate=0.0)
>>> p.reset()   # no-op: an estimate depends only on the next
>>> #           segment

estimate

estimate(x: complex) -> PolynomialPhaseEstimate

Estimate (freq, chirp-rate) of a complex sequence via the 2-lag HAF.

Runs the full 2-D matched-filter search in one shot: for each chirp-rate hypothesis the segment is dechirped and FFT-ed, and the peak of the resulting surface — refined sub-bin by parabolic interpolation on both axes — gives the estimate. With max_rate = 0 the rate axis collapses to a single FFT (pure Doppler) and the returned rate is forced to exactly 0.

Feed a segment whose modulation has already been stripped (data-aided by the known symbols, or non-data-aided by the M-th-power trick — remembering that raising to the M-th power scales both returned values by M). The result carries freq_norm (cycles/sample), rate_norm (cycles/sample^2), and snr_db (a rough peak-to-mean confidence).

Parameters:

Name Type Description Default
x complex

Complex segment (modulation already stripped by the caller).

required

Returns:

Type Description
PolynomialPhaseEstimate

The estimate; all fields are zeroed if n_in is out of range.

Examples:

>>> import numpy as np
>>> from doppler.dsss import PolynomialPhaseEstimator
>>> m = np.arange(512)
>>> f, r = 0.05, 1e-5               # true Doppler + chirp rate
>>> x = np.exp(2j*np.pi*(f*m + 0.5*r*m*m)).astype(np.complex64)
>>> p = PolynomialPhaseEstimator(max_len=512, max_rate=5e-5)
>>> e = p.estimate(x)                        # one-shot coherent search
>>> round(e.freq_norm, 4), round(e.rate_norm, 7)
(0.0501, 1e-05)

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

Enter a context manager, returning this object.

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

Returns:

Type Description
PolynomialPhaseEstimator

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

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.

...

BurstDemod — feedforward DSSS frame demodulator

BurstDemod is the whole post-acquisition payload chain, in C, with no tracking loops: it estimates the residual Doppler and Doppler rate feedforward (composing PolynomialPhaseEstimator over the unmodulated preamble), dechirps the burst at sample rate, despreads the short data code to soft BPSK symbols, frame-syncs against a known word, and checks a CRC-16 trailer. The one max_rate knob spans both operating points: near-static Doppler (0, a single-FFT estimate) and a severe LEO chirp (> 0, the coherent rate search). It is one-shot per burst — seed it from acquisition and call demod.

The frame is [sync header][payload][CRC-16 trailer] in BPSK symbols (no FEC). demod(x) returns the payload bits; the read-back properties report frame_valid (CRC), est_freq_hz, est_rate_hz, frame_offset, and n_symbols.

import numpy as np
from doppler.dsss import BurstDemod

# Build a burst: 5x acquisition preamble, then a spread frame
# [Barker-13 sync | payload | CRC-16]. A real receiver takes (f0, code
# phase) from `Acquisition`; here we seed a known prior so the block runs.
acq_code = ((np.arange(500) * 2654435761 >> 13) & 1).astype(np.uint8)
data_code = ((np.arange(50) * 40503 >> 7) & 1).astype(np.uint8)
sync_word = np.array([0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0], np.uint8)
payload = ((np.arange(64) * 7 + 3) & 1).astype(np.uint8)


def _crc16(bits):                          # CRC-16/CCITT, MSB-first
    c = 0xFFFF
    for b in bits:
        c ^= (int(b) & 1) << 15
        c = ((c << 1) ^ 0x1021) & 0xFFFF if c & 0x8000 else (c << 1) & 0xFFFF
    return c


def _sign(b):                              # 0/1 chips -> +1/-1 BPSK
    return np.where(np.asarray(b) & 1, -1.0, 1.0)


crc = _crc16(payload)
crc_bits = np.array([(crc >> (15 - j)) & 1 for j in range(16)], np.uint8)
frame = np.concatenate([sync_word, payload, crc_bits])
chips = [np.tile(_sign(acq_code), 5)]      # unmodulated preamble
chips += [_sign(b) * _sign(data_code) for b in frame]
f0, preamble_start = 0.012, 0              # cyc/sample; from acquisition
bb = np.repeat(np.concatenate(chips), 4).astype(np.complex64)
nn = np.arange(len(bb))
rx = (bb * np.exp(2j * np.pi * f0 * nn)).astype(np.complex64)

d = BurstDemod(data_code, spc=4, chip_rate=1e6, carrier_hz=0.0,
               max_rate=0.0, frame_syms=13 + 64 + 16, est_segments=10)
d.set_preamble(acq_code, reps=5)
d.set_sync(sync_word)               # 0/1 BPSK sync header
d.set_prior(f0, preamble_start)
frame = d.demod(rx)                 # the FRAME's bits, sync word first
# The demodulator stops at decisions; the frame is undone one layer up.
from doppler.wfm import crc16
payload = frame[13:13 + 64]
rx_crc = 0
for b in frame[13 + 64:][:16]:
    rx_crc = (rx_crc << 1) | int(b)
assert rx_crc == int(crc16(payload))            # the CHECK is the caller's

BurstDemod

Create a feedforward BPSK DSSS burst demodulator.

Parameters:

Name Type Description Default
data_code NDArray[uint8]

Data spreading code, one 0/1 chip per element; copied into the object (its length is the data spreading factor, chips/symbol).

required
spc int

Samples per chip (front-end oversample).

4
chip_rate float

Chip rate (Hz); sets the sample rate as spc*chip_rate.

1.0e6
carrier_hz float

RF carrier (Hz) for code-Doppler scaling; 0 = ignore.

0.0
max_rate float

Chirp-rate search half-span (cycles/sample^2 at the input rate); 0 = Doppler only (no rate search).

0.0
frame_syms int

Symbols the frame occupies after the sync word — how many bits demod() hands back per burst. What they mean is a frame description's business.

0
est_segments int

Partial correlations per acq period (segmentation for the feedforward estimate; larger tolerates more rate).

10

Examples:

>>> import numpy as np
>>> from doppler.dsss import BurstDemod
>>> spc, acq_sf, reps, data_sf = 4, 500, 5, 50
>>> sync = np.array([0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0], np.uint8)
>>> acode = ((np.arange(acq_sf) * 2654435761 >> 13) & 1).astype(
...     np.uint8)
>>> dcode = ((np.arange(data_sf) * 40503 >> 7) & 1).astype(np.uint8)
>>> payload = ((np.arange(64) * 7 + 3) & 1).astype(np.uint8)
>>> def crc16(bits):
...     c = 0xFFFF
...     for b in bits:
...         c ^= (int(b) & 1) << 15
...         c = (((c << 1) ^ 0x1021) & 0xFFFF
...              if c & 0x8000 else (c << 1) & 0xFFFF)
...     return c
>>> crc = crc16(payload)
>>> crc_bits = np.array(
...     [(crc >> (15 - j)) & 1 for j in range(16)], np.uint8)
>>> frame = np.concatenate([sync, payload, crc_bits])
>>> csign = lambda b: np.where(np.asarray(b) & 1, -1.0, 1.0)
>>> chips = ([np.tile(csign(acode), reps)]
...          + [csign(b) * csign(dcode) for b in frame])
>>> bb = np.repeat(np.concatenate(chips), spc).astype(np.complex64)
>>> n = np.arange(len(bb))
>>> f0 = 0.012
>>> x = (bb * np.exp(2j * np.pi * f0 * n)).astype(np.complex64)
>>> d = BurstDemod(dcode, spc=spc, chip_rate=1e6, frame_syms=len(frame))
>>> d.set_preamble(acode, reps)   # unmodulated (f0, rate) preamble
>>> d.set_sync(sync)              # Barker-13: frame align + sign fix
>>> d.set_prior(f0, 0)            # coarse Doppler + preamble start
>>> bits = d.demod(x)      # estimate -> dechirp -> despread -> slice
>>> bool(np.array_equal(bits, frame))   # the FRAME, not the payload
True

frame_offset property

frame_offset: int

symbol offset of the sync word.

n_symbols property

n_symbols: int

despread data symbols produced.

est_freq_hz property

est_freq_hz: float

estimated residual Doppler (Hz).

est_rate_hz property

est_rate_hz: float

estimated Doppler rate (Hz/s).

est_snr_db property

est_snr_db: float

estimator confidence (dB).

frame_syms property

frame_syms: int

symbols the frame occupies AFTER the sync word — a number the caller states. What they MEAN is the frame description's business, one layer up.

est_n0 property

est_n0: float

Noise power the LLRs are scaled by, referred to unit symbol amplitude — 2·var(Im)/mean|Re|² over the derotated frame, floored at 1e-12 so a noiseless capture stays finite. llrs() is divided by this, so multiplying back recovers the raw projection, and two bursts are comparable only because both were scaled by their own estimate. Reading it beside symbols() is what turns the constellation into an absolute measurement rather than a picture.

reset

reset() -> None

Clear the per-burst read-backs, leaving the configuration intact.

Zeros the after-demod fields (frame_offset, n_symbols, and the est_* estimates) so a stale result cannot be mistaken for a fresh one. The spreading codes, sync word, and prior set up before the first burst are preserved, so the object is immediately ready to demodulate the next burst.

Examples:

>>> import numpy as np
>>> from doppler.dsss import BurstDemod
>>> dcode = (np.arange(50) & 1).astype(np.uint8)
>>> d = BurstDemod(dcode, spc=4, chip_rate=1e6, frame_syms=93)
>>> d.reset()          # clears the estimates, keeps the config
>>> d.frame_offset
0

set_preamble

set_preamble(acq_code: NDArray[uint8], reps: int) -> None

Set the (unmodulated) acquisition preamble code + repetition count used for the feedforward (f0, rate) estimate.

The preamble is the acq spreading code transmitted reps times with no data modulation; demod() segment-despreads it into partial correlations and feeds those to the polynomial-phase estimator to recover the coarse (frequency, chirp-rate). Call once after construction; the code is copied.

Parameters:

Name Type Description Default
acq_code NDArray[uint8]

Acq preamble spreading code, one 0/1 chip per element; copied into the object.

required
reps int

Number of preamble repetitions in the burst.

required

Examples:

>>> import numpy as np
>>> from doppler.dsss import BurstDemod
>>> dcode = (np.arange(50) & 1).astype(np.uint8)
>>> d = BurstDemod(dcode, spc=4, chip_rate=1e6, frame_syms=93)
>>> acode = (np.arange(500) & 1).astype(np.uint8)  # unmodulated
>>> d.set_preamble(acode, reps=5)  # 5 reps drive the (f0, rate) fit

set_sync

set_sync(sync: NDArray[uint8]) -> None

Set the known frame-sync word (0/1 BPSK symbols) used for frame alignment and phase/sign resolution. The ONLY thing this object is told about the frame's content, and for a physical-layer reason: without the sign the slicer would be a coin toss. Where the payload sits, which stages cover what and whether a check passed all need the frame's description and belong one layer up (doppler#1022).

After the data section is despread to soft BPSK symbols, demod() correlates them against this word; the complex correlation peak locates the frame (its frame_offset) and its phase resolves the residual carrier rotation and the BPSK sign ambiguity before slicing. Pass the word as 0/1 symbols; it is copied and stored internally as +/-1.

This is the ONLY thing this object is told about the frame's content, and it is told it for a physical-layer reason: without the sign the slicer would be a coin toss. Everything else — where the payload sits, which stages cover what, whether a check passed — needs the frame's description and belongs one layer up (doppler#1022).

Parameters:

Name Type Description Default
sync NDArray[uint8]

Frame-sync word, one 0/1 symbol per element; copied.

required

Examples:

>>> import numpy as np
>>> from doppler.dsss import BurstDemod
>>> dcode = (np.arange(50) & 1).astype(np.uint8)
>>> d = BurstDemod(dcode, spc=4, chip_rate=1e6, frame_syms=93)
>>> sync = np.array([0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0], np.uint8)
>>> d.set_sync(sync)   # Barker-13: frame align + phase/sign fix

llrs

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

The soft bits of the last demod() — one LLR per FRAME bit, in mpsk_soft_demap's convention: positive means bit 0, so L < 0 reproduces exactly the bits demod() returned. Re(sym * derot) IS the log-likelihood ratio up to a scale and used to be computed, sliced to one bit and freed; a hard decision costs roughly 2 dB of the coding gain a soft-input decoder exists to deliver. Spans the whole frame rather than the payload alone, because a code covers what its description says it covers. Scaled by est_n0, the burst's own noise estimate, so LLRs from different bursts are comparable — a Viterbi would not care, but combining across bursts does.

crealf(sym * derot) IS the log-likelihood ratio up to a scale, and it was computed, sliced to one bit and freed on every burst. A hard decision throws away roughly 2 dB of the coding gain a soft-input decoder exists to deliver (mpsk_soft_demap's own docstring), so this is what makes a coded burst worth coding.

The convention is not a new one: mpsk_soft_demap's, which is mpsk_demap's decision rule seen a second way. Positive means bit 0, so L < 0 reproduces exactly the bits demod() returned — asserted in the tests rather than assumed.

Spans the WHOLE frame, not just the payload, because a code covers what its description says it covers and a decoder needs the bits the code protects. The payload's own span is field_off/field_bits of the layout.

Scaled by est_n0 rather than left raw: a Viterbi is invariant to a positive scale, but LLRs from different bursts are not comparable without one, and combining across bursts needs them to be.

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

Receives the LLRs, one per frame bit.

None

Returns:

Type Description
NDArray[float32]

LLRs written — min(frame bits, max_out), or 0 if the last demod() produced no frame.

Examples:

>>> import numpy as np
>>> from doppler.dsss import BurstDemod
>>> dcode = (np.arange(50) & 1).astype(np.uint8)
>>> d = BurstDemod(dcode, spc=4, chip_rate=1e6, frame_syms=93)
>>> d.set_sync(np.zeros(13, dtype=np.uint8))
>>> d.llrs_max_out(1)          # one per frame symbol
93

llrs_max_out

llrs_max_out(n: int) -> int

Max LLRs burst_demod_llrs() writes: the frame's length in bits.

Parameters:

Name Type Description Default
n int

Ignored — the count is the last demod()'s frame.

required

Returns:

Type Description
int

Output.

symbols

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

The DEROTATED complex symbols of the last demod() — the constellation llrs() is the real part of. Same span and normalisation: the whole frame, scaled to unit mean-|Re|, so symbols.real is llrs() up to est_n0. The quadrature is why this exists: after derotation the real axis carries the signal and the imaginary axis carries noise alone, so a residual phase error — which scales Re by cos(phi) without adding noise — is indistinguishable from a genuine amplitude or SNR loss in mean |LLR|, in LLR spread and in BER alike. Measured over 20000 BPSK symbols, a 30° phase error and an amplitude loss of cos(30°) agreed to three decimals in all three and differed only in Q/I energy, 0.386 against 0.077. That is a pointing problem against a link-budget one, on a burst this object already characterised well enough to know. It was built either way and freed unread (doppler#1087).

Same span and same normalisation as burst_demod_llrs(): the whole frame, scaled to unit mean-|Re| by the burst's own estimate, so crealf(symbols[k]) is that bit's LLR up to est_n0.

The quadrature is why this exists. After derotation the real axis carries the signal and the imaginary axis carries noise alone, so Q is diagnostic: a residual phase error scales Re by cos(phi) WITHOUT adding noise, which makes it indistinguishable from a genuine amplitude or SNR loss in mean |LLR|, in LLR spread and in BER alike. Measured over 20000 BPSK symbols, a 30 degree phase error and an amplitude loss of cos(30 deg) agreed to three decimals in all three, and differed only in Q/I energy — 0.386 against 0.077 (doppler#1087). That is the difference between a pointing problem and a link-budget one, on a burst this object already characterised well enough to know.

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

Receives the symbols, one per frame bit.

None

Returns:

Type Description
NDArray[complex64]

Symbols written — min(frame bits, max_out), or 0 if the last demod() produced no frame.

Examples:

>>> import numpy as np
>>> from doppler.dsss import BurstDemod
>>> dcode = (np.arange(50) & 1).astype(np.uint8)
>>> d = BurstDemod(dcode, spc=4, chip_rate=1e6, frame_syms=93)
>>> d.set_sync(np.zeros(13, dtype=np.uint8))
>>> d.symbols_max_out(1)       # one per frame symbol, as llrs()
93

symbols_max_out

symbols_max_out(n: int) -> int

Max symbols burst_demod_symbols() writes: the frame's length.

Parameters:

Name Type Description Default
n int

Ignored — the count is the last demod()'s frame.

required

Returns:

Type Description
int

Output.

set_prior

set_prior(f0_coarse: float, start: int) -> None

Seed from acquisition: coarse Doppler (cycles/sample at the input rate) and the preamble start sample.

These come from the upstream acquisition stage: f0_coarse centres the feedforward frequency search near the true Doppler, and start tells demod() where the preamble begins within the burst so it despreads the right samples. Call once per burst before demod().

Parameters:

Name Type Description Default
f0_coarse float

Coarse Doppler prior (cycles/sample at the input rate).

required
start int

Preamble start sample index within the burst.

required

Examples:

>>> import numpy as np
>>> from doppler.dsss import BurstDemod
>>> dcode = (np.arange(50) & 1).astype(np.uint8)
>>> d = BurstDemod(dcode, spc=4, chip_rate=1e6, frame_syms=93)
>>> d.set_prior(0.012, start=0)   # coarse Doppler + start, from acq

demod

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

Demodulate a burst (preamble + frame); return the payload bits. Read-back properties report the estimates + CRC validity.

Without out=, the returned array is a view into a buffer reused on the next call (see demod_max_out(), or payload_len, to size an out= buffer for an independent, alias-free result).

Parameters:

Name Type Description Default
x NDArray[complex64]

Input.

required
out NDArray[uint8]

Caller-provided output buffer, at least max(demod_max_out(), len(x)) elements.

...

Returns:

Type Description
NDArray[uint8]

Number of bits written (0 on failure / too-short burst). The read-back fields (frame_valid, est_*, frame_offset) are updated.

demod_max_out

demod_max_out() -> int

Max output length demod() can produce for the current state. Use to size the out= buffer.

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

Enter a context manager, returning this object.

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

Returns:

Type Description
BurstDemod

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

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.

...

Despreader — continuous tracking receiver

A complete continuous DSSS-BPSK receiver in one object: Despreader composes a carrier loop (Costas, FLL-assisted) and a code loop (Dll) on a single shared per-sample integrate-and-dump. Per sample it wipes the carrier (integer-NCO) and feeds the de-rotated sample to the DLL's early/prompt/late correlators; per code period it dumps the prompt and updates both loops — the code loop on the early/late envelopes, the carrier loop on the same prompt. steps() emits one despread prompt symbol per code period; bits() turns the prompts into hard data bits.

bn_fll > 0 enables FLL-assisted carrier pull-in. When a data bit spans periods_per_bit code periods (GPS C/A: 20), bits() bit-syncs — it histograms the prompt sign-flip positions to find the bit boundary (bit_phase), then coherently sums periods_per_bit prompts per bit. The despreader is seeded by acquisition (coarse carrier frequency + code phase) and tracks the residual.

import numpy as np
from doppler.dsss import Despreader
from doppler.wfm import Synth

code = np.random.default_rng(1).integers(0, 2, 127).astype(np.uint8)
rx = Synth(type="pn", pn_length=7, sps=8).steps(127 * 8 * 4)  # PN-spread IQ

d = Despreader(code, sps=8, init_norm_freq=0.0, init_chip=0.0,
               bn_carrier=0.05, bn_code=0.005, bn_fll=0.03,
               zeta=0.707, spacing=0.5, periods_per_bit=1)
symbols = d.steps(rx)   # one despread prompt per code period
bits    = d.bits(rx)    # hard data bits (bit-synced when periods_per_bit > 1)

Despreader

Create a continuous DSSS despreader (COPIES code).

Parameters:

Name Type Description Default
code NDArray[uint8]

Spreading code (0/1 chips), one period; copied.

required
sps int

Samples per chip.

4
init_norm_freq float

Seed carrier frequency, cycles/sample (the acquisition estimate).

0.0
init_chip float

Seed code phase, chips (the acquisition estimate).

0.0
bn_carrier float

Carrier loop noise bandwidth, normalized to the code-period (symbol) rate.

0.05
bn_code float

Code loop noise bandwidth, normalized to the code-period rate.

0.005
bn_fll float

Carrier FLL-assist bandwidth (0 = pure PLL); set > 0 for FLL-assisted carrier pull-in.

0.0
zeta float

Damping factor shared by both second-order loops.

0.707
spacing float

DLL early/late correlator tap offset, chips.

0.5
periods_per_bit int

Code periods per data bit (1 = one bit per period).

1

Examples:

>>> import numpy as np
>>> from doppler.dsss import Despreader
>>> rng = np.random.default_rng(3)
>>> code = rng.integers(0, 2, 31).astype(np.uint8)   # one code period
>>> chips = np.where(code & 1, -1.0, 1.0)    # 0 -> +1, 1 -> -1
>>> bits = rng.integers(0, 2, 40).astype(np.uint8)  # 1 bit/period
>>> syms = np.where(bits == 1, -1.0, 1.0)
>>> rx = np.concatenate(
...     [s * np.repeat(chips, 4) for s in syms]).astype(np.complex64)
>>> d = Despreader(code, sps=4)          # seed a fresh tracking loop
>>> data = d.bits(rx)                        # hard data bits, 1/period
>>> e = np.mean(data != bits[:data.size])    # up to a global BPSK flip
>>> round(float(min(e, 1.0 - e)), 4)
0.0

norm_freq property writable

norm_freq: float

Norm freq.

code_phase property

code_phase: float

Code phase.

code_rate property

code_rate: float

chips advanced per nominal chip (~1.0).

lock_metric property

lock_metric: float

EMA of |Re P|/|P| (1 = locked).

carrier_locked property

carrier_locked: bool

Carrier lock decision: the embedded Costas loop's verify-counted detector on its lock-metric EMA (True = locked; see Costas.configure_lock).

code_locked property

code_locked: bool

Code lock decision: the embedded DLL's verify-counted CFAR detector (True = locked; see Dll.configure_lock). Live in composition — the despreader runs the same always-on detector Dll.steps does.

bit_phase property

bit_phase: int

detected bit boundary (argmax flip_hist).

bn_carrier property writable

bn_carrier: float

Bn carrier.

bn_code property writable

bn_code: float

Bn code.

steps

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

Track carrier + code and despread a cf32 block: per sample wipe the carrier (Costas) and correlate early/prompt/late against the code (DLL), update both loops each code period, and emit one complex prompt symbol per period.

Without out=, the returned array is a view into a buffer reused on the next call (see steps_max_out() to size an out= buffer for an independent, alias-free result).

Parameters:

Name Type Description Default
x NDArray[complex64]

Input.

required
out NDArray[complex64]

Caller-provided output buffer, at least max(steps_max_out(), len(x)) elements.

...

Returns:

Type Description
NDArray[complex64]

Output.

steps_max_out

steps_max_out() -> int

Max output length steps() can produce for the current state. Use to size the out= buffer.

bits

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

Same tracking kernel as steps(), but bit-sync the per-period prompts into hard data bits: periods_per_bit prompts are coherently summed across each detected bit boundary and one 0/1 bit is emitted per data bit.

Without out=, the returned array is a view into a buffer reused on the next call (see bits_max_out() to size an out= buffer for an independent, alias-free result).

Parameters:

Name Type Description Default
x NDArray[complex64]

Input.

required
out NDArray[uint8]

Caller-provided output buffer, at least max(bits_max_out(), len(x)) elements.

...

Returns:

Type Description
NDArray[uint8]

Output.

bits_max_out

bits_max_out() -> int

Max output length bits() can produce for the current state. Use to size the out= buffer.

set_telemetry

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

Attach (or detach) a telemetry context across the despreader. Pure forwarder — the despreader registers no probes of its own: the carrier loop registers ".car.lock" / ".e" / ".freq" / ".locked" and the code loop registers ".code.e" / ".rate" / ".lock" / ".locked" (the ".locked" pair are the loops' verify-counted lockdet decisions, 0/1) — eight probes, all thinned by decim and emitted once per code period (the despreader flushes both loops at its per-period update). Passing NULL detaches both loops. 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. "ch0".

required
decim int

Emit every decim-th code period; >= 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.dsss import Despreader
>>> from doppler.telemetry import Telemetry
>>> tlm = Telemetry(1 << 12)
>>> code = (np.arange(31) % 2).astype(np.uint8)
>>> ch = Despreader(code=code, sps=4)
>>> ch.set_telemetry(tlm, "ch0")
>>> names = sorted(tlm.probe_names)
>>> names[:4]
['ch0.car.e', 'ch0.car.freq', 'ch0.car.lock', 'ch0.car.locked']
>>> names[4:]
['ch0.code.e', 'ch0.code.lock', 'ch0.code.locked', 'ch0.code.rate']
>>> chips = 1.0 - 2.0 * (np.arange(31) % 2)
>>> x = np.tile(np.repeat(chips, 4), 40).astype(np.complex64)
>>> _ = ch.steps(x)
>>> recs = tlm.read()   # eight records per code period
>>> len(recs) > 0 and len(recs) % 8 == 0
True

configure_carrier_lock

configure_carrier_lock(
    up_thresh: float,
    down_thresh: float,
    n_up: int,
    n_down: int,
) -> None

Re-tune the embedded carrier loop's lock detector directly: forwards to the Costas loop's configure_lock (locked flips up after n_up consecutive symbols with the lock-metric EMA above up_thresh, and drops after n_down consecutive symbols below down_thresh; see Costas.configure_lock). Symmetric with the carrier_locked state property: state is readable, so config should be writable too, rather than forcing a caller who needs this control to drop to raw Dll+Costas composition.

Thin forwarder to costas_configure_lock() on the embedded Costas loop — symmetric with despreader_get_carrier_locked() exposing its state: state is readable, so config should be writable too, rather than forcing a caller who needs this control to drop to raw Dll+Costas composition instead of Despreader. See costas_configure_lock() for the parameter semantics.

Parameters:

Name Type Description Default
up_thresh float

Declare threshold on the lock-metric EMA.

required
down_thresh float

Drop threshold (<= up_thresh for level hysteresis).

required
n_up int

Consecutive above-threshold symbols to declare.

required
n_down int

Consecutive below-threshold symbols to drop.

required

Examples:

>>> import numpy as np
>>> from doppler.dsss import Despreader
>>> d = Despreader(code=np.zeros(31, dtype=np.uint8), sps=2)
>>> d.configure_carrier_lock(0.9, 0.8, 4, 16)  # tighter declare/drop

configure_code_lock

configure_code_lock(
    pfa: float, n_looks: int, ref_snr_db: float = 0.0
) -> None

Re-tune the embedded code loop's lock detector: forwards to the DLL's configure_lock (see Dll.configure_lock) -- the derived (pfa-style) entry point, matching Despreader's role as the easy composed API (Dll's raw escape hatch, configure_lock_raw, stays a Dll-only control for a caller that composes Dll+Costas directly). Raises ValueError for pfa outside (0, 1).

Thin forwarder to dll_configure_lock() on the embedded DLL — the derived (pfa-style) entry point, matching Despreader's role as the "easy" composed API (Dll's raw escape hatch, dll_configure_lock_raw(), stays a Dll-only control for a caller that composes Dll+Costas directly). See dll_configure_lock() for the parameter semantics.

Parameters:

Name Type Description Default
pfa float

Per-decision false-alarm probability, in (0, 1).

required
n_looks int

Non-coherent integration depth N (looks); clamped >= 1.

required
ref_snr_db float

Noise-reference estimator SNR in dB (> 0), or 0 to derive from n_looks (see dll_configure_lock()).

0.0

Raises:

Type Description
ValueError

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

Examples:

>>> import numpy as np
>>> from doppler.dsss import Despreader
>>> d = Despreader(code=np.zeros(31, dtype=np.uint8), sps=2)
>>> d.configure_code_lock(1e-3, 20)
>>> d.code_locked
False
>>> d.configure_code_lock(2.0, 20)
Traceback (most recent call last):
    ...
ValueError: configure_code_lock failed (rc=-4)

reset

reset() -> None

Re-seed both loops to the create-time frequency/phase; preserve config.

Restores the carrier NCO to init_norm_freq and the code phase to init_chip, zeroes the loop-filter accumulators and the bit-sync histogram, and clears the lock detectors — the spreading code and every configured bandwidth are preserved. Use it to re-run the same despreader over an independent stream and get a fresh instance's result.

Examples:

>>> import numpy as np
>>> from doppler.dsss import Despreader
>>> rng = np.random.default_rng(3)
>>> code = rng.integers(0, 2, 31).astype(np.uint8)
>>> chips = np.where(code & 1, -1.0, 1.0)
>>> syms = np.where(rng.integers(0, 2, 40) == 1, -1.0, 1.0)
>>> rx = np.concatenate(
...     [s * np.repeat(chips, 4) for s in syms]).astype(np.complex64)
>>> d = Despreader(code=code, sps=4)
>>> first = d.bits(rx)
>>> d.reset()                          # re-seed to acquisition
>>> np.array_equal(first, d.bits(rx))  # same result as a fresh object
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 Despreader has already been destroyed.

Returns:

Type Description
int

Byte length of one serialized state blob.

get_state

get_state() -> bytes

Serialize this object's mutable state to bytes.

Captures exactly the state that evolves as the object runs, so a blob taken now and restored later resumes from this point. Construction parameters are not included: restore into an object built the same way.

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

Raises RuntimeError if the Despreader has already been destroyed.

Returns:

Type Description
bytes

Opaque snapshot, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

Restore mutable state from a get_state() blob.

Overwrites the live state in place; the object keeps the parameters it was constructed with. Length is validated against state_bytes() before the blob is handed to the C core, and the core may reject it as well.

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

Enter a context manager, returning this object.

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

Returns:

Type Description
Despreader

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

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.

...

BurstDespreader — tracking receiver

Seeded with a coarse frequency and code-phase estimate (from the Corr2D/CorrDetector2D acquisition engine or Acquisition), the BurstDespreader locks the signal with a code-tracking delay-locked loop and a carrier-tracking Costas loop, despreads the payload, and emits symbols.


How it works

Every dimension is a run-time parameter — spreading code, spreading factor (sf), samples-per-chip (sps), loop bandwidths. Per input sample the despreader wipes the carrier (an inline NCO driven by the Costas loop), then correlates against early / prompt / late replicas of the code. Once per code period it dumps the three accumulators:

  • the prompt accumulator is the despread symbol — its sign is the BPSK decision, its phase/magnitude the soft information;
  • the non-coherent early-minus-late envelope drives the DLL (track.LoopFilter) → code phase/rate;
  • the decision-directed product drives the Costas loop → carrier frequency/phase.

Seeding from acquisition. init_norm_freq is the carrier frequency in cycles/sample and init_chip_phase the code phase in chips; the caller converts the detector's (Doppler bin, code-phase chip) into those units (the bin→Hz map depends on the search grid, so it stays application-side).

Distinct acquisition vs data codes. Real bursts use a long acquisition code for the preamble and a different (often shorter) data code for the payload. set_acq(acq_code, acq_reps) enables preamble-aided pull-in — track the unmodulated, repeated acquisition preamble coherently (a full ±π discriminator, so even a wide residual pulls in), then switch to the data code at the payload. Omit it for payload-only operation (seeded from acquisition).

Tracking state is readable: norm_freq (carrier estimate), code_phase, lock_metric (0–1), snr_est. The cf32 symbol output chains over the stream module's dp_header_t framing like any other DSP block.


Examples

Despread a payload seeded from acquisition

import numpy as np
from doppler.dsss import BurstDespreader

# data_code: 0/1 spreading chips; seed from the acquisition peak.
# rx is the received capture (reuse the burst built above).
data_code = ((np.arange(32) * 40503 >> 7) & 1).astype(np.uint8)
acq_freq, acq_chip = 0.012, 0.0
d = BurstDespreader(data_code, sf=32, sps=2,
               init_norm_freq=acq_freq, init_chip_phase=acq_chip)
symbols = d.steps(rx)        # complex64 prompt symbols
bits    = d.bits(rx)         # or hard BPSK bits (0/1)
round(d.lock_metric, 2)      # ~1.0 once locked

Preamble-aided pull-in with a distinct acquisition code

burst = rx                        # a received capture (from above)
d = BurstDespreader(data_code, sf=32, sps=2)
d.set_acq(acq_code, acq_reps=5)   # 5-rep preamble pulls the loops in
symbols = d.steps(burst)          # preamble emits nothing; payload follows

BurstDespreader

Create a burst despreader instance.

Parameters:

Name Type Description Default
code NDArray[uint8]

Data spreading code (0/1 chips), length code_len; copied.

required
sf int

Spreading factor: chips integrated per prompt symbol (default: 1).

1
sps int

Samples per chip (default: 2).

2
init_norm_freq float

Seed carrier frequency, cycles/sample — the acquisition estimate (default: 0.0).

0.0
init_chip_phase float

Seed code phase, chips (default: 0.0).

0.0
bn_carrier float

Carrier (Costas) loop noise bandwidth, normalized to the symbol rate (default: 0.05).

0.05
bn_code float

Code (DLL) loop noise bandwidth, normalized to the symbol rate (default: 0.01).

0.01

Examples:

>>> import numpy as np
>>> from doppler.dsss import BurstDespreader
>>> rng = np.random.default_rng(1)
>>> code = rng.integers(0, 2, 31).astype(np.uint8)  # length-31 code
>>> chips = np.where(code & 1, -1.0, 1.0)    # 0 -> +1, 1 -> -1
>>> bits = rng.integers(0, 2, 30).astype(np.uint8)    # payload bits
>>> syms = np.where(bits == 1, -1.0, 1.0)             # BPSK symbols
>>> tx = np.concatenate(
...     [np.repeat(s * chips, 4) for s in syms]).astype(np.complex64)
>>> b = BurstDespreader(code, sf=31, sps=4)           # 31 chips/symbol
>>> sym = b.steps(tx)                        # one prompt/symbol
>>> sym.shape
(30,)
>>> hard = (sym.real < 0).astype(np.uint8)            # BPSK decision
>>> float(np.mean(hard != bits))             # payload recovered
0.0

bn_carrier property writable

bn_carrier: float

Carrier (Costas) loop noise bandwidth, normalized to the symbol rate.

bn_code property writable

bn_code: float

Code (DLL) loop noise bandwidth, normalized to the symbol rate.

norm_freq property writable

norm_freq: float

Current carrier frequency estimate, cycles/sample.

code_phase property

code_phase: float

Current tracked code phase within the symbol, chips.

lock_metric property

lock_metric: float

Lock indicator in [0,1]: the mean of |Re prompt|/|prompt| over every prompt of the burst (cumulative, not EMA). ~1 when phase-locked; ~2/pi (0.637) with no carrier.

snr_est property

snr_est: float

Post-despread SNR estimate over the burst, accumulate-then-ratio: (sum Re^2 - sum Im^2)/sum Im^2, clamped >= 0. This is the effective post-loop SNR (residual tracking jitter included) - the quantity that predicts demodulation performance; it converges to the AWGN-only A^2/sigma^2 as the loop bandwidths shrink.

lock_stat property

lock_stat: float

Calibrated whole-burst lock statistic R = sqrt(stat_n * sum Re^2 / sum Im^2) — the one-shot analog of the tracking loops' verify-counted detectors. Because the noise reference is estimated from as many samples as the signal sum, the exact H0 law is R^2 = stat_n * F(stat_n, stat_n): gate with R > sqrt(stat_n * det_threshold_f(pfa, stat_n)) — exact for every stat_n (a chi-square gate would realize tens of times the priced pfa). Payload prompts only; reset() re-arms.

stat_n property

stat_n: int

Number of prompts folded into the burst statistics so far.

steps

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

Despread a cf32 block; emit one complex prompt symbol per code period.

Streams: a partial symbol is carried in state across calls. Each emitted symbol is the complex prompt integrate-and-dump (carrier-wiped, code-stripped) — its sign is the BPSK decision, its phase/magnitude the soft information. During a burst_despreader_set_acq preamble no symbols are emitted (the loops are pulling in); payload symbols follow.

Without out=, the returned array is a view into a buffer reused on the next call (see steps_max_out() to size an out= buffer for an independent, alias-free result).

Parameters:

Name Type Description Default
x NDArray[complex64]

Input CF32 samples, length x_len.

required
out NDArray[complex64]

Caller-provided output buffer, at least max(steps_max_out(), len(x)) elements.

...

Returns:

Type Description
NDArray[complex64]

Number of symbols written.

Examples:

>>> import numpy as np
>>> from doppler.dsss import BurstDespreader
>>> rng = np.random.default_rng(1)
>>> code = rng.integers(0, 2, 31).astype(np.uint8)  # length-31 code
>>> bits = rng.integers(0, 2, 30).astype(np.uint8)   # payload bits
>>> chips = np.where(code & 1, -1.0, 1.0)    # 0 -> +1, 1 -> -1
>>> syms = np.where(bits == 1, -1.0, 1.0)             # BPSK symbols
>>> tx = np.concatenate(
...     [np.repeat(s * chips, 4) for s in syms]).astype(np.complex64)
>>> d = BurstDespreader(code, sf=31, sps=4)
>>> sym = d.steps(tx)                        # one prompt/symbol
>>> sym.shape
(30,)
>>> hard = (sym.real < 0).astype(np.uint8)            # BPSK decision
>>> float(np.mean(hard != bits))             # payload recovered
0.0

steps_max_out

steps_max_out() -> int

Max output length steps() can produce for the current state. Use to size the out= buffer.

bits

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

Despread a cf32 block; emit one hard BPSK bit per code period.

Same streaming kernel as burst_despreader_steps(), but emits the hard decision crealf(prompt) >= 0 instead of the complex symbol.

Without out=, the returned array is a view into a buffer reused on the next call (see bits_max_out() to size an out= buffer for an independent, alias-free result).

Parameters:

Name Type Description Default
x NDArray[complex64]

Input CF32 samples, length x_len.

required
out NDArray[uint8]

Caller-provided output buffer, at least max(bits_max_out(), len(x)) elements.

...

Returns:

Type Description
NDArray[uint8]

Number of bits written.

bits_max_out

bits_max_out() -> int

Max output length bits() can produce for the current state. Use to size the out= buffer.

set_acq

set_acq(acq_code: NDArray[uint8], acq_reps: int) -> None

Enable preamble-aided pull-in: track acq_reps periods of the (distinct) acq_code coherently before despreading the payload with the data code. Call before feeding the burst; clears when the preamble is consumed.

Track acq_reps periods of acq_code coherently (the unmodulated, repeated acquisition preamble — a full ±pi phase discriminator, so the loops pull in even a wide residual) before switching to the data code for the payload. Call before feeding the burst; the acq mode clears automatically once the preamble is consumed, and re-arms on burst_despreader_reset(). NB: set_acq re-arms the PREAMBLE only — the cumulative burst statistics (lock_metric / snr_est / lock_stat / stat_n) are re-armed by burst_despreader_reset(); call it between bursts.

Parameters:

Name Type Description Default
acq_code NDArray[uint8]

Acquisition code (0/1), length acq_code_len; copied.

required
acq_reps int

Number of acq-code periods in the preamble.

required

Examples:

>>> import numpy as np
>>> from doppler.dsss import BurstDespreader
>>> rng = np.random.default_rng(5)
>>> acq = rng.integers(0, 2, 128).astype(np.uint8)    # long acq code
>>> data_code = rng.integers(0, 2, 32).astype(np.uint8)
>>> pbits = rng.integers(0, 2, 40).astype(np.uint8)
>>> asig = np.where(acq & 1, -1.0, 1.0)
>>> dch = np.where(data_code & 1, -1.0, 1.0)
>>> psyms = np.where(pbits == 1, -1.0, 1.0)
>>> pre = np.concatenate([np.repeat(asig, 4) for _ in range(4)])
>>> pay = np.concatenate([np.repeat(s * dch, 4) for s in psyms])
>>> burst = np.concatenate([pre, pay]).astype(np.complex64)
>>> d = BurstDespreader(data_code, sf=32, sps=4)
>>> d.set_acq(acq, 4)            # 4 preamble reps, pulls loops in
>>> out = d.bits(burst)          # preamble emits nothing
>>> out.shape                    # only the payload symbols come out
(40,)
>>> e = np.mean(out != pbits)
>>> round(float(min(e, 1.0 - e)), 4)
0.0

reset

reset() -> None

Re-seed the loops to the create-time phase/frequency; preserve config.

Restores the carrier NCO to the seed frequency and the code phase to the seed chip, zeroes the loop accumulators, and clears the cumulative burst read-backs (lock_metric / snr_est / lock_stat / stat_n) — the spreading code and bandwidths are kept. Call it between bursts so each burst's statistics start clean; a prior burst_despreader_set_acq() preamble is also re-armed.

Examples:

>>> import numpy as np
>>> from doppler.dsss import BurstDespreader
>>> rng = np.random.default_rng(1)
>>> code = rng.integers(0, 2, 31).astype(np.uint8)
>>> chips = np.where(code & 1, -1.0, 1.0)
>>> syms = np.where(rng.integers(0, 2, 30) == 1, -1.0, 1.0)
>>> tx = np.concatenate(
...     [np.repeat(s * chips, 4) for s in syms]).astype(np.complex64)
>>> d = BurstDespreader(code, sf=31, sps=4)
>>> first = d.bits(tx)
>>> d.reset()                          # re-arm for a new burst
>>> np.array_equal(first, d.bits(tx))  # same as a fresh object
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 BurstDespreader has already been destroyed.

Returns:

Type Description
int

Byte length of one serialized state blob.

get_state

get_state() -> bytes

Serialize this object's mutable state to bytes.

Captures exactly the state that evolves as the object runs, so a blob taken now and restored later resumes from this point. Construction parameters are not included: restore into an object built the same way.

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

Raises RuntimeError if the BurstDespreader has already been destroyed.

Returns:

Type Description
bytes

Opaque snapshot, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

Restore mutable state from a get_state() blob.

Overwrites the live state in place; the object keeps the parameters it was constructed with. Length is validated against state_bytes() before the blob is handed to the C core, and the core may reject it as well.

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

Enter a context manager, returning this object.

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

Returns:

Type Description
BurstDespreader

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

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.

...

DsssReceiver — the composed continuous receiver

The single-object form of Acquisition -> Dll(segments) -> RateConverter -> MpskReceiver: only code/chip_rate/symbol_rate are required, everything else defaults to this project's own validated values, with configure_search_raw/configure_lock_raw/configure_chain_raw as the power-user escape hatches. See the DsssReceiver gallery page for the full story this composes.

configure_search_raw bypasses the mislock-avoiding auto-sizer

See the gallery page's own warning before pinning a large doppler_bins directly — DsssReceiver always has symbol_rate set, so the default auto-sizing exists specifically to avoid a confirmed mislock failure mode that a raw pin bypasses.

import numpy as np
from doppler.dsss import DsssReceiver

code = np.random.default_rng(1).integers(0, 2, 127).astype(np.uint8)
rx = DsssReceiver(code, chip_rate=3.0e6, symbol_rate=2100.0)
x = np.zeros(1024, dtype=np.complex64)   # no real signal here -- just show the call
syms = rx.steps(x)   # empty while searching; demodulated symbols once locked
rx.tracking          # 0 = searching, 1 = locked and demodulating

DsssReceiver

Create a DSSS receiver in the searching state.

Parameters:

Name Type Description Default
code NDArray[uint8]

Spreading code, one 0/1 chip per element (0 -> +1, 1 -> -1 BPSK; only the low bit is used, so pass 0/1, not +/-1).

required
chip_rate float

Chip rate, Hz. Required.

1000000.0
symbol_rate float

Data-symbol rate, Hz. Required — passed straight to the embedded Acquisition's own symbol_rate (diagnostic there; see acq_create_continuous()).

1000.0
spc int

Samples/chip (front-end oversample); default 2 (fs = 2x chip_rate).

2
m int

PSK order, 2/4/8; default 2 (BPSK).

2
cn0_dbhz float

Design C/N0 for acquisition sizing, dB-Hz; default 55.0.

55.0
pfa float

Acquisition false-alarm target; default 1e-3.

1e-3
pd float

Acquisition detection-probability target; default 0.9.

0.9
doppler_uncertainty float

One-sided Doppler search half-range, Hz; default 100.0.

100.0
segments int

Dll's own non-coherent partial-correlation count per code epoch — its tracking- robustness parameter, independent of sps (see the module docstring); default 4, this story's own validated sweet spot.

4
sps int

MpskReceiver's samples/symbol, reached by an internal RateConverter bridging the despreader's own partial rate to this rate; default 8, MpskReceiver's own constructor default.

8
differential int

MpskReceiver's differential (rotation- invariant) demap; default 0 (coherent).

0

Raises:

Type Description
ValueError

If construction fails. The exception message is ``DsssReceiver: invalid parameter (need a non-empty code, chip_rate > 0, symbol_rate > 0, spc

= 1, m in {2,4,8}, segments >= 1, sps >= 2 -- sps = 1 cannot carry an m_out, whose smallest legal value is 2 and which MpskReceiver requires sps to reach)``.

Examples:

>>> import numpy as np
>>> from doppler.dsss import DsssReceiver
>>> from doppler.wfm import Gold
>>> sf, chip, sym, spc = 1023, 3.0e6, 2100.0, 2
>>> fs, te, tsym = chip * spc, sf * spc, chip * spc / sym
>>> code = np.asarray(Gold().generate(sf)).astype(np.uint8)
>>> csign = np.where(code & 1, -1.0, 1.0)
>>> rng = np.random.default_rng(6)
>>> n = int(400 * tsym) + 2 * te            # 400 BPSK data symbols
>>> idx = np.arange(n)
>>> data = (rng.integers(0, 2, 404) * 2 - 1).astype(float)
>>> si = np.clip((idx / tsym).astype(int), 0, 403)
>>> spread = data[si] * csign[(idx // spc) % sf]        # DSSS chips
>>> sig = spread * np.exp(2j * np.pi * (50.0 / fs) * idx)  # +50 Hz
>>> pre = 3 * te                     # noise-only lead-in, pre-signal
>>> sigma = np.sqrt(fs / 10 ** (90.0 / 10))            # ~90 dB-Hz C/N0
>>> noise = (sigma / np.sqrt(2)) * (rng.standard_normal(pre + n)
...          + 1j * rng.standard_normal(pre + n))
>>> x = (np.concatenate([np.zeros(pre), sig]).astype(np.complex64)
...      + noise.astype(np.complex64))
>>> rx = DsssReceiver(code, chip_rate=chip, symbol_rate=sym, spc=spc,
...                   cn0_dbhz=55.0, doppler_uncertainty=100.0)
>>> syms = [rx.steps(x[p:p + te]) for p in range(0, len(x) - te, te)]
>>> syms = np.concatenate([s for s in syms if len(s)])
>>> rx.tracking                  # acquired, now demodulating
1
>>> len(syms) > 300              # a few hundred symbols recovered
True

Nearly all the energy lands on I, so the BPSK phase is resolved:

>>> bool(np.mean(syms.real**2) > 10 * np.mean(syms.imag**2))
True

tracking property

tracking: int

0 = searching, 1 = locked and demodulating.

doppler_hz property

doppler_hz: float

Doppler hz.

cn0_dbhz_est property

cn0_dbhz_est: float

Cached from the winning acquisition hit.

segments property

segments: int

Dll's own tracking parameter.

sps property

sps: int

MpskReceiver's own samples/symbol.

n property

n: int

MpskReceiver's own carrier-arm count.

chip_phase property

chip_phase: float

Dll's live tracked code phase (chips); 0.0 while searching.

code_rate property

code_rate: float

Dll's own tracking-quality indicator; 1.0 while searching.

lock property

lock: float

MpskReceiver's carrier lock EMA; 0.0 while searching.

norm_freq property

norm_freq: float

MpskReceiver's tracked carrier frequency; 0.0 while searching.

steps

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

Stream raw cf32 samples through the receiver. While searching, samples feed the embedded Acquisition and nothing is emitted (an empty array is normal, not an error). The moment a hit fires, Dll/RateConverter/MpskReceiver are built and seeded from it -- the same phase-inversion hand-off and rate-bridging this project's async-DSSS-receiver gallery story validated by hand -- and the unconsumed tail of this same call is handed straight to them, so no samples are dropped at the transition. While tracking, samples feed Dll -> RateConverter -> MpskReceiver in sequence and demodulated symbols are returned. Accepts any block size; state carries across calls.

While searching, samples feed the embedded Acquisition and nothing is emitted (0 return is normal, not an error). The moment a hit fires, Dll/RateConverter/MpskReceiver are built and seeded from it, and the unconsumed tail of THIS call — computed exactly from acq->samples_consumed, no samples dropped or double-fed — is handed straight to them in the same call. While tracking, samples feed Dll -> RateConverter -> MpskReceiver in sequence. Accepts any block size; state carries across calls (Acquisition/Dll/ RateConverter/MpskReceiver are all already block-size invariant, so this object needs no ring-buffering of its own).

Parameters:

Name Type Description Default
x NDArray[complex64]

Input cf32 samples.

required
out NDArray[complex64] | None

Output symbols; caller provides max_out capacity.

None

Returns:

Type Description
NDArray[complex64]

Number of symbols written (0 while searching, or while tracking with not yet a full symbol's worth of input).

Examples:

>>> import numpy as np
>>> from doppler.dsss import DsssReceiver
>>> from doppler.wfm import Gold
>>> sf, chip, sym, spc = 1023, 3.0e6, 2100.0, 2
>>> fs, te, tsym = chip * spc, sf * spc, chip * spc / sym
>>> code = np.asarray(Gold().generate(sf)).astype(np.uint8)
>>> csign = np.where(code & 1, -1.0, 1.0)
>>> rng = np.random.default_rng(6)
>>> n = int(400 * tsym) + 2 * te            # 400 BPSK data symbols
>>> idx = np.arange(n)
>>> data = (rng.integers(0, 2, 404) * 2 - 1).astype(float)
>>> si = np.clip((idx / tsym).astype(int), 0, 403)
>>> spread = data[si] * csign[(idx // spc) % sf]        # DSSS chips
>>> sig = spread * np.exp(2j * np.pi * (50.0 / fs) * idx)  # +50 Hz
>>> pre = 3 * te                     # noise-only lead-in, pre-signal
>>> sigma = np.sqrt(fs / 10 ** (90.0 / 10))            # ~90 dB-Hz C/N0
>>> noise = (sigma / np.sqrt(2)) * (rng.standard_normal(pre + n)
...          + 1j * rng.standard_normal(pre + n))
>>> x = (np.concatenate([np.zeros(pre), sig]).astype(np.complex64)
...      + noise.astype(np.complex64))
>>> rx = DsssReceiver(code, chip_rate=chip, symbol_rate=sym, spc=spc,
...                   cn0_dbhz=55.0, doppler_uncertainty=100.0)
>>> syms = [rx.steps(x[p:p + te]) for p in range(0, len(x) - te, te)]
>>> syms = np.concatenate([s for s in syms if len(s)])
>>> rx.tracking                  # acquired and now demodulating
1
>>> len(syms) > 300              # a few hundred symbols recovered
True

Nearly all the energy lands on I, so the BPSK phase is resolved:

>>> bool(np.mean(syms.real**2) > 10 * np.mean(syms.imag**2))
True

steps_max_out

steps_max_out() -> int

Largest number of samples steps() can return in the current state.

Size an out= buffer with this before calling steps(), or use it to allocate one up front. The bound is this object's own: what it depends on is a property of the algorithm, so a header block on steps_max_out() replaces this text.

Returns:

Type Description
int

Upper bound on the output length; the actual call may return fewer.

configure_search_raw

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

Pin the embedded Acquisition's search grid directly, bypassing the symbol_rate-driven auto-sizing -- the escape hatch for a power user who wants a specific (doppler_bins, n_noncoh). Only meaningful while searching.

Parameters:

Name Type Description Default
doppler_bins int

Number of Doppler window tiles to search (>= 1); capped by the create-time doppler_uncertainty span (one tile per code-epoch Doppler bin width).

required
n_noncoh int

Non-coherent looks accumulated per grid cell (1..256); more looks buys sensitivity at the cost of dwell, replacing the auto-sized count.

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.dsss import DsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = DsssReceiver(code, chip_rate=3.0e6, symbol_rate=2100.0, spc=2)
>>> rx.configure_search_raw(doppler_bins=1, n_noncoh=16)  # pin it
>>> rx.tracking                # still searching, on the pinned grid
0

configure_lock_raw

configure_lock_raw(
    up_thresh: float,
    down_thresh: float,
    n_looks: int,
    alpha: float,
    n_up: int,
    n_down: int,
) -> None

Re-tune the embedded Dll's code-lock detector directly. Only meaningful once tracking has begun; a no-op while searching.

Parameters:

Name Type Description Default
up_thresh float

CFAR-statistic level to declare code lock (hit when the statistic exceeds it).

required
down_thresh float

Level below which a look is a miss; choose <= up_thresh for level hysteresis.

required
n_looks int

Looks per decision — the DLL's non-coherent integration depth feeding one statistic.

required
alpha float

EMA smoothing coefficient on the lock statistic (0..1); smaller is smoother/slower.

required
n_up int

Consecutive hits required to declare lock.

required
n_down int

Consecutive misses required to drop lock.

required

Examples:

>>> import numpy as np
>>> from doppler.dsss import DsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = DsssReceiver(code, chip_rate=3.0e6, symbol_rate=2100.0, spc=2)
>>> rx.configure_lock_raw(up_thresh=0.4, down_thresh=0.2, n_looks=20,
...                       alpha=0.1, n_up=5, n_down=3)
>>> rx.tracking                # a no-op until a hit builds the Dll
0

configure_chain_raw

configure_chain_raw(
    segments: int, sps: int, n: int
) -> None

Pin the despread/resample/demod grid directly, bypassing the create-time segments/sps defaults -- segments (Dll's tracking parameter) and sps/n (MpskReceiver's rate/carrier-arm parameters) stay independently overridable here, still bridged by a freshly-sized RateConverter, never coupled to each other. Only meaningful once tracking; rebuilds the chain with every replacement allocated first, so a failed pin leaves the receiver on its prior grid.

The escape hatch for the one composition-specific knob this object adds beyond its children's own: segments (Dll's tracking parameter) and sps/n (MpskReceiver's sample-rate/carrier-arm parameters) are indepen­dently overridable here, still bridged by a freshly-sized RateConverter — never coupled to each other (see the module docstring). Rebuilds dll/rc/rx with every replacement allocated first, only freeing and adopting the old ones once every allocation has succeeded (mirrors Acquisition's own acq_regrid() discipline) — a failed pin leaves the receiver tracking on its prior grid, not half-destroyed. Only meaningful once tracking (the grid defaults still apply to create-time auto-sizing for the next hit while searching; call dsss_receiver_create() with different segments/sps for that, or re-pin here again after the next hit).

Parameters:

Name Type Description Default
segments int

Dll tracking segments per code period.

required
sps int

MpskReceiver samples per symbol (the resample target).

required
n int

MpskReceiver's carrier-arm count; must divide sps.

required

Raises:

Type Description
ValueError

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

Examples:

>>> import numpy as np
>>> from doppler.dsss import DsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = DsssReceiver(code, chip_rate=3.0e6, symbol_rate=2100.0, spc=2)
>>> rx.configure_chain_raw(segments=6, sps=8, n=8)  # re-pin the chain
>>> rx.segments                       # tracking grid updated in place
6

reset

reset() -> None

Return to the searching state: resets the embedded Acquisition and frees Dll/RateConverter/MpskReceiver (rebuilt from scratch on the next hit).

Examples:

>>> import numpy as np
>>> from doppler.dsss import DsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = DsssReceiver(code, chip_rate=3.0e6, symbol_rate=2100.0, spc=2)
>>> rx.reset()                 # abort any lock, hunt from scratch
>>> (rx.tracking, rx.chip_phase)   # back to searching, all cleared
(0, 0.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 DsssReceiver has already been destroyed.

Returns:

Type Description
int

Byte length of one serialized state blob.

get_state

get_state() -> bytes

Serialize this object's mutable state to bytes.

Captures exactly the state that evolves as the object runs, so a blob taken now and restored later resumes from this point. Construction parameters are not included: restore into an object built the same way.

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

Raises RuntimeError if the DsssReceiver has already been destroyed.

Returns:

Type Description
bytes

Opaque snapshot, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

Restore mutable state from a get_state() blob.

Overwrites the live state in place; the object keeps the parameters it was constructed with. Length is validated against state_bytes() before the blob is handed to the C core, and the core may reject it as well.

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

Enter a context manager, returning this object.

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

Returns:

Type Description
DsssReceiver

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

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-oriented front door to the shared acquisition engine: a bounded preamble is searched over a (Doppler, code phase) grid and the peak is reported once, rather than the continuous streaming push of Acquisition. Both wrap the same stateless kernel; the two front doors differ only in how the capture is fed and when the estimate is emitted.

BurstAcquisition

Create a burst-mode acquisition engine (forwards to acq_create_burst() -- see its doc comment in acq_core.h for the full physics).

Parameters:

Name Type Description Default
code NDArray[uint8]

PN chips (0/1), length code_len.

required
reps int

Max coherent code repetitions (>= 1).

1
spc int

Samples per chip (>= 1).

4
chip_rate float

Chip rate in Hz (> 0).

1000000.0
cn0_dbhz float

Carrier-to-noise density in dB-Hz (> 0).

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

CFAR mode index: 0=mean, 1=median, 2=min, 3=max.

"mean"

Examples:

>>> import numpy as np
>>> from doppler.dsss 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(code, reps=8, spc=4, chip_rate=1e6,
...                      cn0_dbhz=50.0)
>>> b.push(burst)[0][:2]      # detects (Doppler bin, code phase)
(0, 17)

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

Code-phase hypotheses searched (= sf*spc, one code period).

doppler_bins property

doppler_bins: int

Coherent depth chosen: the slow-time FFT length in code reps (<= reps), unless doppler_uncertainty exceeds the native span, in which case this reports the wideband window-tile count instead (coherent depth forced to 1 -- see acq_core.h's file doc comment).

sf property

sf: int

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

spc property

spc: int

Samples per chip (chip-rate oversample factor).

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

Bonferroni 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) = chip_rate * spc.

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.

reset

reset() -> None

Drain the input ring and reset the coherent accumulator.

Forwards to acq_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.dsss 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(code, reps=8, spc=4, chip_rate=1e6,
...                      cn0_dbhz=50.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, code phase)
(0, 17)

push

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

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

Forwards to acq_push() on the embedded engine (see its doc comment in acq_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 acq_result_t.

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

Number of events written (0 … max_results).

Examples:

>>> import numpy as np
>>> from doppler.dsss 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(code, reps=8, spc=4, chip_rate=1e6,
...                      cn0_dbhz=50.0)
>>> b.push(burst)[0][:2]      # (Doppler bin, code phase)
(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 acq_configure_search_raw() on the embedded engine (see its doc comment in acq_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, ACQ_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.dsss 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(code, reps=8, spc=4, chip_rate=1e6,
...                      cn0_dbhz=50.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 acq_set_max_peaks() on the embedded engine (see its doc comment in acq_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 one chip 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 … ACQ_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.dsss import BurstAcquisition
>>> code = (np.arange(31) * 5 % 2).astype(np.uint8)
>>> b = BurstAcquisition(code, reps=8, spc=4, chip_rate=1e6,
...                      cn0_dbhz=50.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 exactly the state that evolves as the object runs, so a blob taken now and restored later resumes from this point. Construction parameters are not included: restore into an object built the same way.

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

Raises RuntimeError if the 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; the object keeps the parameters it was constructed with. Length is validated against state_bytes() before the blob is handed to the C core, and the core may reject it as well.

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

Preamble PN chips (0/1), length acq_code_len.

required
burst_len int

Samples in one burst -- what gets captured.

8192
reps int

Preamble code repetitions.

5
spc int

Samples per chip.

4
chip_rate float

Chip rate, Hz.

1000000.0
cn0_dbhz float

C/N0 the search is sized for, dB-Hz.

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

CFAR reference: 0=mean, 1=median, 2=min, 3=max.

"mean"

Raises:

Type Description
ValueError

If construction fails. The exception message is BurstCapture: invalid parameter (need non-empty acq_code, reps >= 1, spc >= 1, chip_rate > 0, burst_len >= 1, cn0_dbhz >= 0, 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_predicted < 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.dsss import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> cap = BurstCapture(code, burst_len=512, reps=4, spc=2)
>>> cap.burst_len
512
>>> cap.retain_span == cap.refine_span + cap.burst_len
True

preamble_start property

preamble_start: int

Stream-absolute sample index the most recent window's preamble starts at. NEVER LATE: a window that began after the preamble has destroyed the burst, so the refine stage's obligation is to be early-or-exact and to say how early.

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 = chip_rate/(sf*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.

refine_margin property

refine_margin: float

The refine stage's own confidence: the best rival code period's score over the winner's. The envelope is (reps-1)/reps when the right repetition wins, so compare against THAT and never a constant -- the floor rises with depth (0.55 at reps=2, 0.77 at 4, 0.94 at 16). Near 1 means the period was not resolved, which nothing else in the chain can see.

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

pd_predicted property

pd_predicted: float

Detection probability the sized grid actually predicts at cn0_dbhz. The number behind underpowered, and the one to compare against the pd that was asked for.

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 is the usual case. 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.

doppler_bins property

doppler_bins: int

Doppler hypotheses searched — the coherent depth the sizer chose, bounded by reps. configure_search_raw is what 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

Code-phase hypotheses per Doppler row: one segment in samples, sf * spc.

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 the history ring refused, lifetime. 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.

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.dsss import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> cap = BurstCapture(code, burst_len=512, reps=4, spc=2)
>>> 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

Input.

required

Returns:

Type Description
int

Output.

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

Optional pre-allocated output buffer. When given, the result is written into it and the returned array is a view of exactly the samples produced; when omitted, a fresh array is allocated.

None

Returns:

Type Description
NDArray[Any]

Output.

Examples:

>>> import numpy as np
>>> from doppler.dsss import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> cap = BurstCapture(code, burst_len=512, reps=4, spc=2)
>>> _ = 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

Input.

required

Returns:

Type Description
int

Output.

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

Optional pre-allocated output buffer. When given, the result is written into it and the returned array is a view of exactly the samples produced; when omitted, a fresh array is allocated.

None

Returns:

Type Description
NDArray[Any]

Output.

Examples:

>>> import numpy as np
>>> from doppler.dsss import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> cap = BurstCapture(code, burst_len=512, reps=4, spc=2)
>>> 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

Input.

required

Returns:

Type Description
int

Output.

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

Input.

required
n_noncoh int

Input.

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.dsss import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> cap = BurstCapture(code, burst_len=512, reps=4, spc=2)
>>> 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

Input.

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.dsss import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> cap = BurstCapture(code, burst_len=512, reps=4, spc=2)
>>> _ = 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 deliberately survives, because a lost burst stays lost.

Examples:

>>> import numpy as np
>>> from doppler.dsss import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> cap = BurstCapture(code, burst_len=512, reps=4, spc=2)
>>> 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 exactly the state that evolves as the object runs, so a blob taken now and restored later resumes from this point. Construction parameters are not included: restore into an object built the same way.

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

Raises RuntimeError if the 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; the object keeps the parameters it was constructed with. Length is validated against state_bytes() before the blob is handed to the C core, and the core may reject it as well.

Raises TypeError if blob is not bytes, ValueError if its length differs from state_bytes() or the core rejects it, and RuntimeError if the BurstCapture 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__() -> 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.68 MB and all but ~20 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 21.6 kB 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
acq_code NDArray[uint8]

Preamble PN chips (0/1), length acq_code_len.

required
burst_len int

Samples in one burst -- what gets captured.

8192
reps int

Preamble code repetitions.

5
spc int

Samples per chip.

4
chip_rate float

Chip rate, Hz.

1000000.0
cn0_dbhz float

C/N0 the search is sized for, dB-Hz.

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

CFAR reference: 0=mean, 1=median, 2=min, 3=max.

"mean"

Raises:

Type Description
ValueError

If construction fails. The exception message is BurstCapture: invalid parameter (need non-empty acq_code, reps >= 1, spc >= 1, chip_rate > 0, burst_len >= 1, cn0_dbhz >= 0, 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_predicted < 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, tempfile, os
>>> from doppler.dsss import BurstCapture, PersistentBurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> path = os.path.join(tempfile.mkdtemp(), "ring.cf32")
>>> cap = PersistentBurstCapture(path, code, burst_len=512,
...                             reps=4, spc=2)
>>> ram = BurstCapture(code, burst_len=512, reps=4, spc=2)
>>> _ = cap.push(np.zeros(4096, dtype=np.complex64))
>>> # the look-back is in the file, so the blob stops carrying it
>>> ram.state_bytes() - cap.state_bytes() == ram.retain_span * 8
True
>>> os.path.getsize(path) > 0
True

preamble_start property

preamble_start: int

Stream-absolute sample index the most recent window's preamble starts at. NEVER LATE: a window that began after the preamble has destroyed the burst, so the refine stage's obligation is to be early-or-exact and to say how early.

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 = chip_rate/(sf*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.

refine_margin property

refine_margin: float

The refine stage's own confidence: the best rival code period's score over the winner's. The envelope is (reps-1)/reps when the right repetition wins, so compare against THAT and never a constant -- the floor rises with depth (0.55 at reps=2, 0.77 at 4, 0.94 at 16). Near 1 means the period was not resolved, which nothing else in the chain can see.

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

pd_predicted property

pd_predicted: float

Detection probability the sized grid actually predicts at cn0_dbhz. The number behind underpowered, and the one to compare against the pd that was asked for.

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 is the usual case. 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.

doppler_bins property

doppler_bins: int

Doppler hypotheses searched — the coherent depth the sizer chose, bounded by reps. configure_search_raw is what 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

Code-phase hypotheses per Doppler row: one segment in samples, sf * spc.

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 the history ring refused, lifetime. 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.

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.dsss import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> cap = BurstCapture(code, burst_len=512, reps=4, spc=2)
>>> 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

Input.

required

Returns:

Type Description
int

Output.

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

Optional pre-allocated output buffer. When given, the result is written into it and the returned array is a view of exactly the samples produced; when omitted, a fresh array is allocated.

None

Returns:

Type Description
NDArray[Any]

Output.

Examples:

>>> import numpy as np
>>> from doppler.dsss import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> cap = BurstCapture(code, burst_len=512, reps=4, spc=2)
>>> _ = 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

Input.

required

Returns:

Type Description
int

Output.

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

Optional pre-allocated output buffer. When given, the result is written into it and the returned array is a view of exactly the samples produced; when omitted, a fresh array is allocated.

None

Returns:

Type Description
NDArray[Any]

Output.

Examples:

>>> import numpy as np
>>> from doppler.dsss import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> cap = BurstCapture(code, burst_len=512, reps=4, spc=2)
>>> 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

Input.

required

Returns:

Type Description
int

Output.

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

Input.

required
n_noncoh int

Input.

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.dsss import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> cap = BurstCapture(code, burst_len=512, reps=4, spc=2)
>>> 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

Input.

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.dsss import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> cap = BurstCapture(code, burst_len=512, reps=4, spc=2)
>>> _ = 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 deliberately survives, because a lost burst stays lost.

Examples:

>>> import numpy as np
>>> from doppler.dsss import BurstCapture
>>> code = np.array([1, 1, 1, 0, 1, 0, 0], dtype=np.uint8)
>>> cap = BurstCapture(code, burst_len=512, reps=4, spc=2)
>>> 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 exactly the state that evolves as the object runs, so a blob taken now and restored later resumes from this point. Construction parameters are not included: restore into an object built the same way.

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

Raises RuntimeError if the 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; the object keeps the parameters it was constructed with. Length is validated against state_bytes() before the blob is handed to the C core, and the core may reject it as well.

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

...

AsyncDsssReceiver — the packaged continuous async receiver

AsyncDsssReceiver wraps the whole acquire → carrier-refine → track chain behind one steps() call for the continuous asynchronous waveform (non-integer chips/symbol). It carries two carrier loops — a pre-despread Costas that tracks the Doppler dynamics before the code loop, and a post-despread mop-up loop — plus carrier→code aiding for coupled clock Doppler and a binary symbol-lock indicator. See the gallery page AsyncDsssReceiver: the SPEC Waveform for an end-to-end decode through physically-coupled Doppler.

AsyncDsssReceiver

Create an AsyncDsssReceiver in the searching state.

Parameters:

Name Type Description Default
code NDArray[uint8]

Spreading code, one 0/1 chip per element (0 -> +1, 1 -> -1 BPSK; only the low bit is used, so pass 0/1, not +/-1).

required
chip_rate float

Chip rate, Hz. Required.

1000000.0
symbol_rate float

Data-symbol rate, Hz. Required.

1000.0
spc int

Samples/chip; default 2.

2
m int

PSK order, 2/4/8; default 2 (BPSK).

2
cn0_dbhz float

Design C/N0, dB-Hz; default 55.0 -- feeds BOTH the embedded Acquisition's own sizing AND (derated by refine_design_margin_db) CarrierAcquisition's design_snr.

55.0
pfa float

Acquisition false-alarm target; default 1e-3. Also CarrierAcquisition's own pfa.

1e-3
pd float

Acquisition detection-probability target; default 0.9. Also CarrierAcquisition's own pd.

0.9
doppler_uncertainty float

One-sided Doppler search half-range, Hz; default 100.0.

100.0
segments int

Live-tracking Dll's own segments; default 4.

4
sps int

MpskReceiver's samples/symbol; default 8.

8
differential int

MpskReceiver's differential demap; default 0 (coherent).

0
refine_max_error_db float

Max tolerable async-lookback correlation-power loss driving the refine-stage collection Dll's coherent-I&D window count via dll_lookback_segments(). Oversampling the epoch is required for the asynchronous data: the residual carrier rides a ~symbol_rate-wide data-modulated spectrum, so segments>1 (default yields 11 at tsamps=2046) samples it above Nyquist; segments=1 undersamples and aliases it. Default 0.5.

0.5
refine_samples_per_symbol int

CarrierAcquisition's own operating rate = this * symbol_rate; default 4.

4
refine_design_margin_db float

Empirical derating of cn0_dbhz before CarrierAcquisition's design_snr; default 14.0.

14.0
refine_n_fft int

CarrierAcquisition's own block size; default 64.

64
refine_zero_pad int

CarrierAcquisition's own zero_pad; default 8.

8
refine_sequential bool

CarrierAcquisition's own sequential mode; default false -- sequential mode's early per-block test fires on far too little averaging at SPEC's own Es/N0 floor (confirmed: as few as 4 blocks, 150-200+ Hz off); false waits the full design_snr-derived dwell_target, matching freq_refine.refine_seed_ carrier_acq()'s own validated default.

False
refine_max_n_blocks int

CarrierAcquisition's own give-up cap in sequential mode; default 100000.

100000
carrier_freq_hz float

Nominal RF carrier frequency, Hz, enabling carrier->code aiding; 0.0 (default) = off. When > 0, the coupled code-rate Doppler (carrier_offset/carrier_freq) is fed to the tracking Dll via dll_set_rate_aid() so the code loop rides a dilated clock the discriminator alone can't pull in at low SNR. Set to the receiver's own downlink RF frequency for a physically-coupled Doppler capture.

0.0
lost_confirm_s float

Release rule: both lock flags down, continuously, for longer than this many seconds puts the receiver in the lost state (see get_lost()). Size it past the longest fade the link must ride. The clock also runs from the first tracking sample, when neither flag is up yet, so a hand-off that never locks within the interval is released the same way as an emitter that leaves. Default 0.0 = never -- the searching flavor's exit is reset(), as before.

0.0

Examples:

>>> import numpy as np
>>> from doppler.dsss import AsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> sf, chip, sym, spc = 1023, 3.069e6, 2700.0, 2
>>> fs, te, tsym = chip * spc, sf * spc, chip * spc / sym
>>> code = np.asarray(Gold().generate(sf)).astype(np.uint8)
>>> csign = np.where(code & 1, -1.0, 1.0)
>>> rng = np.random.default_rng(21)
>>> n = int(600 * tsym) + 4 * te            # 600 async BPSK symbols
>>> idx = np.arange(n)
>>> data = (rng.integers(0, 2, 604) * 2 - 1).astype(float)
>>> si = np.clip((idx / tsym).astype(int), 0, 603)
>>> t = idx / fs

DSSS chips on a carrier sweeping at 500 Hz/s — the ramp the async receiver has to track:

>>> sig = (data[si] * csign[(idx // spc) % sf]
...        * np.exp(1j * 2 * np.pi * 0.5 * 500.0 * t * t))
>>> cn0 = 20.0 + 10 * np.log10(sym)         # Es/N0 = 20 dB
>>> sigma = np.sqrt(fs / 10 ** (cn0 / 10))
>>> pre = 5 * te                            # noise-only lead-in
>>> noise = (sigma / np.sqrt(2)) * (rng.standard_normal(pre + n)
...          + 1j * rng.standard_normal(pre + n))
>>> x = (np.concatenate([np.zeros(pre), sig]).astype(np.complex64)
...      + noise.astype(np.complex64))
>>> rx = AsyncDsssReceiver(
...     code, chip_rate=chip, symbol_rate=sym, spc=spc,
...     cn0_dbhz=cn0, doppler_uncertainty=500.0)
>>> syms = [rx.steps(x[p:p + te]) for p in range(0, len(x) - te, te)]
>>> syms = np.concatenate([s for s in syms if len(s)])
>>> rx.tracking                  # searched, refined, now tracking
1
>>> len(syms) > 300              # symbols recovered under the ramp
True

Nearly all the energy lands on I, so the BPSK phase is resolved:

>>> bool(np.mean(syms.real**2) > 10 * np.mean(syms.imag**2))
True

refine_min_blocks property

refine_min_blocks: int

Floor on the refine's dwell, blocks (default 7); set with set_refine_min_blocks().

tracking property

tracking: int

1 once the live tracking chain is built and demodulating; 0 while searching or refining.

refining property

refining: int

1 while the refine stage (CarrierAcquisition collection) is active; 0 while searching or tracking.

idle property

idle: int

1 while waiting for a seed (the hand-off flavor before seed() or after reset()); 0 in every other state.

lost property

lost: int

1 once the release rule has fired: both lock flags were down, continuously, for longer than lost_confirm_s while tracking. The loops have stopped and samples are discarded; the holder releases the assignment and calls reset(). Always 0 with lost_confirm_s = 0.

doppler_hz property

doppler_hz: float

The current best Doppler estimate: the coarse handoff value while refining, the CarrierAcquisition-refined value once tracking.

cn0_dbhz_est property

cn0_dbhz_est: float

Cached from the winning acquisition hit.

segments property

segments: int

Live-tracking Dll's own segments -- distinct from refine_segments above (see the module docstring / dll_lookback_segments()'s own doc on the WINDOWS vs TRACK_WINDOWS split).

sps property

sps: int

MpskReceiver's own samples/symbol.

n property

n: int

MpskReceiver's own carrier-arm count.

chip_phase property

chip_phase: float

Live Dll code phase in chips, Dll's own instantaneous-phase convention (the mirror image of acq_result_t::code_phase's correlation-lag convention -- see acq_build_handoff()'s doc comment).

code_rate property

code_rate: float

Live Dll code rate: chips advanced per nominal chip (~1.0).

lock property

lock: float

decision rule on lock_metric: thresholds + verify counters, stepped per symbol.

norm_freq property

norm_freq: float

Smoothed carrier estimate (integrator only, cycles/sample of the MpskReceiver output rate); lags a Doppler ramp by the constant Type-II ramp error.

nco_freq property

nco_freq: float

Live carrier loop-filter output = NCO frequency command (cycles/sample of the MpskReceiver output rate): its mean tracks a Doppler ramp with no lag, its variance is the carrier loop stress.

locked property

locked: int

Binary receiver lock: the hysteretic (up/down verify-counted) lock detector on the emitted symbols -- declared when lock_metric stays >= lock_threshold for the up-count and dropped below it for the down-count.

lock_metric property

lock_metric: float

Symbol-lock metric: SNR-weighted running mean of the BPSK lock signal (I^2-Q^2)/(I^2+Q^2) = cos(2*phi) over the emitted symbols (locked -> ~+1). Drives locked; exposed for engineering debug.

lock_threshold property

lock_threshold: float

The lock_metric declare threshold locked latches above (the lockdet up_thresh); exposed alongside lock_metric for engineering debug.

car_last_error property

car_last_error: float

Pre-despread Costas phase discriminator (rad): the residual carrier phase loop 1 (de-rotates before the Dll) is not nulling. Engineering debug.

car_nco_freq property

car_nco_freq: float

Loop 1 (pre-despread Costas) loop-filter output = NCO frequency command, cycles/sample of the front-end (chip_rate*spc) rate. Engineering debug.

mpsk_last_error property

mpsk_last_error: float

MpskReceiver carrier phase discriminator (rad): the residual carrier phase loop 2 (post-despread) is not nulling. Engineering debug.

code_locked property

code_locked: int

Binary code-lock flag from the live tracking Dll's own verify-counted (pfa-tuned) lock detector -- the fundamental DSSS "am I despreading" lock, de-chattered by up/down hysteresis.

steps

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

Stream raw cf32 samples through the receiver. While searching, samples feed the embedded Acquisition and nothing is emitted. On a hit, the refine stage (a frozen-carrier Dll collection feeding CarrierAcquisition) is built and seeded from it, and the unconsumed tail of this call is handed straight to it -- no samples dropped. Once CarrierAcquisition reports ready (or its own give-up cap is reached), the live tracking chain (Dll + per-partial Costas + RateConverter + MpskReceiver) is built fresh, seeded from the ORIGINAL handoff chip phase and the refined-or-unrefined Doppler estimate, and demodulated symbols are returned from then on. Accepts any block size; state carries across calls.

Drives the search -> refine -> track state machine. While searching or refining, nothing is emitted (an empty return is normal, not an error): a hit seeds the frozen-carrier refine chain, CarrierAcquisition sharpens the coarse Doppler estimate, and only once it is ready (or gives up) is the live tracking chain built and demodulation begins. Accepts any block size; state carries across calls, so a capture can be fed in frames of any length with no seam. Idle (hand-off mode, before a seed) and lost (after the release rule fires) consume the samples and emit nothing, so the feeding loop is the same in every state; while tracking, the release clock runs on the two lock flags after every call (see lost_confirm_s). Under SPEC's coupled offset + 500 Hz/s Doppler ramp the pre-despread Costas removes the full carrier dynamics before the code loop, so the recovered constellation lands cleanly on the BPSK real axis.

Parameters:

Name Type Description Default
x NDArray[complex64]

Input cf32 samples.

required
out NDArray[complex64] | None

Output symbols; caller provides max_out capacity.

None

Returns:

Type Description
NDArray[complex64]

Number of symbols written (0 while searching/refining, or while tracking with not yet a full symbol's worth of input).

Examples:

>>> import numpy as np
>>> from doppler.dsss import AsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> sf, chip, sym, spc = 1023, 3.069e6, 2700.0, 2
>>> fs, te, tsym = chip * spc, sf * spc, chip * spc / sym
>>> code = np.asarray(Gold().generate(sf)).astype(np.uint8)
>>> csign = np.where(code & 1, -1.0, 1.0)
>>> rng = np.random.default_rng(21)
>>> n = int(600 * tsym) + 4 * te            # 600 async BPSK symbols
>>> idx = np.arange(n)
>>> data = (rng.integers(0, 2, 604) * 2 - 1).astype(float)
>>> si = np.clip((idx / tsym).astype(int), 0, 603)
>>> t = idx / fs

DSSS chips on a carrier sweeping at 500 Hz/s — the ramp the async receiver has to track:

>>> sig = (data[si] * csign[(idx // spc) % sf]
...        * np.exp(1j * 2 * np.pi * 0.5 * 500.0 * t * t))
>>> cn0 = 20.0 + 10 * np.log10(sym)         # Es/N0 = 20 dB
>>> sigma = np.sqrt(fs / 10 ** (cn0 / 10))
>>> pre = 5 * te                            # noise-only lead-in
>>> noise = (sigma / np.sqrt(2)) * (rng.standard_normal(pre + n)
...          + 1j * rng.standard_normal(pre + n))
>>> x = (np.concatenate([np.zeros(pre), sig]).astype(np.complex64)
...      + noise.astype(np.complex64))
>>> rx = AsyncDsssReceiver(
...     code, chip_rate=chip, symbol_rate=sym, spc=spc,
...     cn0_dbhz=cn0, doppler_uncertainty=500.0)
>>> syms = [rx.steps(x[p:p + te]) for p in range(0, len(x) - te, te)]
>>> syms = np.concatenate([s for s in syms if len(s)])
>>> rx.tracking                  # searched, refined, now tracking
1
>>> len(syms) > 300              # symbols recovered under the ramp
True

Nearly all the energy lands on I, so the BPSK phase is resolved:

>>> bool(np.mean(syms.real**2) > 10 * np.mean(syms.imag**2))
True

steps_max_out

steps_max_out() -> int

Largest number of samples steps() can return in the current state.

Size an out= buffer with this before calling steps(), or use it to allocate one up front. The bound is this object's own: what it depends on is a property of the algorithm, so a header block on steps_max_out() replaces this text.

Returns:

Type Description
int

Upper bound on the output length; the actual call may return fewer.

seed

seed(
    chip_phase: float,
    doppler_hz_est: float,
    cn0_dbhz_est: float,
) -> None

Take a detection from outside and start refining from it: the hit's chip phase (Dll's instantaneous convention, at the next sample fed), coarse Doppler estimate and C/N0 estimate -- exactly what the searching flavor's own hit produces. Accepted while idle (hand-off flavor) or searching; refused on a receiver that already holds a seed (refining, tracking or lost -- reset() releases it) and for a chip_phase outside [0, code_len).

The hand-off of docs/design/async-dsss-receiver.md section 11.1: the three numbers a searcher's hit carries that this receiver uses -- acq_handoff_t's chip_phase, doppler_hz_est and cn0_dbhz_est -- exactly as its own hit would have produced them (the searching flavor's steps() calls this on its own hit). chip_phase is the code's instantaneous phase in chips, Dll's convention, at the FIRST sample of the next steps() call; the Python-side conversion from a lag is doppler.dsss.handoff. The refine chain is rebuilt from the seed and the state becomes refining; the unconsumed tail is the caller's to feed.

Refused (DP_ERR_INVALID, nothing changes) on a receiver that is not waiting for one -- refining, tracking or lost -- because "assigned once" is a property of the object, not of the caller's bookkeeping; reset() releases it. Accepted while idle (hand-off mode) or searching (the searching flavor: an outside hit simply beats its own). Also refused for a chip_phase outside [0, code_len) or a non-finite value.

Parameters:

Name Type Description Default
chip_phase float

Code phase at the next sample, chips, in [0, code_len).

required
doppler_hz_est float

Coarse Doppler estimate, Hz (the refine stage sharpens it).

required
cn0_dbhz_est float

The hit's C/N0 estimate, dB-Hz; reported back by get_cn0_dbhz_est() until tracking refreshes it.

required

Raises:

Type Description
ValueError

If the C call returns a non-zero status. The exception message is seed refused: the receiver already holds an assignment (refining, tracking or lost -- reset() releases it), or chip_phase is outside [0, code_len), with the return code appended (gh-869).

Examples:

>>> import numpy as np
>>> from doppler.dsss import HandoffAsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = HandoffAsyncDsssReceiver(code, chip_rate=3.069e6,
...                               symbol_rate=2700.0, spc=2)
>>> rx.seed(chip_phase=512.25, doppler_hz_est=-1500.0,
...         cn0_dbhz_est=48.0)
>>> (rx.idle, rx.refining, rx.doppler_hz, rx.cn0_dbhz_est)
(0, 1, -1500.0, 48.0)

Already assigned -- refused until reset():

>>> rx.seed(0.0, 0.0, 48.0)
Traceback (most recent call last):
    ...
ValueError: seed refused: ...
>>> rx.reset()

A chip phase must be inside the code, [0, code_len):

>>> rx.seed(1023.0, 0.0, 48.0)
Traceback (most recent call last):
    ...
ValueError: seed refused: ...

status

status() -> ReceiverStatus

One consistent picture of the receiver, by value (design section 11.3): state, where the emitter is now (the whole carrier estimate -- loop 1's plus what loop 2 took up beyond it -- only as good as locked; chip phase, code rate, C/N0), both lock flags with the symbol-lock metric and threshold, both residual carrier errors, and the two clocks in input samples (since the state was entered; both flags down without a break). Read on demand by the holder of a pool -- the one-at-a-time properties are the same fields' other face. No timestamp: the holder owns the sample clock and stamps it.

Cheap and allocation-free: every field is a read of live state. The one-at-a-time getters below report the same fields; this is the face a pool holder uses.

Returns:

Type Description
ReceiverStatus

The record, by value.

Examples:

>>> import numpy as np
>>> from doppler.dsss import HandoffAsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = HandoffAsyncDsssReceiver(code, chip_rate=3.069e6,
...                               symbol_rate=2700.0, spc=2)
>>> st = rx.status()
>>> (st.state, st.doppler_hz, st.code_locked, st.locked)   # idle
(3, 0.0, 0, 0)
>>> rx.seed(chip_phase=100.0, doppler_hz_est=-250.0, cn0_dbhz_est=50.0)
>>> st = rx.status()
>>> (st.state, round(st.doppler_hz, 6), st.cn0_dbhz_est)  # refining
(1, -250.0, 50.0)
>>> _ = rx.steps(np.zeros(2046, np.complex64))
>>> rx.status().state_samples                             # since seed
2046

configure_search_raw

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

Pin the embedded Acquisition's search grid directly, bypassing the symbol_rate-driven auto-sizing. Only meaningful while searching.

Parameters:

Name Type Description Default
doppler_bins int

Number of Doppler window tiles to search (>= 1); capped by the create-time doppler_uncertainty span (one tile per code-epoch Doppler bin width).

required
n_noncoh int

Non-coherent looks accumulated per grid cell (1..256); more looks buys sensitivity at the cost of dwell, replacing the auto-sized count.

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.dsss import AsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = AsyncDsssReceiver(code, chip_rate=3.069e6, symbol_rate=2700.0,
...                        spc=2, doppler_uncertainty=500.0)
>>> rx.configure_search_raw(doppler_bins=1, n_noncoh=16)  # pin it
>>> rx.refining                # still searching, on the pinned grid
0

set_refine_min_blocks

set_refine_min_blocks(n_blocks: int) -> None

Floor the refine's dwell at n_blocks whatever the detection sizing asks (design section 12.16, #1265): CarrierAcquisition's dwell is sized for detection at the derated C/N0 and shortens as the C/N0 rises -- two blocks at 45 dB-Hz with the shipped margin -- while the noise of the estimate it hands the tracking chain does not shorten with it (210 Hz at two blocks against a chain that pulls in from a few hundred). The default of 7 blocks (42 ms) holds it to 77 Hz. Applied to the next refine chain built; 0 removes the floor; clamped by refine_max_n_blocks. Config, not running state.

CarrierAcquisition's dwell is sized for DETECTION at the derated C/N0 (cn0_dbhz - refine_design_margin_db), so it shortens as the C/N0 rises -- two blocks at 45 dB-Hz with the shipped margin -- while the noise of the estimate it hands the tracking chain does not shorten with it: 210 Hz at two blocks against a chain that pulls in from a few hundred, so one hand-over in sixty landed outside and tracked the code with the carrier never locked. Seven blocks (42 ms, the default and section 12.10's floor dwell) hold the estimate to 77 Hz. Applied to the next refine chain built -- a receiver already refining keeps its dwell. Config, not running state: not in the blob. n_blocks of 0 removes the floor; the value is clamped by refine_max_n_blocks where that cap is lower.

Parameters:

Name Type Description Default
n_blocks int

The floor, blocks.

required

Raises:

Type Description
ValueError

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

Examples:

>>> from doppler.dsss import AsyncDsssReceiver
>>> rx = AsyncDsssReceiver(code=[1, 0, 1, 1, 0, 0, 1], chip_rate=1e6,
...                        symbol_rate=1e6 / 28.0, spc=4, cn0_dbhz=60.0)
>>> rx.refine_min_blocks                     # the default floor
7
>>> rx.set_refine_min_blocks(12)
>>> rx.refine_min_blocks
12

configure_lock_raw

configure_lock_raw(
    up_thresh: float,
    down_thresh: float,
    n_looks: int,
    alpha: float,
    n_up: int,
    n_down: int,
) -> None

Re-tune the live-tracking Dll's code-lock detector directly. Only meaningful once tracking has begun; a no-op while searching or refining.

Parameters:

Name Type Description Default
up_thresh float

CFAR-statistic level to declare code lock (hit when the statistic exceeds it).

required
down_thresh float

Level below which a look is a miss; choose <= up_thresh for level hysteresis.

required
n_looks int

Looks per decision — the DLL's non-coherent integration depth feeding one statistic.

required
alpha float

EMA smoothing coefficient on the lock statistic (0..1); smaller is smoother/slower.

required
n_up int

Consecutive hits required to declare lock.

required
n_down int

Consecutive misses required to drop lock.

required

Examples:

>>> import numpy as np
>>> from doppler.dsss import AsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = AsyncDsssReceiver(code, chip_rate=3.069e6, symbol_rate=2700.0,
...                        spc=2, doppler_uncertainty=500.0)
>>> rx.configure_lock_raw(up_thresh=0.4, down_thresh=0.2, n_looks=20,
...                       alpha=0.1, n_up=5, n_down=3)
>>> rx.tracking                       # a no-op until tracking begins
0

configure_chain_raw

configure_chain_raw(
    segments: int, sps: int, n: int
) -> None

Pin the live-tracking despread/resample/demod grid directly, bypassing the create-time segments/sps defaults. Only meaningful once tracking; rebuilds the chain with every replacement allocated first, so a failed pin leaves the receiver on its prior grid.

Parameters:

Name Type Description Default
segments int

Live-tracking Dll segments per code period.

required
sps int

MpskReceiver samples per symbol (the resample target).

required
n int

MpskReceiver's carrier-arm count; must divide sps.

required

Raises:

Type Description
ValueError

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

Examples:

>>> import numpy as np
>>> from doppler.dsss import AsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = AsyncDsssReceiver(code, chip_rate=3.069e6, symbol_rate=2700.0,
...                        spc=2, doppler_uncertainty=500.0)
>>> rx.configure_chain_raw(segments=6, sps=8, n=8)  # re-pin the chain
>>> rx.segments                       # tracking grid updated in place
6

reset

reset() -> None

Return to the searching state: resets the embedded Acquisition and frees every refine-stage/track-stage child (rebuilt from scratch on the next hit).

Examples:

>>> import numpy as np
>>> from doppler.dsss import AsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = AsyncDsssReceiver(code, chip_rate=3.069e6, symbol_rate=2700.0,
...                        spc=2, doppler_uncertainty=500.0)
>>> rx.reset()                 # abort any lock, hunt from scratch
>>> (rx.tracking, rx.refining, rx.chip_phase)   # all cleared
(0, 0, 0.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 AsyncDsssReceiver has already been destroyed.

Returns:

Type Description
int

Byte length of one serialized state blob.

get_state

get_state() -> bytes

Serialize this object's mutable state to bytes.

Captures exactly the state that evolves as the object runs, so a blob taken now and restored later resumes from this point. Construction parameters are not included: restore into an object built the same way.

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

Raises RuntimeError if the AsyncDsssReceiver has already been destroyed.

Returns:

Type Description
bytes

Opaque snapshot, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

Restore mutable state from a get_state() blob.

Overwrites the live state in place; the object keeps the parameters it was constructed with. Length is validated against state_bytes() before the blob is handed to the C core, and the core may reject it as well.

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

Enter a context manager, returning this object.

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

Returns:

Type Description
AsyncDsssReceiver

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

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.

...

HandoffAsyncDsssReceiver — the same receiver, seeded from outside

The pool shape of the continuous multi-emitter design (§11): one searcher finds every emitter on a channel, and one of these per emitter tracks it. It is AsyncDsssReceiver with no search of its own — a view over the same core — so it starts idle, takes the searcher's detection through seed(chip_phase, doppler_hz_est, cn0_dbhz_est) (assigned once: a second seed is refused until reset(), which returns to idle), and runs the same refine → track chain. Its release rule is the design's §11.2: both lock flags down, continuously, for longer than lost_confirm_s sets lost, stops the loops, and leaves the holder to reset() it for its next seed. seed(), idle and lost exist on AsyncDsssReceiver too (an outside hit beats its own search); only the constructor differs.

HandoffAsyncDsssReceiver

Create a receiver in hand-off mode: idle, with no search of its own.

Parameters:

Name Type Description Default
code NDArray[uint8]

Spreading code, 0/1 chips (see async_dsss_receiver_create()).

required
chip_rate float

Chip rate, Hz. Required.

1000000.0
symbol_rate float

Data-symbol rate, Hz. Required.

1000.0
spc int

Samples/chip; default 2.

2
m int

PSK order, 2/4/8; default 2.

2
cn0_dbhz float

Design C/N0, dB-Hz; default 55.0 (derated by refine_design_margin_db into CarrierAcquisition's design_snr).

55.0
pfa float

CarrierAcquisition's false-alarm target; default 1e-3.

1e-3
pd float

CarrierAcquisition's detection target; default 0.9.

0.9
segments int

Live-tracking Dll's segments; default 4.

4
sps int

MpskReceiver's samples/symbol; default 8.

8
differential int

MpskReceiver's differential demap; default 0.

0
refine_max_error_db float

As async_dsss_receiver_create().

0.5
refine_samples_per_symbol int

As async_dsss_receiver_create().

4
refine_design_margin_db float

As async_dsss_receiver_create().

14.0
refine_n_fft int

As async_dsss_receiver_create().

64
refine_zero_pad int

As async_dsss_receiver_create().

8
refine_sequential bool

As async_dsss_receiver_create().

False
refine_max_n_blocks int

As async_dsss_receiver_create().

100000
carrier_freq_hz float

Nominal RF carrier for carrier->code aiding; 0.0 (default) = off.

0.0
lost_confirm_s float

Release rule, seconds of both flags down; default 2.0. 0 = never lost.

2.0

Examples:

>>> import numpy as np
>>> from doppler.dsss import Acquisition, HandoffAsyncDsssReceiver
>>> from doppler.dsss import bin_to_signed
>>> from doppler.dsss.handoff import dll_init_chip_from_acq
>>> from doppler.wfm import Gold
>>> sf, chip, sym, spc = 1023, 3.069e6, 2700.0, 2
>>> fs, te, tsym = chip * spc, sf * spc, chip * spc / sym
>>> code = np.asarray(Gold().generate(sf)).astype(np.uint8)
>>> csign = np.where(code & 1, -1.0, 1.0)
>>> rng = np.random.default_rng(21)
>>> n = int(600 * tsym) + 4 * te            # 600 async BPSK symbols
>>> idx = np.arange(n)
>>> data = (rng.integers(0, 2, 604) * 2 - 1).astype(float)
>>> si = np.clip((idx / tsym).astype(int), 0, 603)
>>> t = idx / fs
>>> sig = (data[si] * csign[(idx // spc) % sf]
...        * np.exp(1j * 2 * np.pi * 0.5 * 500.0 * t * t))
>>> cn0 = 20.0 + 10 * np.log10(sym)         # Es/N0 = 20 dB
>>> sigma = np.sqrt(fs / 10 ** (cn0 / 10))
>>> pre = 5 * te                            # noise-only lead-in
>>> noise = (sigma / np.sqrt(2)) * (rng.standard_normal(pre + n)
...          + 1j * rng.standard_normal(pre + n))
>>> x = (np.concatenate([np.zeros(pre), sig]).astype(np.complex64)
...      + noise.astype(np.complex64))

The search is a separate object -- in a pool, one searcher per channel serves every receiver on it. Its hit is a correlation lag and a Doppler bin; the two documented helpers turn those into the seed:

>>> acq = Acquisition(code, spc=spc, chip_rate=chip, symbol_rate=sym,
...                   cn0_dbhz=cn0, doppler_uncertainty=500.0)
>>> for p in range(0, len(x) - te, te):
...     hits = acq.push(x[p:p + te])
...     if hits:
...         break
>>> d_bin, lag, _, _, _, cn0_est, consumed = hits[0]
>>> chip_phase = dll_init_chip_from_acq(lag, spc, sf)
>>> res_hz = acq.doppler_res_hz
>>> doppler_hz = bin_to_signed(d_bin, acq.doppler_bins) * res_hz

The receiver never searched: it waits idle, takes the seed, and the samples from the hit onwards go to it.

>>> rx = HandoffAsyncDsssReceiver(
...     code, chip_rate=chip, symbol_rate=sym, spc=spc, cn0_dbhz=cn0)
>>> rx.idle
1
>>> rx.seed(chip_phase, doppler_hz, cn0_est)
>>> (rx.idle, rx.refining)
(0, 1)
>>> syms = [rx.steps(x[p:p + te])
...         for p in range(int(consumed), len(x) - te, te)]
>>> syms = np.concatenate([s for s in syms if len(s)])
>>> rx.tracking                  # refined and tracking, no search
1
>>> len(syms) > 300
True
>>> bool(np.mean(syms.real**2) > 10 * np.mean(syms.imag**2))
True

Assigned once: a second seed is refused until reset(), which in this mode returns to idle, not to searching.

>>> rx.seed(0.0, 0.0, cn0)
Traceback (most recent call last):
    ...
ValueError: seed refused: ...
>>> rx.reset()
>>> rx.idle
1

refine_min_blocks property

refine_min_blocks: int

Floor on the refine's dwell, blocks (default 7); set with set_refine_min_blocks().

tracking property

tracking: int

1 once the live tracking chain is built and demodulating; 0 while searching or refining.

refining property

refining: int

1 while the refine stage (CarrierAcquisition collection) is active; 0 while searching or tracking.

idle property

idle: int

1 while waiting for a seed (the hand-off flavor before seed() or after reset()); 0 in every other state.

lost property

lost: int

1 once the release rule has fired: both lock flags were down, continuously, for longer than lost_confirm_s while tracking. The loops have stopped and samples are discarded; the holder releases the assignment and calls reset(). Always 0 with lost_confirm_s = 0.

doppler_hz property

doppler_hz: float

The current best Doppler estimate: the coarse handoff value while refining, the CarrierAcquisition-refined value once tracking.

cn0_dbhz_est property

cn0_dbhz_est: float

Cached from the winning acquisition hit.

segments property

segments: int

Live-tracking Dll's own segments -- distinct from refine_segments above (see the module docstring / dll_lookback_segments()'s own doc on the WINDOWS vs TRACK_WINDOWS split).

sps property

sps: int

MpskReceiver's own samples/symbol.

n property

n: int

MpskReceiver's own carrier-arm count.

chip_phase property

chip_phase: float

Live Dll code phase in chips, Dll's own instantaneous-phase convention (the mirror image of acq_result_t::code_phase's correlation-lag convention -- see acq_build_handoff()'s doc comment).

code_rate property

code_rate: float

Live Dll code rate: chips advanced per nominal chip (~1.0).

lock property

lock: float

decision rule on lock_metric: thresholds + verify counters, stepped per symbol.

norm_freq property

norm_freq: float

Smoothed carrier estimate (integrator only, cycles/sample of the MpskReceiver output rate); lags a Doppler ramp by the constant Type-II ramp error.

nco_freq property

nco_freq: float

Live carrier loop-filter output = NCO frequency command (cycles/sample of the MpskReceiver output rate): its mean tracks a Doppler ramp with no lag, its variance is the carrier loop stress.

locked property

locked: int

Binary receiver lock: the hysteretic (up/down verify-counted) lock detector on the emitted symbols -- declared when lock_metric stays >= lock_threshold for the up-count and dropped below it for the down-count.

lock_metric property

lock_metric: float

Symbol-lock metric: SNR-weighted running mean of the BPSK lock signal (I^2-Q^2)/(I^2+Q^2) = cos(2*phi) over the emitted symbols (locked -> ~+1). Drives locked; exposed for engineering debug.

lock_threshold property

lock_threshold: float

The lock_metric declare threshold locked latches above (the lockdet up_thresh); exposed alongside lock_metric for engineering debug.

car_last_error property

car_last_error: float

Pre-despread Costas phase discriminator (rad): the residual carrier phase loop 1 (de-rotates before the Dll) is not nulling. Engineering debug.

car_nco_freq property

car_nco_freq: float

Loop 1 (pre-despread Costas) loop-filter output = NCO frequency command, cycles/sample of the front-end (chip_rate*spc) rate. Engineering debug.

mpsk_last_error property

mpsk_last_error: float

MpskReceiver carrier phase discriminator (rad): the residual carrier phase loop 2 (post-despread) is not nulling. Engineering debug.

code_locked property

code_locked: int

Binary code-lock flag from the live tracking Dll's own verify-counted (pfa-tuned) lock detector -- the fundamental DSSS "am I despreading" lock, de-chattered by up/down hysteresis.

steps

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

Stream raw cf32 samples through the receiver. While searching, samples feed the embedded Acquisition and nothing is emitted. On a hit, the refine stage (a frozen-carrier Dll collection feeding CarrierAcquisition) is built and seeded from it, and the unconsumed tail of this call is handed straight to it -- no samples dropped. Once CarrierAcquisition reports ready (or its own give-up cap is reached), the live tracking chain (Dll + per-partial Costas + RateConverter + MpskReceiver) is built fresh, seeded from the ORIGINAL handoff chip phase and the refined-or-unrefined Doppler estimate, and demodulated symbols are returned from then on. Accepts any block size; state carries across calls.

Drives the search -> refine -> track state machine. While searching or refining, nothing is emitted (an empty return is normal, not an error): a hit seeds the frozen-carrier refine chain, CarrierAcquisition sharpens the coarse Doppler estimate, and only once it is ready (or gives up) is the live tracking chain built and demodulation begins. Accepts any block size; state carries across calls, so a capture can be fed in frames of any length with no seam. Idle (hand-off mode, before a seed) and lost (after the release rule fires) consume the samples and emit nothing, so the feeding loop is the same in every state; while tracking, the release clock runs on the two lock flags after every call (see lost_confirm_s). Under SPEC's coupled offset + 500 Hz/s Doppler ramp the pre-despread Costas removes the full carrier dynamics before the code loop, so the recovered constellation lands cleanly on the BPSK real axis.

Parameters:

Name Type Description Default
x NDArray[complex64]

Input cf32 samples.

required
out NDArray[complex64] | None

Output symbols; caller provides max_out capacity.

None

Returns:

Type Description
NDArray[complex64]

Number of symbols written (0 while searching/refining, or while tracking with not yet a full symbol's worth of input).

Examples:

>>> import numpy as np
>>> from doppler.dsss import AsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> sf, chip, sym, spc = 1023, 3.069e6, 2700.0, 2
>>> fs, te, tsym = chip * spc, sf * spc, chip * spc / sym
>>> code = np.asarray(Gold().generate(sf)).astype(np.uint8)
>>> csign = np.where(code & 1, -1.0, 1.0)
>>> rng = np.random.default_rng(21)
>>> n = int(600 * tsym) + 4 * te            # 600 async BPSK symbols
>>> idx = np.arange(n)
>>> data = (rng.integers(0, 2, 604) * 2 - 1).astype(float)
>>> si = np.clip((idx / tsym).astype(int), 0, 603)
>>> t = idx / fs

DSSS chips on a carrier sweeping at 500 Hz/s — the ramp the async receiver has to track:

>>> sig = (data[si] * csign[(idx // spc) % sf]
...        * np.exp(1j * 2 * np.pi * 0.5 * 500.0 * t * t))
>>> cn0 = 20.0 + 10 * np.log10(sym)         # Es/N0 = 20 dB
>>> sigma = np.sqrt(fs / 10 ** (cn0 / 10))
>>> pre = 5 * te                            # noise-only lead-in
>>> noise = (sigma / np.sqrt(2)) * (rng.standard_normal(pre + n)
...          + 1j * rng.standard_normal(pre + n))
>>> x = (np.concatenate([np.zeros(pre), sig]).astype(np.complex64)
...      + noise.astype(np.complex64))
>>> rx = AsyncDsssReceiver(
...     code, chip_rate=chip, symbol_rate=sym, spc=spc,
...     cn0_dbhz=cn0, doppler_uncertainty=500.0)
>>> syms = [rx.steps(x[p:p + te]) for p in range(0, len(x) - te, te)]
>>> syms = np.concatenate([s for s in syms if len(s)])
>>> rx.tracking                  # searched, refined, now tracking
1
>>> len(syms) > 300              # symbols recovered under the ramp
True

Nearly all the energy lands on I, so the BPSK phase is resolved:

>>> bool(np.mean(syms.real**2) > 10 * np.mean(syms.imag**2))
True

steps_max_out

steps_max_out() -> int

Largest number of samples steps() can return in the current state.

Size an out= buffer with this before calling steps(), or use it to allocate one up front. The bound is this object's own: what it depends on is a property of the algorithm, so a header block on steps_max_out() replaces this text.

Returns:

Type Description
int

Upper bound on the output length; the actual call may return fewer.

seed

seed(
    chip_phase: float,
    doppler_hz_est: float,
    cn0_dbhz_est: float,
) -> None

Take a detection from outside and start refining from it: the hit's chip phase (Dll's instantaneous convention, at the next sample fed), coarse Doppler estimate and C/N0 estimate -- exactly what the searching flavor's own hit produces. Accepted while idle (hand-off flavor) or searching; refused on a receiver that already holds a seed (refining, tracking or lost -- reset() releases it) and for a chip_phase outside [0, code_len).

The hand-off of docs/design/async-dsss-receiver.md section 11.1: the three numbers a searcher's hit carries that this receiver uses -- acq_handoff_t's chip_phase, doppler_hz_est and cn0_dbhz_est -- exactly as its own hit would have produced them (the searching flavor's steps() calls this on its own hit). chip_phase is the code's instantaneous phase in chips, Dll's convention, at the FIRST sample of the next steps() call; the Python-side conversion from a lag is doppler.dsss.handoff. The refine chain is rebuilt from the seed and the state becomes refining; the unconsumed tail is the caller's to feed.

Refused (DP_ERR_INVALID, nothing changes) on a receiver that is not waiting for one -- refining, tracking or lost -- because "assigned once" is a property of the object, not of the caller's bookkeeping; reset() releases it. Accepted while idle (hand-off mode) or searching (the searching flavor: an outside hit simply beats its own). Also refused for a chip_phase outside [0, code_len) or a non-finite value.

Parameters:

Name Type Description Default
chip_phase float

Code phase at the next sample, chips, in [0, code_len).

required
doppler_hz_est float

Coarse Doppler estimate, Hz (the refine stage sharpens it).

required
cn0_dbhz_est float

The hit's C/N0 estimate, dB-Hz; reported back by get_cn0_dbhz_est() until tracking refreshes it.

required

Raises:

Type Description
ValueError

If the C call returns a non-zero status. The exception message is seed refused: the receiver already holds an assignment (refining, tracking or lost -- reset() releases it), or chip_phase is outside [0, code_len), with the return code appended (gh-869).

Examples:

>>> import numpy as np
>>> from doppler.dsss import HandoffAsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = HandoffAsyncDsssReceiver(code, chip_rate=3.069e6,
...                               symbol_rate=2700.0, spc=2)
>>> rx.seed(chip_phase=512.25, doppler_hz_est=-1500.0,
...         cn0_dbhz_est=48.0)
>>> (rx.idle, rx.refining, rx.doppler_hz, rx.cn0_dbhz_est)
(0, 1, -1500.0, 48.0)

Already assigned -- refused until reset():

>>> rx.seed(0.0, 0.0, 48.0)
Traceback (most recent call last):
    ...
ValueError: seed refused: ...
>>> rx.reset()

A chip phase must be inside the code, [0, code_len):

>>> rx.seed(1023.0, 0.0, 48.0)
Traceback (most recent call last):
    ...
ValueError: seed refused: ...

status

status() -> ReceiverStatus

One consistent picture of the receiver, by value (design section 11.3): state, where the emitter is now (the whole carrier estimate -- loop 1's plus what loop 2 took up beyond it -- only as good as locked; chip phase, code rate, C/N0), both lock flags with the symbol-lock metric and threshold, both residual carrier errors, and the two clocks in input samples (since the state was entered; both flags down without a break). Read on demand by the holder of a pool -- the one-at-a-time properties are the same fields' other face. No timestamp: the holder owns the sample clock and stamps it.

Cheap and allocation-free: every field is a read of live state. The one-at-a-time getters below report the same fields; this is the face a pool holder uses.

Returns:

Type Description
ReceiverStatus

The record, by value.

Examples:

>>> import numpy as np
>>> from doppler.dsss import HandoffAsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = HandoffAsyncDsssReceiver(code, chip_rate=3.069e6,
...                               symbol_rate=2700.0, spc=2)
>>> st = rx.status()
>>> (st.state, st.doppler_hz, st.code_locked, st.locked)   # idle
(3, 0.0, 0, 0)
>>> rx.seed(chip_phase=100.0, doppler_hz_est=-250.0, cn0_dbhz_est=50.0)
>>> st = rx.status()
>>> (st.state, round(st.doppler_hz, 6), st.cn0_dbhz_est)  # refining
(1, -250.0, 50.0)
>>> _ = rx.steps(np.zeros(2046, np.complex64))
>>> rx.status().state_samples                             # since seed
2046

set_refine_min_blocks

set_refine_min_blocks(n_blocks: int) -> None

Floor the refine's dwell at n_blocks whatever the detection sizing asks (design section 12.16, #1265): CarrierAcquisition's dwell is sized for detection at the derated C/N0 and shortens as the C/N0 rises -- two blocks at 45 dB-Hz with the shipped margin -- while the noise of the estimate it hands the tracking chain does not shorten with it (210 Hz at two blocks against a chain that pulls in from a few hundred). The default of 7 blocks (42 ms) holds it to 77 Hz. Applied to the next refine chain built; 0 removes the floor; clamped by refine_max_n_blocks. Config, not running state.

CarrierAcquisition's dwell is sized for DETECTION at the derated C/N0 (cn0_dbhz - refine_design_margin_db), so it shortens as the C/N0 rises -- two blocks at 45 dB-Hz with the shipped margin -- while the noise of the estimate it hands the tracking chain does not shorten with it: 210 Hz at two blocks against a chain that pulls in from a few hundred, so one hand-over in sixty landed outside and tracked the code with the carrier never locked. Seven blocks (42 ms, the default and section 12.10's floor dwell) hold the estimate to 77 Hz. Applied to the next refine chain built -- a receiver already refining keeps its dwell. Config, not running state: not in the blob. n_blocks of 0 removes the floor; the value is clamped by refine_max_n_blocks where that cap is lower.

Parameters:

Name Type Description Default
n_blocks int

The floor, blocks.

required

Raises:

Type Description
ValueError

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

Examples:

>>> from doppler.dsss import AsyncDsssReceiver
>>> rx = AsyncDsssReceiver(code=[1, 0, 1, 1, 0, 0, 1], chip_rate=1e6,
...                        symbol_rate=1e6 / 28.0, spc=4, cn0_dbhz=60.0)
>>> rx.refine_min_blocks                     # the default floor
7
>>> rx.set_refine_min_blocks(12)
>>> rx.refine_min_blocks
12

configure_lock_raw

configure_lock_raw(
    up_thresh: float,
    down_thresh: float,
    n_looks: int,
    alpha: float,
    n_up: int,
    n_down: int,
) -> None

Re-tune the live-tracking Dll's code-lock detector directly. Only meaningful once tracking has begun; a no-op while searching or refining.

Parameters:

Name Type Description Default
up_thresh float

CFAR-statistic level to declare code lock (hit when the statistic exceeds it).

required
down_thresh float

Level below which a look is a miss; choose <= up_thresh for level hysteresis.

required
n_looks int

Looks per decision — the DLL's non-coherent integration depth feeding one statistic.

required
alpha float

EMA smoothing coefficient on the lock statistic (0..1); smaller is smoother/slower.

required
n_up int

Consecutive hits required to declare lock.

required
n_down int

Consecutive misses required to drop lock.

required

Examples:

>>> import numpy as np
>>> from doppler.dsss import AsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = AsyncDsssReceiver(code, chip_rate=3.069e6, symbol_rate=2700.0,
...                        spc=2, doppler_uncertainty=500.0)
>>> rx.configure_lock_raw(up_thresh=0.4, down_thresh=0.2, n_looks=20,
...                       alpha=0.1, n_up=5, n_down=3)
>>> rx.tracking                       # a no-op until tracking begins
0

configure_chain_raw

configure_chain_raw(
    segments: int, sps: int, n: int
) -> None

Pin the live-tracking despread/resample/demod grid directly, bypassing the create-time segments/sps defaults. Only meaningful once tracking; rebuilds the chain with every replacement allocated first, so a failed pin leaves the receiver on its prior grid.

Parameters:

Name Type Description Default
segments int

Live-tracking Dll segments per code period.

required
sps int

MpskReceiver samples per symbol (the resample target).

required
n int

MpskReceiver's carrier-arm count; must divide sps.

required

Raises:

Type Description
ValueError

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

Examples:

>>> import numpy as np
>>> from doppler.dsss import AsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = AsyncDsssReceiver(code, chip_rate=3.069e6, symbol_rate=2700.0,
...                        spc=2, doppler_uncertainty=500.0)
>>> rx.configure_chain_raw(segments=6, sps=8, n=8)  # re-pin the chain
>>> rx.segments                       # tracking grid updated in place
6

reset

reset() -> None

Return to the searching state: resets the embedded Acquisition and frees every refine-stage/track-stage child (rebuilt from scratch on the next hit).

Examples:

>>> import numpy as np
>>> from doppler.dsss import AsyncDsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = AsyncDsssReceiver(code, chip_rate=3.069e6, symbol_rate=2700.0,
...                        spc=2, doppler_uncertainty=500.0)
>>> rx.reset()                 # abort any lock, hunt from scratch
>>> (rx.tracking, rx.refining, rx.chip_phase)   # all cleared
(0, 0, 0.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 HandoffAsyncDsssReceiver has already been destroyed.

Returns:

Type Description
int

Byte length of one serialized state blob.

get_state

get_state() -> bytes

Serialize this object's mutable state to bytes.

Captures exactly the state that evolves as the object runs, so a blob taken now and restored later resumes from this point. Construction parameters are not included: restore into an object built the same way.

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

Raises RuntimeError if the HandoffAsyncDsssReceiver has already been destroyed.

Returns:

Type Description
bytes

Opaque snapshot, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

Restore mutable state from a get_state() blob.

Overwrites the live state in place; the object keeps the parameters it was constructed with. Length is validated against state_bytes() before the blob is handed to the C core, and the core may reject it as well.

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

Enter a context manager, returning this object.

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

Returns:

Type Description
HandoffAsyncDsssReceiver

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

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.

...

Both flavors report one consistent picture by value through status(). It carries no timestamp: the holder owns the sample clock and stamps it (design §8.1).

ReceiverStatus

Bases: tuple[int, float, float, float, float, int, int, float, float, float, float, int, int]

AsyncDsssReceiver's status record: state (0 searching, 1 refining, 2 tracking, 3 idle, 4 lost), the live estimates, both lock flags, and the two clocks in input samples.

Attributes:

Name Type Description
state int

The receiver's ASYNC_DSSS_RX_* state; -1 for a slot that does not exist.

doppler_hz float

Signed coarse Doppler, folded, Hz.

chip_phase float

Chips, Dll's own instantaneous-phase convention (the mirror image of acq_result_t::code_phase's correlation-lag convention -- see acq_build_handoff()'s doc comment).

code_rate float

chips advanced per nominal chip (~1.0).

cn0_dbhz_est float

C/N0 lower bound from the hit, dB-Hz.

code_locked int

Presence flag.

locked int

Health flag (symbol lock).

lock_metric float

mean of |Re P|/|P| over the burst (~1 locked, ~2/pi with no carrier).

lock_threshold float

locked latches above this.

car_last_error float

Pre-despread Costas residual, rad.

mpsk_last_error float

Post-despread carrier residual, rad.

state_samples int

Samples since the receiver's state was entered.

both_down_samples int

The release clock, samples.

state property

state: int

The receiver's ASYNC_DSSS_RX_* state; -1 for a slot that does not exist.

doppler_hz property

doppler_hz: float

Signed coarse Doppler, folded, Hz.

chip_phase property

chip_phase: float

Chips, Dll's own instantaneous-phase convention (the mirror image of acq_result_t::code_phase's correlation-lag convention -- see acq_build_handoff()'s doc comment).

code_rate property

code_rate: float

chips advanced per nominal chip (~1.0).

cn0_dbhz_est property

cn0_dbhz_est: float

C/N0 lower bound from the hit, dB-Hz.

code_locked property

code_locked: int

Presence flag.

locked property

locked: int

Health flag (symbol lock).

lock_metric property

lock_metric: float

mean of |Re P|/|P| over the burst (~1 locked, ~2/pi with no carrier).

lock_threshold property

lock_threshold: float

locked latches above this.

car_last_error property

car_last_error: float

Pre-despread Costas residual, rad.

mpsk_last_error property

mpsk_last_error: float

Post-despread carrier residual, rad.

state_samples property

state_samples: int

Samples since the receiver's state was entered.

both_down_samples property

both_down_samples: int

The release clock, samples.

AsyncDsssPool — one object holds the population

The holder of the continuous multi-emitter design (§8.2): one searcher (Acquisition in continuous mode with the block coherence of §2.3 and a peak list), n_slots HandoffAsyncDsssReceivers created idle, the assigned table, and the run's EventLog attached through set_event_log(). One push() per block feeds the searcher, drops every peak inside one exclusion zone of a live row as that emitter's own, seeds each survivor into a free slot or counts it dropped, feeds every receiver across the threads the pool is given, and releases every receiver that reports lost or has held its slot past max_emitter_on_time_secs. Every transition — seeded, tracking, degrade, lost, released, dropped — is an event at the sample it happened. Per slot and by index: status(slot) by value and symbols(slot), the last push's symbols. Nothing about the waveform or the population is baked in; every number is a constructor parameter whose default is the operating point of §6.1.

AsyncDsssPool

Create a async_dsss_pool instance.

Parameters:

Name Type Description Default
code NDArray[uint8]

Spreading code, one 0/1 chip per element.

required
chip_rate float

Chip rate, Hz (default: 1000000.0).

1000000.0
symbol_rate float

Data-symbol rate, Hz (default: 1000.0).

1000.0
spc int

Samples per chip (default: 2).

2
m int

PSK order of the receivers (default: 2).

2
cn0_dbhz float

Design C/N0 for the searcher's sizing and the receivers' (default: 55.0).

55.0
pfa float

False-alarm target, the searcher's and the refine's (default: 1e-3).

1e-3
pd float

Detection-probability target (default: 0.9).

0.9
doppler_uncertainty float

The searcher's one-sided span, Hz (default: 100.0).

100.0
code_only_epochs int

Whole code-only epochs the waveform's window holds at any chip phase -- the block depth of section 2.3; 1 = no window (default: 1).

1
doppler_rate float

Doppler rate the depth is bounded against, Hz/s; 0 leaves the window as the only bound (default: 0.0).

0.0
max_peaks int

The searcher's list capacity per dwell (default: 16).

16
n_slots int

Receivers held (default: 12).

12
threads int

Threads the receivers and the searcher's fan run across; <= 0 picks the online core count, 1 is serial (default: 1).

1
carrier_freq_hz float

RF carrier the Doppler is physically coupled to, Hz, told to the searcher and every receiver; 0.0 = uncoupled (default: 0.0).

0.0
lost_confirm_s float

The release rule's interval, seconds (section 10) (default: 2.0).

2.0
max_emitter_on_time_secs float

Maximum on-air time of one emitter, seconds: a slot held longer is released (reason on_time); 0 = never (default: 900.0, ASYNC_DSSS_POOL_MAX_EMITTER_ON_ TIME_SECS).

900.0
segments int

The receivers' live Dll segments (default: 4).

4
sps int

The receivers' samples per symbol (default: 8).

8
differential int

The receivers' differential demap (default: 0).

0
refine_max_error_db float

As async_dsss_receiver_create() (default: 0.5).

0.5
refine_samples_per_symbol int

As async_dsss_receiver_create() (default: 4).

4
refine_design_margin_db float

As async_dsss_receiver_create() (default: 14.0).

14.0
refine_n_fft int

As async_dsss_receiver_create() (default: 64).

64
refine_zero_pad int

As async_dsss_receiver_create() (default: 8).

8
refine_sequential bool

As async_dsss_receiver_create() (default: false).

False
refine_max_n_blocks int

As async_dsss_receiver_create() (default: 100000).

100000

Examples:

>>> import numpy as np
>>> from doppler.dsss import AsyncDsssPool
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> pool = AsyncDsssPool(code, chip_rate=5e6, symbol_rate=2700.0,
...                      spc=2, cn0_dbhz=45.0, doppler_uncertainty=5e3,
...                      n_slots=4, lost_confirm_s=0.5)
>>> (pool.n_slots, pool.n_assigned, pool.coherent_bins)
(4, 0, 1)
>>> round(pool.doppler_res_hz)          # one Doppler row of the searcher
4888

refine_min_blocks property

refine_min_blocks: int

The receivers' floor on the refine dwell, blocks (default 7); set with set_refine_min_blocks().

n_slots property

n_slots: int

Receivers the pool holds; it never exceeds this.

n_assigned property

n_assigned: int

Slots assigned right now.

dropped property

dropped: int

Detections dropped for want of a free slot since create or reset.

events property

events: int

Transitions since create or reset, logged or not.

samples_consumed property

samples_consumed: int

Input samples pushed since create or reset -- the stream position every event is stamped at.

doppler_res_hz property

doppler_res_hz: float

The searcher's Doppler row, Hz -- the resolution a seed's Doppler is reported at.

coherent_bins property

coherent_bins: int

The searcher's block-coherent depth D, from code_only_epochs and doppler_rate (section 2.3).

reset

reset() -> None

Release every slot and start over: the searcher reset, every receiver back to idle, the table cleared, the counters zeroed.

The attached log stays attached and nothing is logged -- a reset is the holder's decision, not an emitter's transition.

Examples:

>>> import numpy as np
>>> from doppler.dsss import AsyncDsssPool
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> pool = AsyncDsssPool(code, chip_rate=5e6, symbol_rate=2700.0,
...                      spc=2, cn0_dbhz=45.0, n_slots=2)
>>> _ = pool.push(np.zeros(2046, np.complex64))
>>> pool.samples_consumed
2046
>>> pool.reset()
>>> (pool.samples_consumed, pool.n_assigned, pool.events)
(0, 0, 0)

push

push(x: NDArray[complex64]) -> int

One block of raw cf32 samples through the population (design section 8.2), in order: the searcher; the table refreshed from every live receiver's status(); every peak within one chip of a live row's code phase, at any Doppler, dropped as that emitter's own (the zone is the code axis alone: a tracked emitter's data blocks put smeared copies of it at its own phase rows away, section 12.14); each survivor seeded into a free slot, or counted dropped when there is none; every receiver fed (an idle or lost one consumes and discards, so the feed has no per-state branch), across the threads the pool was given; then every receiver that reports lost, or has held its slot past max_emitter_on_time_secs, released -- the row cleared, the receiver reset to idle. Every transition -- seeded, tracking, degrade, lost, released, dropped -- goes to the attached event log at the sample it happened. Accepts any block size; a searcher dwell is decided when its samples arrive. Returns the receivers assigned after this push.

In order: the searcher; the table refreshed; every peak within one chip of a live row's code phase, at any Doppler, dropped as that emitter's own; each survivor seeded into a free slot or counted dropped; every receiver fed, across the pool's threads; every receiver that reports lost, or has held its slot past the maximum on-air time, released. Every transition goes to the attached log at the sample it happened. Accepts any block size: a hit decided inside the block is referred to the block's start before it seeds (the receiver is fed the whole block), on the dilated clock when the carrier is known.

Parameters:

Name Type Description Default
x NDArray[complex64]

Input samples.

required

Returns:

Type Description
int

Receivers assigned after this push.

Examples:

>>> import numpy as np
>>> from doppler.dsss import AsyncDsssPool
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> pool = AsyncDsssPool(code, chip_rate=5e6, symbol_rate=2700.0,
...                      spc=2, cn0_dbhz=45.0, n_slots=2)
>>> int(pool.push(np.zeros(4 * 2046, np.complex64)))  # silence: no one
0
>>> pool.samples_consumed                  # the stream position
8184

status

status(slot: int) -> PoolSlot

One slot's picture, by value: whether it is assigned, the seed it was assigned from (sample, chip phase, Doppler, C/N0 -- the searcher's hand-off record, verbatim), the receiver's own status record (state, the live Doppler, chip phase, code rate and C/N0, both lock flags, the symbol-lock metric, the two clocks), and the samples since the assignment. Raises ValueError for a slot outside [0, n_slots).

Allocation-free: the row plus the receiver's own status record. A slot outside [0, n_slots) returns a zero record with state -1.

Parameters:

Name Type Description Default
slot int

The slot.

required

Returns:

Type Description
PoolSlot

The record.

Examples:

>>> import numpy as np
>>> from doppler.dsss import AsyncDsssPool
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> pool = AsyncDsssPool(code, chip_rate=5e6, symbol_rate=2700.0,
...                      spc=2, cn0_dbhz=45.0, n_slots=2)
>>> r = pool.status(1)
>>> (r.slot, r.assigned, r.state)         # idle: 3, nothing assigned
(1, 0, 3)
>>> pool.status(2).state                  # no such slot
-1

symbols

symbols(
    slot: int, out: NDArray[complex64] | None = None
) -> NDArray[np.complex64]

The symbols slot slot's receiver decided on the last push(), borrowed from the pool's own buffer (sized once at create by the receiver's steps_max_out()): empty while the slot is idle, refining or lost. Valid until the next push(), reset() or set_state(). Raises ValueError for a slot outside [0, n_slots).

Copied from the pool's own buffer, which the next push() overwrites. Empty while the slot is idle, refining or lost, and for a slot outside [0, n_slots).

Parameters:

Name Type Description Default
slot int

The slot.

required
out NDArray[complex64] | None

Caller buffer.

None

Returns:

Type Description
NDArray[complex64]

Symbols written.

Examples:

>>> import numpy as np
>>> from doppler.dsss import AsyncDsssPool
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> pool = AsyncDsssPool(code, chip_rate=5e6, symbol_rate=2700.0,
...                      spc=2, cn0_dbhz=45.0, n_slots=2)
>>> _ = pool.push(np.zeros(2046, np.complex64))
>>> pool.symbols(0).shape                 # idle: nothing decided
(0,)

symbols_max_out

symbols_max_out() -> int

The per-slot symbol capacity symbols() can return -- grown with the largest block pushed so far (0 before the first push).

Returns:

Type Description
int

Output.

set_event_log

set_event_log(log: object | None) -> None

Attach the run's event log (design section 8.1): from now on every transition -- seeded, tracking, degrade, lost, released, dropped -- is appended at the sample it happened, with the slot, the receiver's state, the Doppler, the chip phase and the C/N0 staged as doppler: fields beside the label (core:label). The pool is the one component that stamps; the log is borrowed, never owned. None detaches.

Borrowed, never owned: the holder opens, finalizes and closes it. From now on every transition is appended at the sample it happened, with slot, state, doppler_hz, chip_phase and cn0_dbhz staged as doppler:<name> fields beside the label (core:label) and, on released, reason (lost or on_time). A log that has already failed keeps failing (its error is sticky); the pool counts the transition either way.

Parameters:

Name Type Description Default
log object | None

The log, or NULL.

required

Raises:

Type Description
ValueError

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

Examples:

>>> import os, tempfile
>>> import numpy as np
>>> from doppler.dsss import AsyncDsssPool
>>> from doppler.telemetry import EventLog
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> pool = AsyncDsssPool(code, chip_rate=5e6, symbol_rate=2700.0,
...                      spc=2, cn0_dbhz=45.0, n_slots=2)
>>> log = EventLog(os.path.join(tempfile.mkdtemp(), "run.events"))
>>> pool.set_event_log(log)               # attached: transitions go here
>>> _ = pool.push(np.zeros(2046, np.complex64))
>>> pool.set_event_log(None)              # detached
>>> log.close()

set_refine_min_blocks

set_refine_min_blocks(n_blocks: int) -> None

Floor every receiver's refine dwell at n_blocks (AsyncDsssReceiver.set_refine_min_blocks(); design section 12.16,

1265): forwarded to all n_slots receivers, each applying it to the

next refine chain it builds. The receivers' default is 7 blocks; 0 removes the floor. Config, not running state.

Forwarded to all n_slots receivers; each applies it to the next refine chain it builds, so a slot already refining keeps its dwell. The receivers' default is 7 blocks. Config, not running state.

Parameters:

Name Type Description Default
n_blocks int

The floor, blocks; 0 removes it.

required

Raises:

Type Description
ValueError

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

Examples:

>>> import numpy as np
>>> from doppler.dsss import AsyncDsssPool
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> pool = AsyncDsssPool(code, chip_rate=5e6, symbol_rate=2700.0,
...                      spc=2, cn0_dbhz=45.0, n_slots=2)
>>> pool.refine_min_blocks
7
>>> pool.set_refine_min_blocks(12)
>>> pool.refine_min_blocks
12

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 AsyncDsssPool has already been destroyed.

Returns:

Type Description
int

Byte length of one serialized state blob.

get_state

get_state() -> bytes

Serialize this object's mutable state to bytes.

Captures exactly the state that evolves as the object runs, so a blob taken now and restored later resumes from this point. Construction parameters are not included: restore into an object built the same way.

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

Raises RuntimeError if the AsyncDsssPool has already been destroyed.

Returns:

Type Description
bytes

Opaque snapshot, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

Restore mutable state from a get_state() blob.

Overwrites the live state in place; the object keeps the parameters it was constructed with. Length is validated against state_bytes() before the blob is handed to the C core, and the core may reject it as well.

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

Enter a context manager, returning this object.

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

Returns:

Type Description
AsyncDsssPool

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

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.

...

PoolSlot

Bases: tuple[int, int, int, int, float, float, float, float, float, float, float, int, int, float, int, int, int]

AsyncDsssPool's slot record: whether the slot is assigned, the seed it was assigned from, the receiver's live status (state 1 refining, 2 tracking, 3 idle, 4 lost), and the samples since the assignment.

Attributes:

Name Type Description
slot int

The slot asked for.

assigned int

1 while a receiver holds an emitter.

state int

The receiver's ASYNC_DSSS_RX_* state; -1 for a slot that does not exist.

seed_sample int

Stream position the row was assigned at.

seed_chip_phase float

The seed's code phase, chips.

seed_doppler_hz float

The seed's Doppler, Hz.

seed_cn0_dbhz float

The seed's C/N0 estimate, dB-Hz.

doppler_hz float

Signed coarse Doppler, folded, Hz.

chip_phase float

Chips, Dll's own instantaneous-phase convention (the mirror image of acq_result_t::code_phase's correlation-lag convention -- see acq_build_handoff()'s doc comment).

code_rate float

chips advanced per nominal chip (~1.0).

cn0_dbhz_est float

C/N0 lower bound from the hit, dB-Hz.

code_locked int

Presence flag.

locked int

Health flag (symbol lock).

lock_metric float

mean of |Re P|/|P| over the burst (~1 locked, ~2/pi with no carrier).

state_samples int

Samples since the receiver's state was entered.

both_down_samples int

The release clock, samples.

assigned_samples int

Samples since the row was assigned.

slot property

slot: int

The slot asked for.

assigned property

assigned: int

1 while a receiver holds an emitter.

state property

state: int

The receiver's ASYNC_DSSS_RX_* state; -1 for a slot that does not exist.

seed_sample property

seed_sample: int

Stream position the row was assigned at.

seed_chip_phase property

seed_chip_phase: float

The seed's code phase, chips.

seed_doppler_hz property

seed_doppler_hz: float

The seed's Doppler, Hz.

seed_cn0_dbhz property

seed_cn0_dbhz: float

The seed's C/N0 estimate, dB-Hz.

doppler_hz property

doppler_hz: float

Signed coarse Doppler, folded, Hz.

chip_phase property

chip_phase: float

Chips, Dll's own instantaneous-phase convention (the mirror image of acq_result_t::code_phase's correlation-lag convention -- see acq_build_handoff()'s doc comment).

code_rate property

code_rate: float

chips advanced per nominal chip (~1.0).

cn0_dbhz_est property

cn0_dbhz_est: float

C/N0 lower bound from the hit, dB-Hz.

code_locked property

code_locked: int

Presence flag.

locked property

locked: int

Health flag (symbol lock).

lock_metric property

lock_metric: float

mean of |Re P|/|P| over the burst (~1 locked, ~2/pi with no carrier).

state_samples property

state_samples: int

Samples since the receiver's state was entered.

both_down_samples property

both_down_samples: int

The release clock, samples.

assigned_samples property

assigned_samples: int

Samples since the row was assigned.

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.dsss 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)
acq = BurstAcquisition(code, reps=4, spc=4, chip_rate=1e6, cn0_dbhz=55.0)

# One repeated-code burst, so push() reports a hit to read the bin off.
chips = np.where(code & 1, -1.0, 1.0)
burst = np.tile(np.repeat(chips, 4), 8).astype(np.complex64)
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.dsss 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

GalleryStreaming Async Despreader, Async DSSS Receiver: the SPEC waveform through coupled Doppler, CarrierAcquisition: RRC Pulse Shaping, Correlation and Detection, DSSS Acquisition — Pd / Pfa vs Es/N0, A 5-Burst DSSS Link — wfmgen's Three Faces, the Full Receiver Chain, DsssBurstReceiver — the Composed Burst Chain, DsssReceiver — the Composed Continuous DSSS Receiver, Gallery, Full-Chain Lock-Up GuidesDSSS Burst Acquisition, Guides, Lock Detection Across doppler.track, Checkpoint & Resume, Waveforms — what you can generate DesignDesign — pure-functional acquisition kernel (elastic fleet), API taxonomy: the DSP building-block hierarchy and its naming axis, AsyncDsssReceiver — the continuous DSSS receiver, 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, Detection Sizing — the four laws behind one prefix, DSSS acquisition: stateless, parallel, dynamics-capable, DsssBurstReceiver: the burst chain, composed in C, Design, MPSK Receiver, The Polynomial-Phase Estimator — the reasoning, State Serialization — the standard bytes interface ContributingDSSS Primary Use Cases for Code Acquisition Design, Validation log, Contributing