Python Loop Filter API¶
The doppler.track module provides LoopFilter — a second-order
proportional-integral loop filter, the shared engine of every tracking loop
(Costas/PLL, DLL, symbol timing). An error e goes in, a control value comes
out (control = integ + kp·e), and the integrator advances integ += ki·e, so
the integrator holds the running frequency/rate estimate and kp·e is the
instantaneous (phase) correction.
Source:
src/doppler/track/__init__.py
See the DSSS despreader gallery page, which uses two of these — one for the carrier loop, one for the code loop.
How it works¶
The gains are derived from a loop noise bandwidth bn (normalized,
cycles/sample), a damping factor zeta (0.707 = critically damped), and the
update period t (samples):
wn = 8·zeta·bn / (4·zeta² + 1)
theta = wn·t
kp = 8·zeta·theta / (4 + 4·zeta·theta + theta²)
ki = 4·theta² / (4 + 4·zeta·theta + theta²)
configure(bn, zeta, t) recomputes the gains while preserving the integrator
(so a tracker can retune mid-stream without losing lock); reset() zeroes the
integrator. The state struct is public C, so trackers embed it by value and
drive it with the same kernel — there is no per-update allocation.
Examples¶
Drive a loop with a constant error¶
from doppler.track import LoopFilter
lf = LoopFilter(bn=0.02, zeta=0.707, t=1.0)
lf.step(1.0) # integ += ki; returns integ + kp
round(lf.integ, 6) # == ki (one update of unit error)
Retune without losing the estimate¶
Costas — carrier-tracking loop¶
Costas is the first loop built on LoopFilter: a continuous BPSK
carrier-recovery loop. Per sample it de-rotates the input with the integer-phase
source.LO NCO (carrier wipe-off); every tsamps samples it
dumps the coherent integrate-and-dump accumulator, runs a decision-directed
Costas phase discriminator, filters the error through an embedded LoopFilter,
and steers the NCO frequency and phase. It tracks the small residual carrier
offset left after FFT acquisition removes the bulk Doppler — an offset larger
than the per-symbol integration bandwidth must be removed upstream, not by the
loop. Because the steering NCO is integer-phase, the carrier phase is bounded and
exactly reproducible (no double-accumulator drift).
FLL assist. Setting bn_fll > 0 enables a frequency-lock-loop assist: a
data-wiped cross-product frequency discriminator over consecutive prompts whose
linear range is far wider than the phase discriminator's. It pulls the loop's
frequency integrator onto a large or fast-moving residual the bare PLL cannot
acquire, then the PLL refines phase (an FLL-assisted PLL). bn_fll = 0 (the
default) is a pure Costas PLL.
See the carrier loop stress gallery page for the bare PLL stalling on a large residual while the FLL assist pulls it in.
from doppler.track import Costas
from doppler.wfm import Synth
rx = Synth(type="qpsk", sps=16, snr=20, freq=0.01).steps(4096) # received IQ
# bn_fll > 0 adds the FLL assist for large/fast-moving residuals
c = Costas(bn=0.05, zeta=0.707, init_norm_freq=0.0, tsamps=16, bn_fll=0.03)
symbols = c.steps(rx) # one complex prompt symbol per tsamps samples
f_est = c.norm_freq # tracked residual carrier (cycles/sample)
locked = c.lock_metric # |Re P|/|P| EMA, ~1.0 when phase-locked
CarrierMpsk — M-PSK carrier-tracking loop¶
CarrierMpsk is the M-ary generalization of Costas: the same integer-NCO
wipe-off, coherent integrate-and-dump, embedded LoopFilter, and FLL assist,
but with a decision-directed M-PSK phase discriminator instead of the BPSK
one. Each symbol it slices the prompt to the nearest constellation point
ahat and forms e = Im(P · conj(ahat)) / |P| (the sine of the residual phase
error near lock). m selects the constellation — 2 (BPSK), 4 (QPSK), or
8 (8PSK); at m = 2 it is byte-for-byte the Costas loop (same prompt
stream, same tracked frequency), which is the loop's validation anchor.
The loop locks to one of m phases — an M-fold ambiguity on absolute
phase. Resolve it downstream with differential demapping
(mpsk.mpsk_diff_demap) or a sync word; this loop only
recovers the carrier and emits the prompts. The FLL assist (bn_fll > 0)
matters more as m grows: 8PSK's phase discriminator is linear only over
±π/8, so a sizeable residual needs the wide cross-product frequency
discriminator to pull in before the PLL can refine phase.
from doppler.track import CarrierMpsk
# QPSK carrier loop, 16 samples/symbol, FLL-assisted; all params keyword-capable
c = CarrierMpsk(bn=0.05, zeta=0.707, init_norm_freq=0.0, tsamps=16, bn_fll=0.01, m=4)
symbols = c.steps(rx) # one complex prompt symbol per tsamps samples
f_est = c.norm_freq # tracked residual carrier (cycles/sample)
locked = c.lock_metric # Re(P conj ahat)/|P| EMA, ~1.0 when phase-locked
# resolve the M-fold ambiguity downstream, e.g. mpsk_diff_demap(mpsk_demap(...))
CarrierNda — non-data-aided carrier loop¶
CarrierNda is the non-data-aided carrier-recovery loop — the cold-start
counterpart to CarrierMpsk. Per sample it de-rotates with the integer lo NCO;
it filters the de-rotated samples through a free-running I/Q boxcar moving
average of sps/n samples (one output per input sample — no rate change), and
on every sample runs an M-th-power phase discriminator
(z²/z⁴/z⁸ by repeated squaring). Raising the arm sample to the Mth power
strips the M-PSK data, so the loop acquires the carrier with no symbol timing
and no data present — a bare/unmodulated carrier, or modulated data before
timing settles. phase_error = Im(z^M) (gain-normalized to a slope-2 S-curve for
every M); lock is the M-th-power lock metric. It locks to one of m phases
(M-fold ambiguity, resolved downstream). steps() returns the de-rotated sample
stream. See the NDA carrier gallery and the
MPSK receiver design.
from doppler.track import CarrierNda
# QPSK NDA loop, 8 samples/symbol, sps/n = 2-sample boxcar arm; keyword-capable
c = CarrierNda(bn=0.01, zeta=0.707, init_norm_freq=0.0, sps=8, n=4, m=4)
derot = c.steps(rx) # de-rotated samples (one per input sample)
f_est = c.norm_freq # tracked carrier (cycles/sample)
locked = c.lock # M-th-power lock metric (→ lock_scale when locked)
MpskReceiver — pulse-shaped M-PSK modem¶
MpskReceiver is a complete per-sample M-PSK demodulator that composes the
tracking primitives on one shared sample loop: a CarrierNda carrier loop
(per-sample integer-NCO wipe-off + non-data-aided M-th-power acquisition), an
owned matched filter on the de-rotated stream (pulse="iandd" integrate-and-
dump boxcar by default, or pulse="rrc" root-raised-cosine for band-limited
links), and a SymbolSync Gardner timing loop. Carrier recovery follows the
project rule — predetection de-rotation (always) and postdetection
discrimination: the NDA loop acquires with no data and no symbol timing, then,
when auto_handover=1 (opt-in) and the loop has locked, the receiver hands the
shared NCO to a lower-jitter decision-directed loop on the recovered symbols
(essential for 8PSK, whose M-th-power phase noise would otherwise cross the
±π/8 margins). The loop locks to one of m phases (M-fold ambiguity); resolve it
with bits(..., differential=1) or a sync word. steps() returns the recovered
symbols; bits() returns hard Gray bits (coherent, or rotation-invariant
differential). A DSSS-MPSK receiver is Dll(segments) → MpskReceiver. All
constructor parameters are keyword-capable with defaults. See the
MPSK receiver gallery and the
MPSK receiver design.
from doppler.track import MpskReceiver
from doppler.wfm import Synth
iq = Synth(type="qpsk", sps=8, snr=20).steps(4096) # received IQ
# QPSK, 8 samples/symbol, I&D matched filter; NDA acquisition + opt-in handover
rx = MpskReceiver(m=4, sps=8, n=4, pulse="iandd",
bn_carrier=0.01, bn_timing=0.01,
auto_handover=1, lock_thresh=0.4)
sym = rx.steps(iq) # recovered symbols (~ len(iq) / sps)
bits = rx.bits(iq) # hard Gray bits (LSB-first per symbol)
f = rx.norm_freq # tracked carrier (cycles/sample)
lk = rx.lock # carrier lock metric (-> + at lock, every M)
Dll — code-tracking loop¶
Dll is the code-loop counterpart to Costas: a delay-lock loop that tracks
the phase of a continuous, repeating spreading code (PN / Gold sequence) on a
carrier-wiped sample stream. Per sample it correlates the input against
three taps of the local code — early (+spacing chips), prompt, late
(-spacing chips) — accumulating an integrate-and-dump over one code period;
per period it runs the non-coherent envelope discriminator
(|E| - |L|) / (|E| + |L|), filters it through an embedded LoopFilter, and
steers the code rate and phase. The half-chip discriminator is steep, so the
loop bandwidth is small (a few thousandths); Dll is data-insensitive (it works
on envelopes, so BPSK data flips don't matter).
In a full receiver the carrier loop (Costas) wipes the carrier and the Dll
wipes the code; a channel composes the two.
import numpy as np
from doppler.track import Dll
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
# code: 0/1 chips for one period; sps samples per chip
d = Dll(code, sps=4, init_chip=0.0, bn=0.005, zeta=0.707, spacing=0.5)
symbols = d.steps(rx) # one prompt symbol per code period
phase = d.code_phase # tracked code phase (chips)
rate = d.code_rate # tracked chip rate (~1.0 + code Doppler)
Sub-epoch partials for an asynchronous symbol clock (segments). When the
data-symbol rate is on the order of the code-epoch rate but asynchronous to it,
a coherent full-epoch despread straddles data transitions and collapses. Set
segments > 1 to split each epoch into that many sub-epoch partial
correlations: steps() then emits segments partial prompts per period — a
stream at ~segments samples/symbol (since symbol ≈ epoch) for a downstream
symbol matched filter + SymbolSync — and the code is tracked non-coherently
across the partials ((Σ|E| − Σ|L|)/(Σ|E| + Σ|L|)), which a data flip cannot
collapse. segments=1 (default) is the plain coherent DLL above; choose ≥ 2
for symbol-timing recovery. This segments mode is the streaming despreader:
its job is to remove the PN code and output samples. Because the code loop is
non-coherent it is carrier-blind — it locks with a residual carrier still on
the samples, and (a short partial window being carrier-tolerant) the residual
just rides out on the partials. Carrier recovery (Costas) and symbol
extraction (SymbolSync) are downstream, fed from this output. See the
streaming async despreader gallery and the
async despreader design note.
# 4 partial correlations per epoch -> non-coherent (carrier-blind) code tracking
# + an oversampled async-BPSK stream; carrier + symbol recovery are downstream.
d = Dll(code, sps=8, bn=0.002, zeta=0.707, spacing=0.5, segments=4)
partials = d.steps(rx) # 4 partial prompts per code epoch (PN removed)
# downstream: Costas(...).steps(partials) -> SymbolSync(...).steps(...) -> bits
Channel — full tracking channel¶
Channel is the receiver that composes the loops: a Costas carrier loop and a
Dll code loop 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
nav_period 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 nav_period prompts per bit. The channel is seeded by
acquisition (coarse carrier frequency + code phase) and tracks the residual.
See the tracking channel gallery page for the full receiver acquiring and despreading end to end.
from doppler.track import Channel
ch = Channel(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, nav_period=1)
symbols = ch.steps(rx) # one despread prompt per code period
bits = ch.bits(rx) # hard data bits (bit-synced when nav_period > 1)
SymbolSync — symbol timing recovery¶
SymbolSync recovers the symbol clock of an asynchronous data stream (a
symbol rate not locked to the sample clock). It is a Gardner timing-error
detector closing a PI loop around an integer timing NCO and a Farrow
interpolator: the NCO's post-wrap value is the interpolation fraction µ (free, no
floating-point timing phase), so timing stays exact while only the interpolation
is floating point. Two interpolants per symbol (on-time + mid) are derived from
the phase value, and the loop steers the NCO frequency only — slip-free, so
the strobe count never drifts.
steps() emits one timing-corrected symbol per recovered instant; rate is the
tracked samples/symbol; order picks the Farrow interpolator. See the
symbol-timing gallery page for the loop locking and
tracking an asynchronous clock end to end.
from doppler.track import SymbolSync
ss = SymbolSync(sps=4, bn=0.01, zeta=0.707, order="cubic")
symbols = ss.steps(rx) # timing-corrected symbols
ss.rate # recovered samples/symbol
LoopFilter
¶
LoopFilter component.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bn
|
float
|
bn constructor parameter. |
0.01
|
zeta
|
float
|
zeta constructor parameter. |
0.707
|
t
|
float
|
t constructor parameter. |
1.0
|
Examples:
Create with defaults:
step
¶
Advance the loop one update with error x; return the control.
integ += ki*x; return integ + kp*x.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
float
|
Loop error. |
required |
Returns:
| Type | Description |
|---|---|
float
|
Control value (integ + kp*x). |
steps
¶
Run a block of errors through the loop.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[float64]
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[float64]
|
Output. |
configure
¶
Recompute the loop gains for a new (bn, zeta, t); preserves the integrator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bn
|
float
|
Loop noise bandwidth, normalized cycles/sample (>= 0). |
required |
zeta
|
float
|
Damping factor (typically 0.707). |
required |
t
|
float
|
Update period in samples (> 0). |
required |
Costas
¶
Costas component.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bn
|
float
|
bn constructor parameter. |
0.05
|
zeta
|
float
|
zeta constructor parameter. |
0.707
|
init_norm_freq
|
float
|
init_norm_freq constructor parameter. |
0.0
|
tsamps
|
int
|
tsamps constructor parameter. |
64
|
bn_fll
|
float
|
bn_fll constructor parameter. |
0.0
|
Examples:
Create with defaults:
>>> from doppler.track import Costas
>>> obj = Costas(bn=0.05, zeta=0.707, init_norm_freq=0.0, tsamps=64, bn_fll=0.0)
steps
¶
De-rotate a cf32 block with the integer-NCO carrier, coherently integrate over each tsamps-sample symbol, run the decision-directed Costas discriminator, and emit one complex prompt symbol per symbol.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
Output. |
configure
¶
Recompute the loop gains for a new (bn, zeta); preserves the frequency/phase estimate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bn
|
float
|
Input. |
required |
zeta
|
float
|
Input. |
required |
CarrierMpsk
¶
Create an M-PSK carrier loop instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bn
|
float
|
Loop noise bandwidth (default 0.05). |
0.05
|
zeta
|
float
|
Damping factor (default 0.707). |
0.707
|
init_norm_freq
|
float
|
Seed carrier frequency, cycles/sample (default 0.0). |
0.0
|
tsamps
|
int
|
Samples per symbol (default 64). |
64
|
bn_fll
|
float
|
FLL-assist bandwidth (default 0.0 = pure PLL). |
0.0
|
m
|
int
|
Constellation order M, 2/4/8 (default 4 = QPSK). |
4
|
Examples:
Create with defaults:
>>> from doppler.track import CarrierMpsk
>>> obj = CarrierMpsk(bn=0.05, zeta=0.707, init_norm_freq=0.0, tsamps=64, bn_fll=0.0, m=4)
steps
¶
De-rotate a cf32 block with the integer-NCO carrier, coherently integrate over each tsamps-sample symbol, run the decision-directed M-PSK discriminator (slice to the nearest constellation point, error Im(P*conj(ahat))/|P|), and emit one complex prompt symbol per symbol. The loop tracks a small residual carrier (bulk Doppler removed upstream); it locks to one of m phases, so resolve the M-fold ambiguity downstream (mpsk_diff_demap or a sync word). At m=2 this is exactly the BPSK Costas loop.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
Output. |
configure
¶
Recompute the loop gains for a new (bn, zeta); preserves the frequency/phase estimate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bn
|
float
|
Input. |
required |
zeta
|
float
|
Input. |
required |
CarrierNda
¶
Create an NDA carrier loop instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bn
|
float
|
Loop noise bandwidth (default 0.01). |
0.01
|
zeta
|
float
|
Damping factor (default 0.707). |
0.707
|
init_norm_freq
|
float
|
Seed carrier frequency, cycles/sample (default 0.0). |
0.0
|
sps
|
int
|
Samples per symbol (default 8). |
8
|
n
|
int
|
MA window divisor: window = sps/n (default 4; sps%n==0). |
4
|
m
|
int
|
Constellation order M, 2/4/8 (default 4 = QPSK). |
4
|
Examples:
Create with defaults:
>>> from doppler.track import CarrierNda
>>> obj = CarrierNda(bn=0.01, zeta=0.707, init_norm_freq=0.0, sps=8, n=4, m=4)
steps
¶
De-rotate a cf32 block with the integer-NCO carrier and return the de-rotated samples (one per input sample). Internally the loop runs a non-data-aided M-th-power discriminator on an I/Q arm integrate-and-dump at n dumps per symbol and steers the NCO, so it acquires the carrier with no symbol timing and no data present (it strips the M-PSK modulation by raising the arm sample to the Mth power). It locks to one of m phases (M-fold ambiguity), resolved downstream. Read norm_freq for the tracked carrier and lock for the carrier lock metric.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
Output. |
Dll
¶
Create a DLL instance (COPIES code).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
code
|
NDArray[uint8]
|
Spreading code (0/1 chips), one period; copied internally. |
...
|
sps
|
int
|
Samples per chip (default 2). |
2
|
init_chip
|
float
|
Seed code phase, chips (default 0.0). |
0.0
|
bn
|
float
|
Loop noise bandwidth (default 0.01). |
0.01
|
zeta
|
float
|
Damping factor (default 0.707). |
0.707
|
spacing
|
float
|
Early/late tap offset, chips (default 0.5). |
0.5
|
segments
|
int
|
Partial correlations per code epoch (default 1). 1 = a coherent full-epoch integrate-and-dump (one prompt/period). >1 splits each epoch into that many sub-epoch partials: it emits that many partial prompts/period and tracks the code non-coherently across them (robust to an asynchronous data-symbol clock). segments/epoch ~ samples/symbol at a downstream SymbolSync when the symbol rate is near the code rate, so choose >= 2 for symbol-timing recovery. |
1
|
locked
property
¶
True when the code-lock detector's statistic exceeds its CFAR threshold (latched at each n_looks-look decision; see configure_lock).
lock_stat
property
¶
Last code-lock test statistic R = sqrt(2*sum|P|^2 / E|O|^2); compare against det_threshold_noncoherent(pfa, n_looks).
noise_est
property
¶
Current CFAR noise-power estimate E|O|^2 from the off-peak (noise) tap EMA.
steps
¶
Correlate a cf32 block against the local code with early/prompt/late taps and steer the code NCO each code period on the non-coherent (sum|E|-sum|L|)/(sum|E|+sum|L|) discriminator. With segments=1 (default) this is a coherent full-epoch integrate-and-dump: one prompt symbol per period. With segments>1 each epoch is split into that many sub-epoch partial correlations: it emits that many partial prompts per period (a stream at ~segments samples/symbol when the symbol rate is near the code rate) and tracks the code non-coherently across the partials, which a data flip cannot collapse (robust to an asynchronous data-symbol clock). segments>1 is the streaming despreader: it removes the PN code and outputs samples. The non-coherent loop is carrier-blind, so it tracks with a residual carrier still on the input; carrier recovery (Costas) and symbol-timing recovery (SymbolSync) are downstream stages fed from the partial output. The output is an independent array per call (block-size invariant).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
Output. |
configure
¶
Recompute the loop gains for a new (bn, zeta); preserves the code phase/rate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bn
|
float
|
Input. |
required |
zeta
|
float
|
Input. |
required |
configure_lock
¶
Tune the always-on code-lock detector to a target (pfa, n_looks). The detector reuses acquisition's non-coherent statistic R = sqrt(2*sum|P|^2 / E|O|^2), where the prompt powers of n_looks consecutive looks are summed and E|O|^2 is an EMA of a random off-peak (noise) correlation re-drawn each epoch; it declares lock when R exceeds det_threshold_noncoherent(pfa, n_looks). Size n_looks with detection.det_n_noncoh(snr, ...) for your operating C/N0. The default is pfa=1e-3 over 20 looks. Read the result from the locked / lock_stat / noise_est properties.
The DLL carries a lock detector that reuses acquisition's non-coherent
test statistic. Every emitted look (a partial in segments mode, or the
full-epoch prompt when segments == 1) is also correlated at a random
off-peak code phase — re-drawn each epoch and kept noise_guard chips
clear of the prompt/early/late lobe — to give a signal-free CFAR noise
sample (valid for a low-sidelobe code, e.g. Gold). The offset power
feeds an EMA reference E|O|^2; the prompt powers of n_looks
consecutive looks are summed into S = sum|P_k|^2, and the detector
declares lock when
R = sqrt(2 * S / E|O|^2) > threshold
which under H0 has P(R > threshold) = marcum_q(n_looks, 0, threshold)
— so a caller sizes threshold = det_threshold_noncoherent(pfa, n_looks)
and n_looks = det_n_noncoh(snr, ...) to meet a target (Pfa, Pd). The
threshold is passed in (not derived) so the core stays dependency-free;
the Python binding converts a pfa via the detection module. The EMA
must average many more cells than the test integrates (1/alpha >>
n_looks) or the noise estimate's own variance inflates Pfa; the binding
defaults 1/alpha to max(1024, 32*n_looks).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pfa
|
float
|
Input. |
required |
n_looks
|
int
|
Non-coherent integration depth N (looks); clamped to >= 1. |
required |
Channel
¶
Create a tracking channel (COPIES code).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
code
|
NDArray[uint8]
|
code constructor parameter. |
...
|
sps
|
int
|
sps constructor parameter. |
4
|
init_norm_freq
|
float
|
init_norm_freq constructor parameter. |
0.0
|
init_chip
|
float
|
init_chip constructor parameter. |
0.0
|
bn_carrier
|
float
|
bn_carrier constructor parameter. |
0.05
|
bn_code
|
float
|
bn_code constructor parameter. |
0.005
|
bn_fll
|
float
|
bn_fll constructor parameter. |
0.0
|
zeta
|
float
|
zeta constructor parameter. |
0.707
|
spacing
|
float
|
spacing constructor parameter. |
0.5
|
nav_period
|
int
|
nav_period constructor parameter. |
1
|
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.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
Output. |
bits
¶
Same tracking kernel as steps(), but bit-sync the per-period prompts into hard data bits: nav_period prompts are coherently summed across each detected bit boundary and one 0/1 bit is emitted per data bit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[uint8]
|
Output. |
SymbolSync
¶
SymbolSync component.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sps
|
int
|
sps constructor parameter. |
4
|
bn
|
float
|
bn constructor parameter. |
0.01
|
zeta
|
float
|
zeta constructor parameter. |
0.707
|
order
|
Literal['linear', 'parabolic', 'cubic']
|
order constructor parameter. |
"cubic"
|
Examples:
Create with defaults:
>>> from doppler.track import SymbolSync
>>> obj = SymbolSync(sps=4, bn=0.01, zeta=0.707, order="cubic")
steps
¶
Recover symbol timing from an oversampled cf32 baseband block: a Gardner timing-error detector drives an integer timing NCO whose post-wrap value gives the interpolation fraction for free, and a Farrow interpolator emits one symbol-rate sample per recovered symbol instant.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
Output. |
configure
¶
Recompute the loop gains for a new (bn, zeta); preserve the timing estimate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bn
|
float
|
Input. |
required |
zeta
|
float
|
Input. |
required |