Calibrating matrix-lattice static gain diagnostics

Tutorial goal

Use a known matrix-lattice all-pass response to validate static gain compensation diagnostics.

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 is a calibration case for the experimental realization scaffold. It starts from a known matrix-lattice all-pass response, wraps it in static nonunitary gains, and then fits static left/right gains back around the lattice response. The compensated error should collapse when the only mismatch is static gain.

Key idea and equations

Given a lattice all-pass response G(e^{j\omega}) and a target

\[H(e^{j\omega}) = L\,G(e^{j\omega})\,R,\]

the diagnostic solves a least-squares problem for static matrices L and R. This separates static nonunitary gain mismatch from dynamic lattice/all-pass mismatch.

How to read the result

Look for near-zero all-pass calibration error and a large improvement after fitting static gains around the gain-wrapped lattice response.

Run command

python examples/experimental_mimo_matrix_lattice_calibration.py

Source code

  1"""Tutorial: calibrating matrix-lattice static gain diagnostics.
  2
  3This example uses a known matrix-lattice all-pass response, then wraps it in
  4static nonunitary gains.  The diagnostic fit should recover a near-perfect
  5compensated response.  This is a calibration case for the experimental
  6MIMO state-space to matrix-lattice realization scaffold: it separates true
  7all-pass/lattice mismatch from static gain mismatch.
  8"""
  9
 10from __future__ import annotations
 11
 12import csv
 13import os
 14from pathlib import Path
 15
 16import numpy as np
 17
 18import lattice_dsp as ld
 19
 20
 21def artifact_dir() -> Path:
 22    path = Path(os.environ.get("LATTICE_DSP_ARTIFACT_DIR", "reports/example-artifacts"))
 23    path.mkdir(parents=True, exist_ok=True)
 24    return path
 25
 26
 27def known_lattice(dimension: int = 3, order: int = 4) -> ld.MatrixLatticeAllPass:
 28    rng = np.random.default_rng(421)
 29    reflections = []
 30    for stage in range(order):
 31        raw = rng.normal(size=(dimension, dimension)) + 1j * rng.normal(size=(dimension, dimension))
 32        reflections.append(ld.contractive_matrix_from_raw(0.30 * raw / (1.0 + stage)))
 33    residue_raw = rng.normal(size=(dimension, dimension)) + 1j * rng.normal(
 34        size=(dimension, dimension)
 35    )
 36    residue = ld.unitary_polar_factor(residue_raw)
 37    return ld.MatrixLatticeAllPass(reflections, residue=residue)
 38
 39
 40def main() -> None:
 41    out_dir = artifact_dir()
 42    dimension = 3
 43    order = 4
 44    n_freq = 384
 45    omega = np.linspace(0.0, np.pi, n_freq)
 46
 47    lattice = known_lattice(dimension=dimension, order=order)
 48    response = lattice.frequency_response(omega, n_threads=1)
 49
 50    rng = np.random.default_rng(422)
 51    left_true = np.diag([1.8, 0.75, 0.35]).astype(np.complex128)
 52    right_true = ld.unitary_polar_factor(
 53        rng.normal(size=(dimension, dimension)) + 1j * rng.normal(size=(dimension, dimension))
 54    )
 55    target = np.asarray([left_true @ h @ right_true for h in response], dtype=np.complex128)
 56
 57    allpass_fit = ld.fit_static_matrix_gains(response, response, mode="both", n_iter=6)
 58    gain_fit = ld.fit_static_matrix_gains(target, response, mode="both", n_iter=40)
 59
 60    summary = {
 61        "dimension": dimension,
 62        "order": order,
 63        "known_lattice_unitarity_error": lattice.unitarity_error(omega),
 64        "allpass_raw_error": allpass_fit["raw_relative_error"],
 65        "allpass_compensated_error": allpass_fit["compensated_relative_error"],
 66        "gain_wrapped_raw_error": gain_fit["raw_relative_error"],
 67        "gain_wrapped_compensated_error": gain_fit["compensated_relative_error"],
 68        "gain_improvement": float(gain_fit["raw_relative_error"])
 69        / max(float(gain_fit["compensated_relative_error"]), 1e-30),
 70        "left_gain_condition": gain_fit["left_gain_condition"],
 71        "right_gain_condition": gain_fit["right_gain_condition"],
 72        "max_reflection_singular_value": lattice.max_reflection_singular_value(),
 73    }
 74
 75    csv_path = out_dir / "experimental_mimo_matrix_lattice_calibration_summary.csv"
 76    with csv_path.open("w", newline="", encoding="utf-8") as f:
 77        writer = csv.DictWriter(f, fieldnames=list(summary))
 78        writer.writeheader()
 79        writer.writerow(summary)
 80
 81    print("known lattice dimension:", dimension)
 82    print("known lattice order:", order)
 83    print("known lattice unitarity error:", f"{summary['known_lattice_unitarity_error']:.3e}")
 84    print("max reflection singular value:", f"{summary['max_reflection_singular_value']:.4f}")
 85    print("all-pass calibration raw error:", f"{summary['allpass_raw_error']:.3e}")
 86    print("all-pass calibration compensated error:", f"{summary['allpass_compensated_error']:.3e}")
 87    print("gain-wrapped raw error:", f"{summary['gain_wrapped_raw_error']:.3e}")
 88    print("gain-wrapped compensated error:", f"{summary['gain_wrapped_compensated_error']:.3e}")
 89    print("static-gain improvement:", f"{summary['gain_improvement']:.2e}x")
 90    print(
 91        "left/right fitted gain condition:",
 92        f"{summary['left_gain_condition']:.3f}",
 93        f"{summary['right_gain_condition']:.3f}",
 94    )
 95    print(f"wrote {csv_path}")
 96
 97    try:
 98        import matplotlib.pyplot as plt
 99    except Exception:
100        print("matplotlib is not installed; skipped figures")
101        return
102
103    compensated = np.asarray(gain_fit["compensated_response"], dtype=np.complex128)
104    raw_per_freq = np.linalg.norm(target - response, axis=(1, 2)) / np.maximum(
105        np.linalg.norm(target, axis=(1, 2)), 1e-30
106    )
107    compensated_per_freq = np.linalg.norm(target - compensated, axis=(1, 2)) / np.maximum(
108        np.linalg.norm(target, axis=(1, 2)), 1e-30
109    )
110
111    fig, ax = plt.subplots(figsize=(8.0, 4.5))
112    ax.semilogy(omega, raw_per_freq, label="raw lattice vs gain-wrapped target")
113    ax.semilogy(omega, compensated_per_freq, label="after fitted static gains")
114    ax.set_title("Static gain calibration for matrix-lattice response")
115    ax.set_xlabel("radian frequency")
116    ax.set_ylabel("relative Frobenius error")
117    ax.grid(True, alpha=0.3)
118    ax.legend()
119    fig.tight_layout()
120    fig_path = out_dir / "experimental_mimo_matrix_lattice_calibration_error.png"
121    fig.savefig(fig_path, dpi=160)
122    print(f"wrote {fig_path}")
123
124    sv = np.linalg.svd(np.asarray(gain_fit["left_gain"]), compute_uv=False)
125    fig2, ax2 = plt.subplots(figsize=(7.0, 4.0))
126    ax2.plot(np.arange(1, sv.size + 1), sv, marker="o")
127    ax2.set_title("Fitted left static-gain singular values")
128    ax2.set_xlabel("singular-value index")
129    ax2.set_ylabel("singular value")
130    ax2.grid(True, alpha=0.3)
131    fig2.tight_layout()
132    fig2_path = out_dir / "experimental_mimo_matrix_lattice_calibration_gain.png"
133    fig2.savefig(fig2_path, dpi=160)
134    print(f"wrote {fig2_path}")
135
136
137if __name__ == "__main__":
138    main()