Documentation
¶
Overview ¶
Package equalizer implements adaptive channel equalizers used to fight simulcast distortion — the inter-symbol interference produced when multiple transmitters cover the same frequency at slightly different arrival delays at the receiver. Premium hardware scanners market this capability as "True I/Q"; with an SDR we always have I/Q, so the win is what we do with it.
Two complementary algorithms ship here:
lms.go Least-Mean-Squares adaptive FIR equalizer. Trained
with reference (or decision-directed) symbols; fast to
converge but needs a known training sequence (or a
slicer it can trust).
cma.go Constant Modulus Algorithm — blind equalizer for
constant-envelope modulations (PSK family). Drives the
output toward a constant magnitude without ever needing
a reference; useful when the upstream demod has no
preamble to lock to.
The package operates on complex64 IQ samples / symbols (matching the rest of the DSP stack). Equalizers slot between the channelizer and the symbol-time-recovery / demodulator stages of a per-call chain — the demod-pipeline composer is the natural integration point once a protocol decoder needs them on a real signal.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type CMA ¶
type CMA struct {
// contains filtered or unexported fields
}
CMA is the Constant Modulus Algorithm — a blind adaptive equaliser that requires no training sequence. It exploits the fact that PSK-family signals (BPSK / QPSK / π/4-DQPSK / 8PSK) have a constant modulus on the air; multipath / simulcast distortion blurs that constant-magnitude property, and CMA drives the output back toward it.
Cost function and gradient (Godard / CMA-2):
J = E[(|y|^2 - R^2)^2] ∂J/∂w* ∝ (|y|^2 - R^2) · y · conj(x) w[n+1] = w[n] - μ · (|y|^2 - R^2) · y · conj(x)
Pick R^2 so the equilibrium weight scaling matches the expected constellation. For unit-magnitude PSK use R^2 = 1; QPSK with Gray-coded ±1±j has |y|^2 = 2 so R^2 = 2 is conventional.
Caveats:
- CMA is phase-blind. After convergence the constellation may sit at any rotation; downstream symbol mapping must apply a constellation-aware phase recovery (a per-constellation rotator using known training symbols, or differential decoding).
- On non-constant-modulus signals (FM, OQPSK, QAM) CMA's cost function isn't zero at the right answer; use LMS in decision-directed mode there.
func NewCMA ¶
NewCMA constructs a blind equaliser. `target` is the desired squared modulus (R^2): use 1.0 for unit-modulus PSK, 2.0 for ±1±j QPSK.
func (*CMA) Process ¶
Process consumes one input sample and returns the equalised output. The error proxy (|y|^2 - R^2) is also returned for diagnostics / convergence-monitoring; once it settles near zero the equaliser has opened the constellation.
type FSE ¶ added in v0.3.3
type FSE struct {
// contains filtered or unexported fields
}
FSE is a T/2 fractionally-spaced blind (CMA) adaptive equalizer. It consumes two input samples per symbol — the half-symbol-earlier midpoint interpolant and the on-time symbol sample produced by the timing loop — and emits one equalized symbol.
Why fractionally spaced: a symbol-spaced equalizer can only invert the channel at the symbol rate, so it cannot repair a sub-symbol pulse-shape mismatch. The P25 CQPSK/LSM receiver matched-filters with an RRC, but a real P25 C4FM transmitter shapes a different (CPM) pulse, so the outer ±1800 Hz rails arrive under-shot and the constellation closes — the symbol-spaced CMA cannot open it (issue #492). A T/2 equalizer spans two phases per symbol and therefore synthesizes the receive matched filter implicitly, correcting the pulse mismatch (and any multipath ISI) and is insensitive to residual symbol timing phase.
Like CMA it is blind: the constant-modulus cost J = E[(|y|²−R²)²] needs no training symbols and is rotation-invariant, so the FSE runs ahead of the carrier (Costas) loop. The fractional spacing enlarges the cost's null space (degrees of freedom that leave |y| unchanged), which lets the taps random- walk on a clean, ISI-free channel; a small leakage term w←(1−leak)·w shrinks unexcited taps toward zero so real ISI sustains the taps but noise alone does not. The on-time centre tap is phase-pinned to the positive real axis after each update (as in CMA) so the downstream carrier loop does not read tap-phase drift as a frequency offset.
func NewFSE ¶ added in v0.3.3
NewFSE builds a T/2 fractionally-spaced CMA equalizer. symbolSpan is the equalizer length in symbols; the filter holds 2*symbolSpan T/2-spaced taps. Use an even symbolSpan so the centre tap aligns with an on-time sample (the emission order is [mid, on], so even tap indices are on-time samples). stepSize is the CMA step; target is R² (1.0 for unit-modulus π/4-DQPSK after AGC); leak is the leakage coefficient (0 disables it; ~1e-4..1e-3 typical).
func (*FSE) Process ¶ added in v0.3.3
Process consumes the two T/2-spaced samples of one symbol period (mid = the half-symbol-earlier interpolant, on = the symbol-time sample) and returns the equalized symbol y plus the CMA error proxy |y|²−R². The output is computed once, at the symbol instant, over the full 2-sps tap line; the gradient updates every tap against its own aligned T/2 input sample so both new samples participate.
type LMS ¶
type LMS struct {
// contains filtered or unexported fields
}
LMS is a complex-valued tapped-delay-line adaptive equalizer trained with the standard Least-Mean-Squares update rule:
y[n] = sum_k w_k(n) * x[n-k] // FIR output e[n] = d[n] - y[n] // training error w[n+1] = w[n] + μ · e[n] · conj(x[n-k]) // weight update
Notes:
- The reference signal d[n] can be a true training preamble or, in decision-directed mode, the slicer's hard decision on y[n].
- μ (StepSize) sets the trade-off between convergence speed and mean-squared-error floor; 0.005 to 0.05 are reasonable starting points for symbol-spaced channels.
- Initialised to a centre spike: w_{N/2}(0) = 1, others zero. That starts the equaliser as a pass-through so a benign channel stays roughly intact while training begins.
The struct is not safe for concurrent use; one equaliser belongs to one demod chain.
func NewLMS ¶
NewLMS constructs an equaliser with `taps` complex weights and the supplied step size. taps must be > 0; an odd taps count is recommended so the centre spike is well-defined.
func (*LMS) Process ¶
Process consumes one input sample x and updates the filter. The `desired` argument is the reference / training symbol; in decision-directed mode supply the upstream slicer's hard decision on the previous output. Returns the equalised output y[n] and the instantaneous error e[n].
func (*LMS) Reset ¶
func (e *LMS) Reset()
Reset returns the equaliser to its centre-spike initial state.
func (*LMS) SetStepSize ¶
SetStepSize updates μ. Larger steps converge faster but settle to a noisier weight vector.