Finite Hankel SISO model reduction¶
Tutorial goal
Build a finite Hankel matrix from a stable IIR impulse response and construct lower-order rational models with the C++ backend.
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 the first executable bridge between the model-reduction theory page and the package API. It is deliberately finite-dimensional: a truncated Hankel matrix is built from the impulse response, its singular values are inspected, and a lower-order Ho–Kalman realization is recovered from the leading Hankel factors.
Exact AAK/Nehari theory is an infinite-dimensional optimal rational-approximation theory. The implementation here is a practical finite-section reducer inspired by the same Hankel-operator viewpoint, but it does not claim exact Nehari or AAK optimality.
Key idea and equations¶
Given an impulse response h[n], the finite Hankel matrix is
Its singular values measure input-output memory. A reduced order r keeps the leading
singular directions and forms a balanced finite realization
How to read the result¶
Look for Hankel singular-value decay, retained Hankel energy, impulse-response error, and whether the reduced denominator remains stable.
Run command¶
python examples/finite_hankel_model_reduction.py
Run status¶
Return code: 0
Captured stdout¶
full order: 8
finite Hankel matrix: 48 x 48
leading Hankel singular values: [7.372965, 0.174286, 0.135451, 0.113361, 0.015519, 0.01479, 0.009059, 0.002232]
order=2: stable=True, retained_energy=0.999417, rel_impulse_error=4.282e-03, max_mag_error=1.933 dB
order=4: stable=True, retained_energy=0.999990, rel_impulse_error=3.880e-05, max_mag_error=0.175 dB
order=6: stable=True, retained_energy=0.999998, rel_impulse_error=1.834e-05, max_mag_error=0.129 dB
order=8: stable=True, retained_energy=1.000000, rel_impulse_error=1.115e-23, max_mag_error=0.000 dB
Figures¶
finite_hankel_reduced_responses.png¶
finite_hankel_singular_values.png¶
Generated data files¶
Source code¶
1"""Tutorial: finite Hankel model reduction for a stable SISO IIR.
2
3This example is a practical bridge between the theory page and executable code.
4It builds a stable high-order lattice IIR, forms a finite Hankel matrix from its
5impulse response, inspects the Hankel singular values, and constructs lower-order
6finite-Hankel reduced models with the C++ backend.
7
8The implementation is intentionally labeled "finite-Hankel/Ho-Kalman": it uses a
9truncated Hankel matrix and a Ho-Kalman realization, so it is a useful numerical
10approximation and diagnostic rather than a claim of exact infinite-dimensional
11AAK or Nehari optimality.
12"""
13
14from __future__ import annotations
15
16import csv
17import os
18from pathlib import Path
19
20import numpy as np
21
22import lattice_dsp as ld
23
24
25def artifact_dir() -> Path:
26 path = Path(os.environ.get("LATTICE_DSP_ARTIFACT_DIR", "reports/example-artifacts"))
27 path.mkdir(parents=True, exist_ok=True)
28 return path
29
30
31def freq_response(
32 denominator: np.ndarray, numerator: np.ndarray, n_fft: int = 1024
33) -> tuple[np.ndarray, np.ndarray]:
34 freq = np.linspace(0.0, 0.5, n_fft // 2 + 1)
35 z = np.exp(-2j * np.pi * freq)
36 num = np.zeros_like(z, dtype=complex)
37 den = np.zeros_like(z, dtype=complex)
38 for i, coef in enumerate(numerator):
39 num += float(coef) * z**i
40 for i, coef in enumerate(denominator):
41 den += float(coef) * z**i
42 mag_db = 20.0 * np.log10(np.maximum(np.abs(num / den), 1e-12))
43 return freq, mag_db
44
45
46def write_summary(path: Path, rows: list[dict[str, float | int | str | bool]]) -> None:
47 with path.open("w", newline="", encoding="utf-8") as f:
48 fieldnames = [
49 "order",
50 "stable",
51 "retained_hankel_energy",
52 "relative_impulse_error",
53 "max_magnitude_error_db",
54 ]
55 writer = csv.DictWriter(f, fieldnames=fieldnames)
56 writer.writeheader()
57 writer.writerows(rows)
58
59
60def main() -> None:
61 out_dir = artifact_dir()
62
63 # A stable eighth-order all-pole denominator. In a lattice representation,
64 # stability is controlled by |k_i| < 1.
65 reflection = np.array([0.62, -0.48, 0.36, -0.28, 0.20, -0.14, 0.09, -0.05], dtype=float)
66 numerator = np.array([1.0, -0.22, 0.15, 0.08, -0.05, 0.03, 0.0, 0.0, 0.0], dtype=float)
67 denominator = np.asarray(ld.reflection_to_denominator(reflection), dtype=float)
68
69 n_impulse = 360
70 rows = cols = 48
71 impulse = np.asarray(ld.iir_impulse_response(denominator, numerator, n_impulse), dtype=float)
72 hsv = np.asarray(ld.hankel_singular_values(impulse, rows, cols), dtype=float)
73
74 freq, full_mag = freq_response(denominator, numerator)
75
76 orders = [2, 4, 6, 8]
77 summary: list[dict[str, float | int | str | bool]] = []
78 reduced_curves: dict[int, np.ndarray] = {}
79
80 for order in orders:
81 result = ld.finite_hankel_reduce_iir(
82 reflection.tolist(),
83 numerator.tolist(),
84 reduced_order=order,
85 n_impulse=n_impulse,
86 rows=rows,
87 cols=cols,
88 )
89 red_den = np.asarray(result["denominator"], dtype=float)
90 red_num = np.asarray(result["numerator"], dtype=float)
91 _, red_mag = freq_response(red_den, red_num)
92 reduced_curves[order] = red_mag
93 max_mag_err = float(np.max(np.abs(full_mag - red_mag)))
94 summary.append(
95 {
96 "order": order,
97 "stable": bool(result["stable"]),
98 "retained_hankel_energy": float(result["retained_hankel_energy"]),
99 "relative_impulse_error": float(result["relative_impulse_error"]),
100 "max_magnitude_error_db": max_mag_err,
101 }
102 )
103
104 csv_path = out_dir / "finite_hankel_model_reduction_summary.csv"
105 write_summary(csv_path, summary)
106
107 print("full order:", len(reflection))
108 print("finite Hankel matrix:", f"{rows} x {cols}")
109 print("leading Hankel singular values:", [round(float(v), 6) for v in hsv[:8]])
110 for row in summary:
111 print(
112 "order={order}: stable={stable}, retained_energy={retained_hankel_energy:.6f}, "
113 "rel_impulse_error={relative_impulse_error:.3e}, max_mag_error={max_magnitude_error_db:.3f} dB".format(
114 **row
115 )
116 )
117 print(f"wrote {csv_path}")
118
119 try:
120 import matplotlib.pyplot as plt
121 except Exception:
122 print("matplotlib is not installed; skipped figures")
123 return
124
125 fig, ax = plt.subplots(figsize=(8.5, 4.6))
126 idx = np.arange(1, min(24, hsv.size) + 1)
127 ax.semilogy(idx, hsv[: idx.size], marker="o")
128 ax.set_title("Finite Hankel singular-value decay")
129 ax.set_xlabel("index")
130 ax.set_ylabel("singular value")
131 ax.grid(True, alpha=0.3)
132 fig.tight_layout()
133 fig_path = out_dir / "finite_hankel_singular_values.png"
134 fig.savefig(fig_path, dpi=160)
135 print(f"wrote {fig_path}")
136
137 fig2, ax2 = plt.subplots(figsize=(9, 4.8))
138 ax2.plot(freq, full_mag, linewidth=2.0, label="full order 8")
139 for order in orders:
140 ax2.plot(freq, reduced_curves[order], label=f"reduced order {order}")
141 ax2.set_title("Full IIR response versus finite-Hankel reduced models")
142 ax2.set_xlabel("frequency (cycles/sample)")
143 ax2.set_ylabel("magnitude (dB)")
144 ax2.set_xlim(0.0, 0.5)
145 ax2.grid(True, alpha=0.3)
146 ax2.legend()
147 fig2.tight_layout()
148 fig2_path = out_dir / "finite_hankel_reduced_responses.png"
149 fig2.savefig(fig2_path, dpi=160)
150 print(f"wrote {fig2_path}")
151
152
153if __name__ == "__main__":
154 main()