SymbolSync Timing Lock Detector¶
track.SymbolSync (Gardner/DTTL symbol-timing recovery) had no lock concept
at all until this design landed — the last stage before bit decisions in a
tracking chain, with zero trust signal. The statistic and sizing formula
below were supplied by a doppler user from their own operational experience,
not derived from a paper this codebase carries; this page records the
formula as given, what it means, and the empirical validation that confirms
it actually hits its claimed operating point.
The statistic¶
lock_signal = 2 * (
(symbol.real**2 - mid_symbol.real**2)
+ (symbol.imag**2 - mid_symbol.imag**2)
) / (
(symbol.real**2 + mid_symbol.real**2)
+ (symbol.imag**2 + mid_symbol.imag**2)
)
A Gardner-style eye-opening ratio: symbol is the on-time interpolant (the
Farrow-evaluated sample at the recovered symbol strobe) and mid_symbol is
the mid-symbol (transition-gate) interpolant already formed by the Gardner
TED. At correct timing, a Nyquist (ISI-free) pulse's on-time sample depends
only on the current symbol, so it sits at the eye's peak while the
mid-symbol sample sits closer to a zero crossing — lock_signal is positive
and grows with pulse rolloff and Es/N0. Under noise or wrong timing it
hovers near zero.
An earlier design attempt in this codebase used the algebraically
equivalent ratio q = symbol_pwr / (symbol_pwr + mid_pwr) (lock_signal = 4q - 2) and appeared to invert sign between raised-cosine and rectangular
pulses under a first Monte Carlo pass — that turned out to be a test-harness
bug (a signal generator silently truncating the last ~30 symbols of every
"clean" trial, not a real property of the statistic). Once fixed, both
pulse shapes gave the same, correctly-signed result.
Sizing: (pfa, pd) → (avgs, threshold)¶
mean_lock_detect = (0.6 * rolloff + 0.26) * (
1 - exp(-0.275 * 10 ** (esno_min / 10))
)
avgs = 2 * var * ((erfcinv(2 * pfa) - erfcinv(2 * pd)) / mean_lock_detect) ** 2
threshold = erfcinv(2 * pfa) * mean_lock_detect / (
erfcinv(2 * pfa) - erfcinv(2 * pd)
)
lock_signal is non-coherently block-averaged over avgs looks before each
decision (a tumbling window, mirroring Dll's CFAR pattern — a sliding
window would break the verify-count independence assumption the same way it
would for the DLL). mean_lock_detect estimates the per-look mean from the
matched-filter rolloff and the minimum operating Es/N0; the classic Gaussian
test-statistic sizing (N = variance * ((Q⁻¹(pfa) - Q⁻¹(pd)) / mean)²,
threshold = Q⁻¹(pfa) * mean / (Q⁻¹(pfa) - Q⁻¹(pd))) gives avgs and the
declare threshold. erfcinv is used directly (not the √2·erfcinv(2p) = Q⁻¹(p) conversion a Gaussian Q-function derivation would normally use) —
implemented literally as given, with one correction: the source formula's
avgs used a bare 8 in var's place, an uncalibrated placeholder rather
than a derived constant. var is now SYMSYNC_LOCK_STAT_VARIANCE, the real
per-look variance of lock_signal under noise-only input, measured directly
(5,000,000-sample Monte Carlo: mean ≈0, variance ≈1.343) rather than assumed.
The leading 2 is not part of that variance — because erfcinv is used
directly instead of Q⁻¹, the √2 factors cancel in threshold (identical
either way) but not in avgs, which needs an explicit factor of 2 to match
the classic N = variance * ((Q⁻¹(pfa) - Q⁻¹(pd)) / mean)² derivation once
rewritten in terms of the erfcinv-based denominator. Both hypotheses were
tried empirically before picking one (see the validation section below): the
measured variance alone, without the factor of 2, undersizes avgs and
blows past the pfa target by ~13×. No down-threshold or verify-count
derivation is implied by the source formula, so those default to the same
shape Dll.configure_lock uses (see
native/src/symsync/symsync_core.c's
SYMSYNC_LOCK_STAT_VARIANCE comment for the exact defaults and full
derivation).
No standard C library function computes erfcinv; symsync_core.c carries
a private Winitzki-initial-guess-plus-Newton-refinement implementation
(verified to machine precision against erfc), kept local to that file
pending a second consumer.
Empirical validation — does it actually hit (pfa, pd)?¶
Since the formula wasn't derived here, it was validated by direct Monte
Carlo against the real object rather than trusted on faith
(native/validation/symsync_lock.c, gated in CI via ctest -R validate_symsync_lock):
| Check | Method | Nominal target | Measured |
|---|---|---|---|
| False-alarm rate | 500,000 independent noise-only avgs-symbol blocks |
pfa = 1e-3 (~500 expected) | 429 false declares (8.58e-4) |
| Detection probability | 2,000 independent raised-cosine BPSK blocks at exactly the esno_min design SNR |
pd = 0.9 | 1.0000 true declares |
At the default operating point (rolloff=0.35, esno_min=10dB, pfa=1e-3,
pd=0.9) this gives avgs=133, threshold=0.311 (threshold is
var-independent, so unchanged from the original placeholder). Both targets
land correctly sized rather than accidentally oversized: avgs shrank from
395 (under the original bare 8) to 133 — about 3×, not the ~6× a naive
"replace 8 with the measured 1.343" substitution would give. That naive
substitution was tried first and rejected: it undersizes avgs (drops the
implicit factor of 2 from the erfcinv-vs-Q⁻¹ convention) and blows past
the pfa target by ~13× (empirical 1.31e-2 against a 1e-3 nominal) —
confirming the factor-of-2 correction above is required, not optional.
Usage¶
from doppler.track import SymbolSync
ss = SymbolSync(sps=4, bn=0.01, zeta=0.707)
ss.configure_lock(rolloff=0.35, esno_min_db=10.0, pfa=1e-3, pd=0.9)
assert ss.locked is False # no signal yet
# ... ss.steps(x) on a real oversampled baseband block ...
# ss.locked / ss.lock_stat report the verify-counted decision and the
# last block-averaged lock_signal.
# The raw escape hatch for direct control of avgs/thresholds/verify counts:
ss.configure_lock_raw(
avgs=200, up_thresh=0.3, down_thresh=0.25, n_up=1, n_down=8
)
See also: Lock Detection: Verify Counts for the
shared lockdet_core.h primitive every continuous tracking loop in this
codebase uses, and Dll's CFAR lock detector for the
closed-form-derived counterpart this design mirrors in shape.