MIMO lattice response versus block Levinson AR¶
Tutorial goal
Compare two matrix-valued views: all-pass lattice filtering and vector AR estimation.
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 tutorial puts MIMO/matrix lattice objects next to multichannel AR estimation. They are different models, but both rely on matrix-valued stability or reflection ideas.
Key idea and equations¶
The block-Levinson side fits a predictive VAR model
whose stability is checked through the companion matrix \(C_A\):
The matrix-lattice side uses reflection matrices \(K_i\) with \(\lVert K_i\rVert_2<1\) to build a frequency response \(G(z)\). In this all-pass diagnostic example, the desired property is
The point of the example is not to claim that the two constructions are the same model. It places their diagnostics side by side: VAR prediction error and companion stability versus lattice reflection norms and frequency-wise unitarity.
Causality and data use¶
The block-Levinson side is batch coefficient estimation from covariance data. The matrix-lattice side evaluates a response on a frequency grid. This page compares diagnostics; it is not a sample-by-sample runtime filter.
What this example verifies¶
This is a side-by-side diagnostic, not an equivalence proof. It verifies the VAR side with block-Levinson prediction diagnostics and the matrix-lattice side with contractive reflection norms and frequency-wise unitarity.
How to read the result¶
Use the reflection-norm and singular-value figures to separate the two diagnostics: block Levinson validates VAR prediction, while the matrix lattice validates frequency-wise unitarity.
Run command¶
python examples/mimo_lattice_vs_block_levinson.py
Source code¶
1"""Side-by-side MIMO lattice and block Levinson demonstrations.
2
3The two algorithms are complementary rather than interchangeable:
4
5* block Levinson estimates vector AR predictors from block Toeplitz covariance;
6* matrix lattice all-pass filters represent unitary/paraunitary MIMO responses.
7
8Both expose matrix reflection-style parameters, which is why they belong in the
9same package family.
10"""
11
12from __future__ import annotations
13
14import os
15from pathlib import Path
16
17import numpy as np
18
19import lattice_dsp as ld
20
21
22def _artifact_dir() -> Path:
23 path = Path(os.environ.get("LATTICE_DSP_ARTIFACT_DIR", "reports/example-artifacts"))
24 path.mkdir(parents=True, exist_ok=True)
25 return path
26
27
28def simulate_var(coefficients: list[np.ndarray], samples: int, seed: int = 0) -> np.ndarray:
29 rng = np.random.default_rng(seed)
30 order = len(coefficients)
31 channels = coefficients[0].shape[0]
32 x = np.zeros((samples + 256, channels))
33 noise = rng.normal(size=x.shape)
34 for n in range(order, x.shape[0]):
35 y = noise[n].copy()
36 for lag, a_lag in enumerate(coefficients, start=1):
37 y -= a_lag @ x[n - lag]
38 x[n] = y
39 return x[256:]
40
41
42def _save_figures(
43 *,
44 w: np.ndarray,
45 h: np.ndarray,
46 levinson: ld.MultichannelARResult,
47 lattice_reflection_norms: np.ndarray,
48) -> None:
49 try:
50 import matplotlib.pyplot as plt
51 except ImportError: # pragma: no cover - optional plotting dependency
52 print("matplotlib is not installed; skipped figures")
53 return
54
55 out_dir = _artifact_dir()
56 eye = np.eye(h.shape[1])
57 unitarity_error = np.array([np.linalg.norm(hi.conj().T @ hi - eye) for hi in h])
58 singular_values = np.linalg.svd(h, compute_uv=False)
59
60 fig, axes = plt.subplots(1, 2, figsize=(9.0, 3.8))
61 axes[0].plot(
62 np.arange(1, len(levinson.reflection_spectral_norms) + 1),
63 levinson.reflection_spectral_norms,
64 marker="o",
65 )
66 axes[0].axhline(1.0, linestyle="--", linewidth=1.0)
67 axes[0].set_title("block Levinson AR")
68 axes[0].set_xlabel("stage")
69 axes[0].set_ylabel("reflection norm")
70 axes[1].plot(
71 np.arange(1, len(lattice_reflection_norms) + 1), lattice_reflection_norms, marker="o"
72 )
73 axes[1].axhline(1.0, linestyle="--", linewidth=1.0)
74 axes[1].set_title("matrix all-pass lattice")
75 axes[1].set_xlabel("stage")
76 axes[1].set_ylabel("reflection norm")
77 fig.suptitle("Two matrix reflection diagnostics, two different models")
78 fig.tight_layout()
79 path = out_dir / "mimo_lattice_vs_block_reflection_norms.png"
80 fig.savefig(path, dpi=160)
81 plt.close(fig)
82 print(f"wrote {path}")
83
84 fig, ax = plt.subplots(figsize=(7.2, 4.0))
85 for idx in range(singular_values.shape[1]):
86 ax.plot(w, singular_values[:, idx], label=f"σ{idx + 1}")
87 ax.set_xlabel("rad/sample")
88 ax.set_ylabel("singular value")
89 ax.set_title("Matrix-lattice response stays unitary across frequency")
90 ax.legend(loc="best")
91 fig.tight_layout()
92 path = out_dir / "mimo_lattice_vs_block_singular_values.png"
93 fig.savefig(path, dpi=160)
94 plt.close(fig)
95 print(f"wrote {path}")
96
97 fig, ax = plt.subplots(figsize=(7.0, 3.8))
98 ax.semilogy(w, np.maximum(unitarity_error, 1e-18))
99 ax.set_xlabel("rad/sample")
100 ax.set_ylabel("||HᴴH - I||₂")
101 ax.set_title("All-pass unitarity residual")
102 fig.tight_layout()
103 path = out_dir / "mimo_lattice_vs_block_unitarity_error.png"
104 fig.savefig(path, dpi=160)
105 plt.close(fig)
106 print(f"wrote {path}")
107
108
109def main() -> None:
110 # Classical MIMO AR / block Levinson side.
111 ar_coefficients = [
112 np.array([[0.36, 0.09, 0.02], [-0.04, 0.31, 0.08], [0.03, -0.06, 0.25]]),
113 np.array([[-0.12, 0.02, 0.00], [0.01, -0.10, -0.02], [0.02, 0.03, -0.08]]),
114 ]
115 x = simulate_var(ar_coefficients, samples=30000, seed=11)
116 r = ld.multichannel_autocorrelation(x, order=2)
117 direct = ld.solve_block_yule_walker_direct(r, order=2)
118 levinson = ld.block_levinson_durbin(r, order=2)
119 block_diff = np.linalg.norm(direct.coefficients - levinson.coefficients)
120
121 # Matrix all-pass lattice side.
122 rng = np.random.default_rng(12)
123 channels = 3
124 order = 3
125 reflections = []
126 for _ in range(order):
127 raw = 0.25 * (
128 rng.standard_normal((channels, channels))
129 + 1j * rng.standard_normal((channels, channels))
130 )
131 reflections.append(ld.contractive_matrix_from_raw(raw))
132 residue = ld.unitary_polar_factor(
133 rng.standard_normal((channels, channels)) + 1j * rng.standard_normal((channels, channels))
134 )
135 filt = ld.MatrixLatticeAllPass(reflections, residue)
136 w = np.linspace(0, np.pi, 256)
137 h = filt.frequency_response(w)
138 eye = np.eye(channels)
139 unitarity_error = max(np.linalg.norm(hi.conj().T @ hi - eye) for hi in h)
140 lattice_reflection_norms = np.array(
141 [np.linalg.svd(k, compute_uv=False)[0] for k in reflections]
142 )
143
144 print("channels:", channels)
145 print("block Levinson order:", levinson.order)
146 print("block Levinson/direct coefficient difference:", f"{block_diff:.3e}")
147 print("block Levinson reflection norms:", np.round(levinson.reflection_spectral_norms, 6))
148 print("matrix all-pass lattice order:", order)
149 print("matrix all-pass max reflection norm:", f"{lattice_reflection_norms.max():.6f}")
150 print("matrix all-pass unitarity error:", f"{unitarity_error:.3e}")
151 print(
152 "takeaway: block Levinson validates MIMO AR prediction; matrix lattice covers unitary MIMO filtering"
153 )
154
155 _save_figures(w=w, h=h, levinson=levinson, lattice_reflection_norms=lattice_reflection_norms)
156
157
158if __name__ == "__main__":
159 main()