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
|
doppler_bins
property
¶
Coherent depth chosen: the slow-time FFT length in code reps (<= reps).
pfa_cell
property
¶
Bonferroni per-cell false-alarm probability over the searched cells.
doppler_span_hz
property
¶
Native unambiguous Doppler half-range = +/- chip_rate/(2*sf) Hz.
underpowered
property
¶
True when pd_predicted < pd (the search cannot meet the target).
push
¶
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). |
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)
estimate
¶
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. |
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
|
set_preamble
¶
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 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
¶
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
¶
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
¶
Max output length demod() can produce for the current state. Use to size the out= buffer.
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
¶
Carrier (Costas) loop noise bandwidth, normalized to the symbol rate.
bn_code
property
writable
¶
Code (DLL) loop noise bandwidth, normalized to the symbol rate.
lock_metric
property
¶
Lock indicator in [0,1] (EMA of |Re prompt|/|prompt|; ~1 = locked).
snr_est
property
¶
Post-despread SNR estimate (EMA of (Re prompt)^2 / (Im prompt)^2).
steps
¶
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
¶
Max output length steps() can produce for the current state. Use to size the out= buffer.
bits
¶
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
¶
Max output length bits() can produce for the current state. Use to size the out= buffer.
set_acq
¶
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 |