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

\[A(z)=I+\sum_{k=1}^{p}A_k z^{-k}, \qquad H_{AR}(z)=A(z)^{-1},\]

whose stability is checked through the companion matrix \(C_A\):

\[\rho(C_A) < 1.\]

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

\[G(e^{j\omega})^H G(e^{j\omega}) \approx I.\]

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

Run status

Return code: 0

Captured stdout

channels: 3
block Levinson order: 2
block Levinson/direct coefficient difference: 1.614e-16
block Levinson reflection norms: [0.42297  0.129587]
matrix all-pass lattice order: 3
matrix all-pass max reflection norm: 0.751394
matrix all-pass unitarity error: 9.488e-15
takeaway: block Levinson validates MIMO AR prediction; matrix lattice covers unitary MIMO filtering

Figures

mimo lattice vs block reflection norms

mimo_lattice_vs_block_reflection_norms.png

mimo lattice vs block singular values

mimo_lattice_vs_block_singular_values.png

mimo lattice vs block unitarity error

mimo_lattice_vs_block_unitarity_error.png

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()