Skip to content

Python DSSS API

The doppler.dsss module provides the two halves of a DSSS receiver: Acquisition — the streaming burst-acquisition engine that finds an unknown code phase and Doppler — and Despreader — the tracking receiver that locks and despreads the payload once acquired.

Source: src/doppler/dsss/__init__.py

See the DSSS acquisition & despreading gallery page for the full acquire → track → despread chain with plots.


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 Despreader. See the DSSS Burst Acquisition guide for the search-space sizing and a worked example.

Acquisition

Create a streaming DSSS acquisition engine.

Parameters:

Name Type Description Default
code NDArray[uint8]

PN chips (0/1), length code_len.

...
reps int

Max coherent code repetitions, the coherence ceiling (>=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).

50.0
doppler_uncertainty float

One-sided Doppler search half-range in Hz; 0 uses the full native span +/- chip_rate/(2*sf). Must be <= span.

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"
max_noncoh int

Cap on the auto-split non-coherent look count (>= 1; default 1 keeps the engine purely coherent).

1

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

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.

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

reset

reset() -> None

Drain the input ring and reset the coherent accumulator.

push

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

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

Buffers in, 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.

Parameters:

Name Type Description Default
x complex

Input.

required

Returns:

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

Number of events written (0 … max_results).

state_bytes

state_bytes() -> int

Serialized state size in bytes.

get_state

get_state() -> bytes

Serialize the engine's mutable state to bytes.

set_state

set_state(blob: bytes) -> None

Restore mutable state from a get_state() blob.

destroy

destroy() -> None

Release C resources immediately.


PolyPhaseEstimator — feedforward frequency + chirp-rate estimator

PolyPhaseEstimator 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 PolyPhaseEstimate(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.

PolyPhaseEstimator

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 PolyPhaseEstimator
>>> obj = PolyPhaseEstimator(max_len=4096, max_rate=0.0)

max_len property

max_len: int

Max len.

nfft property

nfft: int

Nfft.

max_rate property

max_rate: float

Max rate.

n_rate property

n_rate: int

N rate.

reset

reset() -> None

No-op (the estimator carries no running state).

estimate

estimate(x: complex) -> tuple[float, float, float]

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

Parameters:

Name Type Description Default
x complex

Input.

required

Returns:

Type Description
tuple[float, float, float]

The estimate; zeroed if n_in is out of range.

destroy

destroy() -> None

Release C resources immediately.


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 PolyPhaseEstimator 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, payload_len=64, 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)
bits = d.demod(rx)
assert d.frame_valid             # CRC passed

BurstDemod

Create a burst demodulator.

Parameters:

Name Type Description Default
data_code NDArray[uint8]

Data spreading code (0/1); copied.

...
spc int

Samples per chip.

4
chip_rate float

Chip rate (Hz).

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

Number of payload data symbols (bits) in a frame.

0
est_segments int

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

10

frame_valid property

frame_valid: int

Frame valid.

frame_offset property

frame_offset: int

Frame offset.

n_symbols property

n_symbols: int

N symbols.

est_freq_hz property

est_freq_hz: float

Est freq hz.

est_rate_hz property

est_rate_hz: float

Est rate hz.

est_snr_db property

est_snr_db: float

Est snr db.

payload_len property

payload_len: int

Payload len.

reset

reset() -> None

Clear the read-backs (config is preserved).

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.

Parameters:

Name Type Description Default
acq_code NDArray[uint8]

Input.

required
reps int

Input.

required

set_sync

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

Set the known frame-sync word (0/1 BPSK symbols) used for frame alignment + phase/sign resolution.

Parameters:

Name Type Description Default
sync NDArray[uint8]

Input.

required

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.

Parameters:

Name Type Description Default
f0_coarse float

Input.

required
start int

Input.

required

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 C resources immediately.


Despreader — tracking receiver

Seeded with a coarse frequency and code-phase estimate (from the Corr2D/Detector2D acquisition engine or Acquisition), the Despreader 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 Despreader

# 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 = Despreader(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 = Despreader(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

Despreader

Despreader component.

Parameters:

Name Type Description Default
code NDArray[uint8]

code constructor parameter.

...
sf int

sf constructor parameter.

1
sps int

sps constructor parameter.

2
init_norm_freq float

init_norm_freq constructor parameter.

0.0
init_chip_phase float

init_chip_phase constructor parameter.

0.0
bn_carrier float

bn_carrier constructor parameter.

0.05
bn_code float

bn_code constructor parameter.

0.01

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] (EMA of |Re prompt|/|prompt|; ~1 = locked).

snr_est property

snr_est: float

Post-despread SNR estimate (EMA of (Re prompt)^2 / (Im prompt)^2).

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

// seed from acquisition (norm_freq cyc/sample, chip phase in chips): despreader_state_t *d = despreader_create(code, n, 32, 2, f0, chip, .05, .01); float complex sym[256]; size_t k = despreader_steps(d, rx, rx_len, sym, 256); // hard bit of sym[i] = crealf(sym[i]) >= 0 despreader_destroy(d);

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

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

reset

reset() -> None

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

state_bytes

state_bytes() -> int

Serialized state size in bytes.

get_state

get_state() -> bytes

Serialize the engine's mutable state to bytes.

set_state

set_state(blob: bytes) -> None

Restore mutable state from a get_state() blob.

destroy

destroy() -> None

Release C resources immediately.