Python DSSS API¶
The doppler.dsss module provides a full DSSS receiver: two despreaders
that track and despread once a signal is acquired — Despreader, the
continuous DLL+Costas receiver (GPS-like, always tracking), and
BurstDespreader, the preamble-aware payload tracker for latency-bound
bursts — and the receivers that compose them. The searches that find the
code phase and Doppler in the first place (Acquisition,
BurstAcquisition, BurstCapture) live in the
acquire API.
Source:
src/doppler/dsss/__init__.py
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)
nfft
property
¶
zero-padded transform length: 4 * next_pow_two (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.
reset
¶
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:
estimate
¶
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
¶
Release the underlying C resources immediately.
Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this to release them at a definite point instead, or use the object as a context manager, which calls it on exit.
Idempotent: calling it again on an already-released object does
nothing. Every other method raises RuntimeError once it has run.
__enter__
¶
Enter a context manager, returning this object.
Lets a 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 whole frame's bits, sync header first, then payload, then
the CRC trailer (the example below slices all three); 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 `BurstAcquisition`/`BurstCapture`; 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)
# ONE description of the frame: the transmitter spreads it, the receiver is
# told it (its sync word and its length), and nobody sums field lengths.
from doppler.wfm import Frame
desc = Frame(sync=sync_word, payload=payload, crc="crc16")
frame = desc.bits() # sync | payload | CRC-16
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, desc, spc=4, chip_rate=1e6, carrier_hz=0.0,
max_rate=0.0, est_segments=10)
d.set_preamble(acq_code, reps=5)
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
¶
The Python binding's constructor: dp_burst_demod_create_desc without the
why out-parameter.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data_code
|
NDArray[uint8]
|
the data spreading code, 0/1 chips. |
required |
frame
|
Any
|
The frame description the transmitter spread (a Frame or a FrameDesc, built or not): its first field is the sync word the receiver correlates for, and its layout is the frame's length. Copied at construction, so it need not outlive the receiver. |
required |
spc
|
int
|
samples per chip. |
4
|
chip_rate
|
float
|
chips per second. |
1.0e6
|
carrier_hz
|
float
|
the carrier the baseband is offset by, Hz. |
0.0
|
max_rate
|
float
|
the Doppler rate searched, cycles/sample^2. |
0.0
|
est_segments
|
int
|
partials per acquisition period for the estimate. |
10
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If construction fails. The exception message is |
Examples:
>>> import numpy as np
>>> from doppler.dsss import BurstDemod
>>> from doppler.wfm import Frame
>>> 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)
>>> desc = Frame(sync=sync, payload=payload, crc="crc16")
>>> frame = desc.bits() # sync | payload | CRC-16: ONE description
>>> 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, desc, spc=spc, chip_rate=1e6)
>>> d.set_preamble(acode, reps) # unmodulated (f0, rate) preamble
>>> 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
est_cn0_dbhz
property
¶
Carrier-to-noise DENSITY of the last demod(), dB-Hz, referred to the
CHANNEL. The realized symbol estimate 1/est_n0 lifted to a density by
the symbol rate (chip_rate/data_sf, the identity C/N0 = Es/N0 * Rs)
and corrected for the despreading loss that est_timing_chips
measures. Sample-rate invariant, so a front-end rate change does not
move it, and it does not move when the receiver's own start is off by a
fraction of a chip -- which a realized SNR does, by 4.8 dB at half a
chip. Zero until a demod() produces a frame. Replaces est_snr_db,
which was never an SNR: it published the preamble estimator's spectral
prominence, carrying the full coherent processing gain and swinging 33
dB with alignment at a fixed input (doppler#1304).
est_timing_chips
property
¶
Burst-start error the last demod() MEASURED, in chips, signed,
relative to the start given to set_prior(). The whole-sample part was
removed before despreading; only the sub-sample fraction remains a
loss, and only that fraction is taken out of est_cn0_dbhz. The
symbols keep the loss either way. Acquisition resolves a start to one
SAMPLE, so a residual of a fraction of a chip is structural rather than
a caller's error -- which is why the number is published instead of
assumed to be zero.
frame_syms
property
¶
symbols the frame occupies, sync word included — the description's layout length, read at create. What they MEAN is the frame description's business, one layer up.
est_n0
property
¶
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
¶
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)
>>> from doppler.wfm import Frame
>>> desc = Frame(sync=np.zeros(13, np.uint8), payload=np.zeros(64, np.uint8),
... crc="crc16") # 13 + 64 + 16 = 93 symbols
>>> d = BurstDemod(dcode, desc, spc=4, chip_rate=1e6)
>>> d.reset() # clears the estimates, keeps the config
>>> d.frame_offset
0
set_preamble
¶
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] | bytes | bytearray | memoryview
|
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)
>>> from doppler.wfm import Frame
>>> desc = Frame(sync=np.zeros(13, np.uint8), payload=np.zeros(64, np.uint8),
... crc="crc16") # 13 + 64 + 16 = 93 symbols
>>> d = BurstDemod(dcode, desc, spc=4, chip_rate=1e6)
>>> acode = (np.arange(500) & 1).astype(np.uint8) # unmodulated
>>> d.set_preamble(acode, reps=5) # 5 reps drive the (f0, rate) fit
llrs
¶
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 |
1
|
out
|
NDArray[float32] | None
|
Receives the LLRs, one per frame bit. |
None
|
Returns:
| Type | Description |
|---|---|
NDArray[float32]
|
LLRs written — |
Examples:
>>> import numpy as np
>>> from doppler.dsss import BurstDemod
>>> dcode = (np.arange(50) & 1).astype(np.uint8)
>>> from doppler.wfm import Frame
>>> desc = Frame(sync=np.zeros(13, np.uint8), payload=np.zeros(64, np.uint8),
... crc="crc16") # 13 + 64 + 16 = 93 symbols
>>> d = BurstDemod(dcode, desc, spc=4, chip_rate=1e6)
>>> d.llrs_max_out(1) # one per frame symbol
93
llrs_max_out
¶
Max LLRs dp_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
¶
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 dp_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 |
1
|
out
|
NDArray[complex64] | None
|
Receives the symbols, one per frame bit. |
None
|
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
Symbols written — |
Examples:
>>> import numpy as np
>>> from doppler.dsss import BurstDemod
>>> dcode = (np.arange(50) & 1).astype(np.uint8)
>>> from doppler.wfm import Frame
>>> desc = Frame(sync=np.zeros(13, np.uint8), payload=np.zeros(64, np.uint8),
... crc="crc16") # 13 + 64 + 16 = 93 symbols
>>> d = BurstDemod(dcode, desc, spc=4, chip_rate=1e6)
>>> d.symbols_max_out(1) # one per frame symbol, as llrs()
93
symbols_max_out
¶
Max symbols dp_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
¶
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)
>>> from doppler.wfm import Frame
>>> desc = Frame(sync=np.zeros(13, np.uint8), payload=np.zeros(64, np.uint8),
... crc="crc16") # 13 + 64 + 16 = 93 symbols
>>> d = BurstDemod(dcode, desc, spc=4, chip_rate=1e6)
>>> d.set_prior(0.012, start=0) # coarse Doppler + start, from acq
demod
¶
Demodulate a burst (preamble + frame); return the frame's bits: sync header, payload, then CRC-16 trailer. 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.
destroy
¶
Release the underlying C resources immediately.
Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this to release them at a definite point instead, or use the object as a context manager, which calls it on exit.
Idempotent: calling it again on an already-released object does
nothing. Every other method raises RuntimeError once it has run.
__enter__
¶
Enter a context manager, returning this object.
Lets a 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
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If construction fails. The exception message is ``Despreader: invalid parameter (need a non-empty code; for the code loop a finite init_chip, bn_code >= 0 and zeta > 0 both finite, whose loop gains come out finite, and 0 < spacing < len(code)/2; for the carrier loop bn_carrier
|
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
code_rate
property
¶
Chips the embedded DLL advances per nominal chip: ~1.0, and its departure from 1 is the code-rate (clock) offset being tracked.
lock_metric
property
¶
The embedded Costas loop's lock statistic: the EMA of |Re P|/|P|
over the prompt correlations, 1 when locked. carrier_locked is the
de-chattered decision made on it.
carrier_locked
property
¶
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 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.
bn_carrier
property
writable
¶
Bn carrier.
Raises:
| Type | Description |
|---|---|
ValueError
|
On assignment, if |
bn_code
property
writable
¶
Bn code.
Raises:
| Type | Description |
|---|---|
ValueError
|
On assignment, if |
steps
¶
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
¶
Max output length steps() can produce for the current state. Use to size the out= buffer.
bits
¶
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
¶
Max output length bits() can produce for the current state. Use to size the out= buffer.
set_telemetry
¶
Attach (or detach) a telemetry context across the despreader. Pure
forwarder — the despreader registers no probes of its own: the carrier
loop registers "
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
|
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[:5]
['ch0.car.e', 'ch0.car.freq', 'ch0.car.lock', 'ch0.car.locked', 'ch0.car.nco']
>>> names[5:]
['ch0.code.e', 'ch0.code.lock', 'ch0.code.locked', 'ch0.code.nco', '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() # ten records per code period
>>> len(recs) > 0 and len(recs) % 10 == 0
True
configure_carrier_lock
¶
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 dp_costas_configure_lock() on the embedded Costas loop — symmetric with dp_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 dp_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:
configure_code_lock
¶
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 dp_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, dp_dll_configure_lock_raw(), stays a Dll-only control for a caller that composes Dll+Costas directly). See dp_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 dp_dll_configure_lock()). |
0.0
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the C call returns a non-zero status. The exception message is
|
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
¶
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
¶
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
¶
Serialize this object's mutable state to bytes.
Captures the state that evolves as the object runs, including any value a setter changed after construction, so a blob taken now and restored later resumes from this point. Construction parameters are not restored: restore into an object built the same way. A core may check some of them and refuse a blob built differently.
The blob is opaque and always state_bytes() long. Its layout is an
implementation detail of the C core and is not a stable format across
builds.
Raises RuntimeError if the Despreader has already been destroyed.
Returns:
| Type | Description |
|---|---|
bytes
|
Opaque snapshot, |
set_state
¶
Restore mutable state from a get_state() blob.
Overwrites the live state in place, including any value a setter
changed after construction. Construction parameters are not restored:
restore into an object built the same way. Length is validated against
state_bytes() before the blob is handed to the C core, which may
reject it too: a core may check some construction parameters and refuse
a blob built differently.
Raises TypeError if blob is not bytes, ValueError if its
length differs from state_bytes() or the core rejects it, and
RuntimeError if the Despreader has already been destroyed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
blob
|
bytes
|
A |
required |
destroy
¶
Release the underlying C resources immediately.
Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this to release them at a definite point instead, or use the object as a context manager, which calls it on exit.
Idempotent: calling it again on an already-released object does
nothing. Every other method raises RuntimeError once it has run.
__enter__
¶
Enter a context manager, returning this object.
Lets a 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 BurstAcquisition), 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; finite (default: 0.0). |
0.0
|
init_chip_phase
|
float
|
Seed code phase, chips; finite (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
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If construction fails. The exception message is |
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
¶
Carrier (Costas) loop noise bandwidth, normalized to the symbol rate.
Raises:
| Type | Description |
|---|---|
ValueError
|
On assignment, if |
bn_code
property
writable
¶
Code (DLL) loop noise bandwidth, normalized to the symbol rate.
Raises:
| Type | Description |
|---|---|
ValueError
|
On assignment, if |
lock_metric
property
¶
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
¶
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
¶
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.
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 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
¶
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 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
¶
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 dp_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 dp_burst_despreader_reset(); call it between bursts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
acq_code
|
NDArray[uint8] | bytes | bytearray | memoryview
|
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
¶
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 dp_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
¶
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
¶
Serialize this object's mutable state to bytes.
Captures the state that evolves as the object runs, including any value a setter changed after construction, so a blob taken now and restored later resumes from this point. Construction parameters are not restored: restore into an object built the same way. A core may check some of them and refuse a blob built differently.
The blob is opaque and always state_bytes() long. Its layout is an
implementation detail of the C core and is not a stable format across
builds.
Raises RuntimeError if the BurstDespreader has already been
destroyed.
Returns:
| Type | Description |
|---|---|
bytes
|
Opaque snapshot, |
set_state
¶
Restore mutable state from a get_state() blob.
Overwrites the live state in place, including any value a setter
changed after construction. Construction parameters are not restored:
restore into an object built the same way. Length is validated against
state_bytes() before the blob is handed to the C core, which may
reject it too: a core may check some construction parameters and refuse
a blob built differently.
Raises TypeError if blob is not bytes, ValueError if its
length differs from state_bytes() or the core rejects it, and
RuntimeError if the BurstDespreader has already been destroyed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
blob
|
bytes
|
A |
required |
destroy
¶
Release the underlying C resources immediately.
Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this to release them at a definite point instead, or use the object as a context manager, which calls it on exit.
Idempotent: calling it again on an already-released object does
nothing. Every other method raises RuntimeError once it has run.
__enter__
¶
Enter a context manager, returning this object.
Lets a 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 |
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 partial correlations per code epoch: its despread output rate and
the grain of its look-back window, independent of |
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 |
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:
norm_freq
property
¶
MpskReceiver's tracked carrier frequency; 0.0 while searching.
steps
¶
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 rest of THIS call from where the hit's frame ended — the hit's own
samples_consumed, so no samples are 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:
steps_max_out
¶
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
¶
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 |
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
|
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
¶
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 pin refused on its domain leaves the receiver on its prior grid. A grid too large for the chain to allocate still aborts; sizing it out is
2112 work.¶
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 samples and terminal outputs per symbol) are
independently overridable here, still bridged by a freshly-sized
RateConverter -- never coupled to each other (see the module
docstring). It rebuilds dll/rc/rx at once, allocating every
replacement before freeing the old ones (Acquisition's acq_regrid()
discipline), and the pinned grid is the one every later chain is built
on: the next hit's hand-off and reset() both rebuild at it.
The new grid is checked before anything is rebuilt, and refused with the receiver left on its prior grid: segments >= 1, sps >= 2, a symbol no faster than a chip and a resampling ratio that is a normal double (doppler#2103), and an n MpskReceiver would accept (doppler#2426). Still an abort: an allocation failure in the rebuild, which runs under dp_xnn (doppler#2112).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
segments
|
int
|
Dll tracking segments per code period, >= 1. |
required |
sps
|
int
|
MpskReceiver samples per symbol (the resample target), >= 2. |
required |
n
|
int
|
MpskReceiver's terminal outputs per symbol ( |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the C call returns a non-zero status. The exception message is
|
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
¶
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
¶
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
¶
Serialize this object's mutable state to bytes.
Captures the state that evolves as the object runs, including any value a setter changed after construction, so a blob taken now and restored later resumes from this point. Construction parameters are not restored: restore into an object built the same way. A core may check some of them and refuse a blob built differently.
The blob is opaque and always state_bytes() long. Its layout is an
implementation detail of the C core and is not a stable format across
builds.
Raises RuntimeError if the DsssReceiver has already been destroyed.
Returns:
| Type | Description |
|---|---|
bytes
|
Opaque snapshot, |
set_state
¶
Restore mutable state from a get_state() blob.
Overwrites the live state in place, including any value a setter
changed after construction. Construction parameters are not restored:
restore into an object built the same way. Length is validated against
state_bytes() before the blob is handed to the C core, which may
reject it too: a core may check some construction parameters and refuse
a blob built differently.
Raises TypeError if blob is not bytes, ValueError if its
length differs from state_bytes() or the core rejects it, and
RuntimeError if the DsssReceiver has already been destroyed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
blob
|
bytes
|
A |
required |
destroy
¶
Release the underlying C resources immediately.
Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this to release them at a definite point instead, or use the object as a context manager, which calls it on exit.
Idempotent: calling it again on an already-released object does
nothing. Every other method raises RuntimeError once it has run.
__enter__
¶
Enter a context manager, returning this object.
Lets a 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. |
...
|
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. |
1000000.0
|
symbol_rate
|
float
|
Data-symbol rate, Hz: finite, > 0, at most |
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 |
55.0
|
pfa
|
float
|
Acquisition false-alarm target; default 1e-3. Also CarrierAcquisition's
own |
1e-3
|
pd
|
float
|
Acquisition detection-probability target; default 0.9. Also
CarrierAcquisition's own |
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). It acts in MpskReceiver's bits(), which this object does not offer, so steps()' symbols are the same either way (#2471). |
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 dp_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 sequential mode; default false -- its early per-block test fires on far too little averaging at SPEC's Es/N0 floor (as few as 4 blocks, 150-200+ Hz off), so false waits the full design_snr-derived dwell. |
False
|
refine_max_n_blocks
|
int
|
CarrierAcquisition's give-up cap in sequential mode, and the cap on the dwell in either 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 dp_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. Coupled, it is above half the sample rate (dp_acq_carrier_freq_ok()). |
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 exit is then reset(). |
0.0
|
reacquire
|
bool
|
Release on symbol lock alone, and search again by itself. With it set, the release clock counts samples of tracking with the SYMBOL lock flag down (code lock is not consulted), and when it passes lost_confirm_s the receiver resets itself to searching instead of entering lost: it never waits for reset(). A pool of receivers that are always online uses it. The clock runs from the first tracking sample, as above, so a track that never locks (a noise false alarm) is released too; size lost_confirm_s past the pull-in time to symbol lock, or a slow lock is released before it comes. Requires a lost_confirm_s of at least one input sample, since a release that never fires would never search again; a blob in the lost state is refused by set_state(). Default false: the release rule above. |
False
|
code_only_s
|
float
|
Seconds of code-only signal the waveform holds at any chip phase (a
symbol-free window; design async-dsss-parallel.md §4), >= 0 and finite.
The receiver hands its searcher |
0.0
|
doppler_rate
|
float
|
Doppler rate in Hz/s, >= 0 and finite, that the slow-time depth is
bounded against: at most |
0.0
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If construction fails. The exception message is ``AsyncDsssReceiver: invalid parameter (need a code of at least 2 chips, a finite chip_rate
|
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.tracking, rx.refining, rx.lost) # searching
(0, 0, 0)
refine_min_blocks
property
¶
Floor on the refine's dwell, in blocks (default 7). The refine's detection sizing needs fewer blocks the higher the C/N0, but the estimate the carrier chain must pull in from does not sharpen with it (#1265). Set with set_refine_min_blocks(), which carries the measurement.
threads
property
¶
Workers the searcher runs, the calling thread included; 1 = serial. Set with dp_async_dsss_receiver_set_threads().
rank
property
¶
Which surviving peak of a dwell the search takes; 0 = the strongest. Set with dp_async_dsss_receiver_set_rank().
coherent_bins
property
¶
The searcher's slow-time depth D: epochs per coherent block.
lifecycle
property
¶
Where the receiver is in its lifecycle, by name: "searching",
"refining", "tracking" or "lost" (released, waiting for
reset()). The same state as status().state, which carries it as the
C integer. With reacquire a release goes back to "searching" and
never reads "lost".
tracking
property
¶
1 once the live tracking chain is built and demodulating; 0 in every other state.
refining
property
¶
1 while refining: the CarrierAcquisition dwell; 0 in every other state.
lost
property
¶
1 once the release rule has fired: both lock flags were down,
continuously, for longer than lost_confirm_s while tracking -- an
emitter that left, or a seed that never locked. The loops have stopped
and samples are consumed and discarded; the holder releases the
assignment and calls reset(). 0 in every other state, and always 0 with
lost_confirm_s = 0.
doppler_hz
property
¶
The Doppler estimate the chain was built from, Hz: the seed's while refining, the CarrierAcquisition-refined value once tracking. The live carrier is status().doppler_hz.
cn0_dbhz_est
property
¶
The seed's C/N0 estimate, dB-Hz, unchanged until reset().
segments
property
¶
The live-tracking Dll's partial correlations per code period (refine_segments is the refine collector's).
chip_phase
property
¶
The live Dll's code phase, chips, in the Dll's instantaneous convention -- the mirror of acq_result_t::code_phase's correlation-lag convention (dp_acq_build_handoff()). While refining there is no live Dll: it is the seed's phase advanced by the clock dilation since, the phase the hand-over starts the live chain from.
code_rate
property
¶
The Dll's code rate, chips per nominal chip (~1.0 at lock), without the carrier aid; a coasting Dll reports its held rate.
lock
property
¶
The carrier lock statistic of the MpskReceiver this receiver tracks with: the EMA of its M-th-power lock signal, near 1 when locked and near 0 on noise.
norm_freq
property
¶
The post-despread carrier the MpskReceiver tracks: its centre plus
its loop's integrator, cycles/sample at its input rate (sps *
symbol_rate). Lags a Doppler ramp by the Type-II ramp error.
nco_freq
property
¶
The MpskReceiver's carrier NCO command, its loop filter's whole
output, cycles/sample at its input rate (sps * symbol_rate). Its mean
tracks a Doppler ramp with no lag (unlike get_norm_freq()); its
variance is the carrier loop's stress.
locked
property
¶
The receiver's symbol-lock flag: a hysteretic detector over get_lock_metric(), declared after 30 consecutive symbols strictly above get_lock_threshold() (0.5) and dropped after 15 consecutive symbols strictly below 0.3 (ASYNC_DSSS_RX_LOCK_N_UP, _UP, _N_DOWN and _DOWN, fixed at create). The flag the release rule and a pool's holder act on.
lock_metric
property
¶
Symbol-lock metric = SNR-weighted EMA of (I^2-Q^2)/(I^2+Q^2) =
cos(2*phi) over the emitted symbols (locked -> ~+1). Drives
locked.
lock_threshold
property
¶
The lock-metric threshold locked is declared strictly above (0.5,
ASYNC_DSSS_RX_LOCK_UP); exposed alongside lock_metric for engineering
debug.
car_last_error
property
¶
Pre-despread Costas phase discriminator (rad): the residual carrier phase LOOP 1 (which de-rotates before the Dll) is not nulling.
car_nco_freq
property
¶
LOOP 1 (pre-despread Costas) loop-filter output = NCO frequency command, cycles/sample of the front-end (chip_rate*spc) rate.
mpsk_last_error
property
¶
MpskReceiver carrier phase discriminator (rad): the residual carrier phase LOOP 2 (post-despread) is not nulling.
code_locked
property
¶
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
¶
Stream raw cf32 samples; emit demodulated symbols once tracking.
Drives the state machine. It emits nothing while searching or refining
(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. A hit inside a call hands
the rest of the call straight to the refine chain, so no sample is
dropped there; at the refine's end, what the refine did not consume of
the last call goes on to the live chain. Accepts any block size; state
carries across calls, so a capture can be fed in frames of any length
with no seam. Lost (after the release rule fires) consumes 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, or on the symbol lock flag alone with reacquire (see
lost_confirm_s). Tracking emits symbols whether or not they are data:
a noise false alarm can reach tracking and emit until the release rule
lets it go -- or, at the default lost_confirm_s of 0, until reset() --
and symbol lock (locked) is what says the symbols are a signal's, so
read it rather than the count (#2439). 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 lost, or before 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:
steps_max_out
¶
The output capacity steps() needs beyond the input's.
Returns 0, which is NOT a bound. A call that completes a code
period carried over from earlier calls emits all of that
period's symbols (code_len times symbol_rate over chip_rate of
them), even if it brings one sample. At more than one symbol per
period a call can therefore emit more symbols than its x_len,
and those past max_out are dropped (see
dp_async_dsss_receiver_steps()). A true bound is #2469.
Returns:
| Type | Description |
|---|---|
int
|
0. |
seed
¶
Take a detection from outside and start refining from it.
The hand-off of docs/design/async-dsss-receiver.md §2.2: 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 (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; dp_acq_build_handoff() makes it from a hit's lag. 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 searching: an outside hit simply
beats the receiver's own. Also refused outside the seed domain,
dp_async_dsss_receiver_seed_ok().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
chip_phase
|
float
|
Code phase at the next sample, chips, in |
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(), unchanged until reset(). |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the C call returns a non-zero status. The exception message is
|
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)
>>> rx.seed(chip_phase=512.25, doppler_hz_est=-1500.0,
... cn0_dbhz_est=48.0)
>>> (rx.refining, rx.doppler_hz, rx.cn0_dbhz_est)
(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):
status
¶
Read the status record, by value: the state; where the emitter is now (the live carrier), its chip phase, code rate and C/N0; both lock flags with the symbol-lock metric and threshold; both carrier residuals; and the two clocks in input samples (since the state was entered; both flags down without a break).
Cheap and allocation-free: every field is a read of live state. The
one-at-a-time getters report the same fields, doppler_hz apart (see
the record); this is the face a controller reads. No timestamp: the
clocks are in input samples, and the holder, which owns the sample
clock, stamps the record.
Returns:
| Type | Description |
|---|---|
ReceiverStatus
|
The record, by value. |
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)
>>> st = rx.status()
>>> (st.state, st.doppler_hz, st.code_locked, st.locked) # searching
(0, 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
¶
Pin the embedded Acquisition's search grid directly. Forwards to
dp_acq_configure_search_raw() -- the escape hatch under this object's
symbol_rate-driven auto-sizing, for a power user who wants a specific
(doppler_bins, n_noncoh). The pin replaces the whole auto-sized grid,
the window-tiled search included. Only meaningful while searching; the
search does not run again until the next reset().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
doppler_bins
|
int
|
The coherent depth to pin, in |
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
|
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
¶
Floor the refine's dwell at n_blocks, whatever the detection sizing asks (docs/design/async-dsss-receiver-measurements.md §12.16, #1265).
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 the
floor dwell of docs/design/async-dsss-receiver-measurements.md §12.10)
hold the estimate to 77 Hz. Applied to the next refine chain built -- a
receiver already refining keeps its dwell. A mutator's value, so it
travels in the state blob (#2022). 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
|
Examples:
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.
Forwards to dp_dll_configure_lock_raw() on the live tracking Dll (the
one behind get_code_locked()), NOT the refine-stage collection Dll.
It retunes the Dll the receiver holds now; every chain build (a seed,
the hand-over, reset() or configure_chain_raw()) replaces that Dll with
a freshly sized one, so call it once tracking. The detector is the
hysteretic lockdet over the DLL's per-N-look CFAR statistic — the
levels and verify counts trade declare latency against false-alarm
rate.
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 # replaced at the next chain build
0
configure_chain_raw
¶
Pin the live-tracking despread/resample/demod grid directly. The
escape hatch for the composition-specific knob: segments (the live
Dll's tracking parameter) and sps/n (MpskReceiver's samples and
terminal outputs per symbol) are independently overridable, still
bridged by a freshly-sized RateConverter and never coupled to each
other. While searching/refining it re-pins the grid used to build the
next tracking chain; once tracking it rebuilds dll/rc/rx in
place, allocating every replacement before adopting it, and re-seeds
the rebuilt chain from the whole carrier -- loop 1 plus loop 2's
integrator, the sum status() reports -- so a retune keeps the signal it
was tracking.
The new grid is checked as create() checked the first, before anything is rebuilt, and refused with the receiver left on its prior grid: the Dll's domain at segments, sps >= 2, a symbol period of at most 2^20 Dll partials, which the Dll's aid would otherwise refuse mid-build (doppler#2103), and an n MpskReceiver would refuse (#2421). Still an abort: an allocation failure in the rebuild (doppler#2112).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
segments
|
int
|
Live-tracking Dll segments per code period. |
required |
sps
|
int
|
MpskReceiver samples per symbol (the resample target), >= 2. |
required |
n
|
int
|
MpskReceiver's terminal outputs per symbol ( |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the C call returns a non-zero status. The exception message is
|
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
¶
Return to the searching state. Resets the embedded Acquisition and rebuilds the refine-stage chain and the live tracking chain back to their placeholder seed (phase 0, no Doppler). A receiver that has locked cannot be "reset back to tracking the same signal," only back to searching -- every object's reset() semantics in this codebase.
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)
set_telemetry
¶
Attach (or detach) a telemetry context across the receiver's loops, and register the receiver's own lifecycle and symbol-lock decision.
The receiver reports under the prefix, once per steps() call that carries samples, after the call, so on the time axis at the stamp the caller set for that block (dp_tlm_set_now()):
- "
.state" -- where the call left the receiver, the ASYNC_DSSS_RX_* value: 0 searching, 1 refining, 2 tracking, 4 lost (status().state); - "
.handover_hz" -- the Doppler the chain was built from, Hz (dp_async_dsss_receiver_get_doppler_hz()): 0 before a seed, the seed's while refining, the refined value from the hand-over on, and 0 again after a refused hand-over or a reset(), which return the receiver to searching. A record is a float, so it resolves about 0.5 Hz near 5 MHz. Not the live carrier: status().doppler_hz reads that, and ".car.freq" and ".car.nco" are the carrier loop's own view of it, in cycles a sample at the input rate, so times chip_rate * spc for Hz; - "
.refused" -- the hits refused as seeds so far (status().refused_hits).
Feed blocks of about a code period, as the examples do, and these read as a time series of the lifecycle: when the search hit, how long the refine took, what it handed over, when the release came.
Each loop reports under the prefix, at its own update rate:
- "
.car" -- the chip-domain carrier loop (dp_costas_set_telemetry(): ".lock", ".e", ".freq", ".locked", ".nco"), once per carrier update: once a code period, except while it coasts, when loop 1 is held; - "
.code" -- the code loop (dp_dll_set_telemetry(): ".e", ".rate", ".lock", ".locked", ".nco"), once per discriminator update; - "
.demod" -- the symbol-domain demodulator (dp_mpsk_receiver_set_telemetry(): its lock, its carrier and timing loops, the symbols, its AGC), once per recovered symbol (the AGC's pair once per gain update); - "
.acq" -- the acquisition engine (dp_acq_set_telemetry()), once per decided dwell; - "
.rc" -- the rate converter between the code loop and the demodulator (dp_RateConverter_set_telemetry()). It reports only an AGC, and this receiver builds it without one, so it registers no probes; - "
.sym.lock" and " .sym.locked" -- the receiver's own symbol-lock metric (dp_async_dsss_receiver_get_lock_metric()) and its decision (dp_async_dsss_receiver_get_locked()), the one its users act on, once per emitted symbol.
The carrier and code loops report their filters two ways: ".car.freq" and ".code.rate" (and the demodulator's ".demod.car.freq") are the integrators, each loop's frequency memory, which a coast holds; ".car.nco", ".code.nco" and ".demod.car.nco" are the complete commands the NCOs run at, the proportional term (and the code loop's carrier aid) included. ".demod.sync.rate" is the timing loop's output, and ".e" is each discriminator's input to its filter.
The attachment is a REQUEST kept on the receiver: every seed rebuilds the tracking chain, and the rebuilt loops are attached again under the same names (so the same probe ids). Records are therefore continuous across re-seeds, and a reset keeps the attachment too.
Setup path, never hot; the context is borrowed and must outlive the attachment, and the receiver must step on one thread (SPSC rules in dp_tlm/dp_tlm_core.h). Passing NULL detaches every loop.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tlm
|
object | None
|
Telemetry context to attach, or NULL to detach. |
required |
prefix
|
str
|
Probe-name prefix; NULL means "rx". |
required |
decim
|
int
|
Emit every decim-th event on each probe; >= 1. |
1
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the C call returns a non-zero status. The exception message is
|
Examples:
>>> import numpy as np
>>> from doppler.dsss import AsyncDsssReceiver
>>> from doppler.telemetry import Telemetry
>>> 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)
>>> tlm = Telemetry(1 << 12)
>>> rx.set_telemetry(tlm, "rx")
>>> sorted(p for p in tlm.probe_names if p.startswith("rx.c"))
['rx.car.e', 'rx.car.freq', 'rx.car.lock', 'rx.car.locked', 'rx.car.nco', 'rx.code.e', 'rx.code.lock', 'rx.code.locked', 'rx.code.nco', 'rx.code.rate']
>>> sorted(p for p in tlm.probe_names if p.startswith(("rx.s", "rx.h",
... "rx.r")))
['rx.handover_hz', 'rx.refused', 'rx.state', 'rx.sym.lock', 'rx.sym.locked']
>>> rx.set_telemetry(None, "rx") # detach
set_threads
¶
Set the workers the searcher fans its Doppler tiles across.
Forwards to dp_acq_set_threads() on the receiver's searcher. The search is bit-identical at every count, so this changes the cost of a steps() call and nothing about its output. A pool that steps its receivers in parallel sets each one to 1. A mutator's value, so it travels in the state blob (#2022). The blob carries the count that RAN, not the request: set_threads(0) on a 20-core writer restores as 20 workers on any reader, by design -- a resumed receiver runs as the one that was checkpointed, and a pool's 1 is 1 everywhere. Setup path; not while another thread is inside steps().
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
|
Examples:
set_rank
¶
Take the rank-th surviving peak of a dwell, not the strongest.
The searcher reports up to ASYNC_DSSS_RX_MAX_PEAKS peaks per dwell, strongest first. The receiver drops every peak within one chip (circularly) of a stronger one, so one signal's neighbouring cells hold one place; then every peak a stronger signal's own sidelobes could explain; then every peak within one chip of an excluded code phase (set_exclusions()). The survivors are ranked from 0. A dwell with no peak at this rank hands nothing off and the search goes on.
The sidelobe rule: a strong signal's list also holds its code's Doppler-offset sidelobes, up to dp_acq_dsl_db() below it and hundreds of chips from its phase (design acq-surface.md §8). A peak counts as a second signal only if its test_stat exceeds the stronger signals' summed test_stat, times that level, by more than the detector's own gate (dp_acq_gate_stat()). A sidelobe then passes only when the noise on it alone would have cleared the gate, the rate the search already runs at. What it costs is near-far: a second signal much more than dp_acq_dsl_db() below a stronger one (-16.95 dB on the SPEC Gold-1023 search) is not taken while the stronger one is on the air (async-dsss-parallel.md §3). Searchers that share a stream and its block grid (get_coherent_bins()) and hold ranks 0..k-1 therefore take k different signals in one dwell. Takes effect at the next dwell. A mutator's value, so it travels in the state blob (#2022).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rank
|
int
|
0 (the strongest, the default) to ASYNC_DSSS_RX_MAX_PEAKS - 1. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the C call returns a non-zero status. The exception message is
|
Examples:
set_exclusions
¶
Code phases the search must not take: signals another receiver already tracks.
Replaces the list. A peak within one chip, circularly, of any of them is dropped from a dwell's list at any Doppler, before ranking (set_rank()). Phases are chips in status().chip_phase's convention and are folded into [0, code_len). An empty list (the default) excludes nothing. A tracking receiver is not affected: the list applies to its next search. A mutator's value, so it travels in the state blob (#2022).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
phases
|
NDArray[float64]
|
Code phases, chips; finite. May be NULL when phases_len is 0. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the C call returns a non-zero status. The exception message is
|
Examples:
state_bytes
¶
Size in bytes of this object's serialized state.
The exact length get_state returns and set_state requires. It
depends on how the object was constructed (state arrays are sized at
construction), so read it from the instance rather than assuming a
constant.
Raises RuntimeError if the AsyncDsssReceiver has already been
destroyed.
Returns:
| Type | Description |
|---|---|
int
|
Byte length of one serialized state blob. |
get_state
¶
Serialize this object's mutable state to bytes.
Captures the state that evolves as the object runs, including any value a setter changed after construction, so a blob taken now and restored later resumes from this point. Construction parameters are not restored: restore into an object built the same way. A core may check some of them and refuse a blob built differently.
The blob is opaque and always state_bytes() long. Its layout is an
implementation detail of the C core and is not a stable format across
builds.
Raises RuntimeError if the AsyncDsssReceiver has already been
destroyed.
Returns:
| Type | Description |
|---|---|
bytes
|
Opaque snapshot, |
set_state
¶
Restore mutable state from a get_state() blob.
Overwrites the live state in place, including any value a setter
changed after construction. Construction parameters are not restored:
restore into an object built the same way. Length is validated against
state_bytes() before the blob is handed to the C core, which may
reject it too: a core may check some construction parameters and refuse
a blob built differently.
Raises TypeError if blob is not bytes, ValueError if its
length differs from state_bytes() or the core rejects it, and
RuntimeError if the AsyncDsssReceiver has already been destroyed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
blob
|
bytes
|
A |
required |
destroy
¶
Release the underlying C resources immediately.
Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this to release them at a definite point instead, or use the object as a context manager, which calls it on exit.
Idempotent: calling it again on an already-released object does
nothing. Every other method raises RuntimeError once it has run.
__enter__
¶
Enter a context manager, returning this object.
Lets a 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. |
...
|
The receiver reports one consistent picture by value through status().
It carries no timestamp: the caller owns the sample clock and stamps it.
ReceiverStatus
¶
Bases: tuple[int, float, float, float, float, int, int, float, float, float, float, int, int, int]
AsyncDsssReceiver's status record: state (0 searching, 1 refining, 2 tracking, 4 lost), the live estimates, both lock flags, and the two clocks in input samples.
Attributes:
| Name | Type | Description |
|---|---|---|
state |
int
|
ASYNC_DSSS_RX_SEARCHING .. _LOST -- where it is. |
doppler_hz |
float
|
Where the emitter is NOW: the whole carrier estimate, Hz -- loop 1's plus what loop 2 has taken up beyond it (the seed while refining, 0 while searching, frozen where it was when lost). Only as good as |
chip_phase |
float
|
Code phase, chips: the live Dll's; while refining, the seed's advanced by the clock dilation since (what the hand-over starts from), so a refining receiver's signal can be excluded too. |
code_rate |
float
|
The Dll's code rate, chips per nominal chip (~1.0 at lock), without the carrier aid; a coasting Dll's is its held rate. |
cn0_dbhz_est |
float
|
C/N0 estimate, dB-Hz (the seed's). |
code_locked |
int
|
Presence flag: the Dll's lock detector. |
locked |
int
|
Health flag: the symbol-lock detector. |
lock_metric |
float
|
cos(2*phi) over the symbols, drives |
lock_threshold |
float
|
|
car_last_error |
float
|
Pre-despread Costas residual, rad. |
mpsk_last_error |
float
|
Post-despread carrier residual, rad. |
state_samples |
int
|
Input samples since |
both_down_samples |
int
|
Input samples both flags have been down without a break (the release clock); in lost it keeps counting -- samples since the flags dropped. With reacquire, the symbol lock flag's clock alone. |
refused_hits |
int
|
Searching hits refused as seeds by the seed domain (a Doppler at or past fs/2), since create or reset. The search went on over each one's tail. |
doppler_hz
property
¶
Where the emitter is NOW: the whole carrier estimate, Hz -- loop 1's
plus what loop 2 has taken up beyond it (the seed while refining, 0
while searching, frozen where it was when lost). Only as good as
locked: with the carrier unlocked, loop 1 free-runs and this
wanders.
chip_phase
property
¶
Code phase, chips: the live Dll's; while refining, the seed's advanced by the clock dilation since (what the hand-over starts from), so a refining receiver's signal can be excluded too.
code_rate
property
¶
The Dll's code rate, chips per nominal chip (~1.0 at lock), without the carrier aid; a coasting Dll's is its held rate.
both_down_samples
property
¶
Input samples both flags have been down without a break (the release clock); in lost it keeps counting -- samples since the flags dropped. With reacquire, the symbol lock flag's clock alone.
refused_hits
property
¶
Searching hits refused as seeds by the seed domain (a Doppler at or past fs/2), since create or reset. The search went on over each one's tail.
Related pages¶
Gallery — Streaming Async Despreader, AsyncDsssReceiver: a signal's lifecycle, AsyncDsssReceiver: the SPEC link, DsssBurstReceiver — the Composed Burst Chain, DsssReceiver — the Composed Continuous DSSS Receiver, Gallery, Full-Chain Lock-Up
Guides — DSSS Burst Acquisition, Lock Detection Across doppler.track, Checkpoint & Resume, Waveforms — what you can generate
Design — API taxonomy: the DSP building-block hierarchy and its naming axis, Parallel Asynchronous-Data DSSS Receivers, AsyncDsssReceiver — the measurement record, AsyncDsssReceiver — the continuous DSSS receiver, from spec to object, BurstCapture: acquisition's output, turned into bursts, 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, A Frame as a Description, Design, MPSK Receiver, The Polynomial-Phase Estimator — the reasoning, A Receiver Built from a Frame Description, State Serialization — the standard bytes interface, Telemetry — zero-cost scalar taps for running pipelines
Contributing — Adding an algorithm — the lifecycle, DSSS Primary Use Cases for Code Acquisition Design, Validation log