Stable adaptive IIR system identification

Tutorial goal

Identify a synthetic recursive system while keeping the learned denominator stable.

Note

New to the terminology? See the lattice DSP concept map and the causality/data-use guide for how online, offline, block, and MIMO examples should be read.

Context

This is a two-signal system-identification example, not one-step self-prediction. A known stable target system receives the reference input x[n] and generates the desired signal d[n]. The adaptive lattice-ladder model receives the same reference input and learns a stable recursive approximation to the target input-output map.

Key idea and equations

The data relationship is

\[d[n] = H_{\mathrm{target}}(q^{-1})x[n], \qquad \widehat d[n] = H_{\theta_n}(q^{-1})x[n].\]

The instantaneous error is

\[e[n] = d[n] - \widehat d[n].\]

The adaptive model updates numerator/ladder parameters and reflection coefficients after forming the current output and error. The reflection update is bounded so the learned denominator remains stable during training. This is causal adaptive filtering because \widehat d[n] uses current/past reference samples and previous filter state, not future desired samples.

Causality and data use

This is inductive/streaming system identification on a synthetic sequence: the target generates d[n] from the same reference x[n] seen by the adaptive filter. It is not a single-signal predictor, and it is not a production echo canceller.

How to read the result

Compare the initial and final MSE. Also inspect the learned reflection coefficients and pole radius if printed.

Run command

python examples/adaptive_iir_system_identification.py

Source code

 1"""Adaptive stable IIR system identification.
 2
 3This example identifies both numerator taps and reflection coefficients. The
 4reflection updates are performed through unconstrained raw variables that are
 5mapped with tanh, so the learned denominator remains stable during adaptation.
 6"""
 7
 8from __future__ import annotations
 9
10import numpy as np
11
12from lattice_dsp import AdaptiveLatticeLadderNLMS, LatticeIIR
13
14rng = np.random.default_rng(42)
15x = rng.normal(size=6000)
16
17true_reflection = [0.45, -0.25]
18true_numerator = [0.4, -0.1, 0.7]
19target = LatticeIIR(true_reflection, true_numerator)
20desired = np.asarray(target.process(x), dtype=float)
21
22adaptive = AdaptiveLatticeLadderNLMS(
23    initial_reflection=[0.0, 0.0],
24    initial_taps=[0.0, 0.0, 0.0],
25    mu_taps=0.08,
26    mu_reflection=0.002,
27    margin=1e-4,
28)
29errors = np.asarray(adaptive.adapt_block(x.tolist(), desired.tolist()), dtype=float)
30
31print("target reflection:", true_reflection)
32print("learned reflection:", np.round(adaptive.reflection, 4).tolist())
33print("target numerator:", true_numerator)
34print("learned numerator:", np.round(adaptive.numerator, 4).tolist())
35print("initial MSE:", float(np.mean(errors[:500] ** 2)))
36print("final MSE:", float(np.mean(errors[-500:] ** 2)))