Finite-section SISO AAK/Nehari certificate¶
Tutorial goal
Use Schmidt-pair identities to certify the finite AAK/Nehari target and attach the rational candidate.
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 the first tutorial whose structure is deliberately AAK/Nehari-shaped. It builds a finite Hankel matrix from an exact rational anticausal tail, computes the first neglected Schmidt pair, checks the singular-vector identities, and attaches the finite Nehari/rational candidate for the same rank.
The tutorial is intentionally finite-section. It is closer to the AAK/Nehari construction than the earlier rank sweeps because it verifies the Schmidt-pair equations directly, but it is still not advertised as a full infinite-dimensional Hardy-space solver.
Key idea and equations¶
For target rank r, the finite certificate reports the first neglected singular value
It also checks the finite Schmidt-pair equations
On an exact rank-three rational tail, the rank-three certificate should recover the stable poles and produce residuals near machine precision.
How to read the result¶
Look for small Schmidt-pair residuals, rank-3 pole recovery, tiny rational error, and poles inside the unit disk.
Run command¶
python examples/aak_siso_certificate_demo.py
Run status¶
Return code: 0
Captured stdout¶
finite Hankel matrix: 48 x 48
target rank: 3
true poles: [-0.42, 0.18, 0.76]
true weights: [1.25, -0.7, 0.4]
leading singular values: [1.28016936, 0.61060229, 0.14951434, 0.0, 0.0, 0.0, 0.0, 0.0]
sigma_next: 1.123664e-16
rank-r SVD error: 5.103015e-16
left Schmidt residual: 1.110e-16
right Schmidt residual: 2.972e-16
candidate tail error: 4.624e-15
candidate rational error: 4.631e-15
candidate pole radius: 0.7600
candidate accepted: True
recovered poles: [-0.42, 0.18, 0.76]
Figures¶
aak_certificate_poles.png¶
aak_certificate_schmidt_pair.png¶
aak_certificate_singular_values.png¶
aak_certificate_tail_recovery.png¶
Generated data files¶
Source code¶
1"""Tutorial: a finite-section SISO AAK/Nehari certificate.
2
3This example is the first deliberately AAK/Nehari-shaped diagnostic in the
4package. It does not merely fit a recurrence. It builds a finite Hankel
5matrix, computes the Schmidt pair associated with the first neglected singular
6value, checks the finite AAK identities, and then attaches the finite
7Nehari/rational candidate for the same rank.
8
9The example uses an exact rank-3 rational tail, so the rank-3 candidate should
10be selected, recover the true poles, and have tiny tail/rational error. This is
11still a finite-section prototype, not a full infinite-dimensional Hardy-space
12AAK/Nehari solver.
13"""
14
15from __future__ import annotations
16
17import csv
18import os
19from pathlib import Path
20
21import numpy as np
22
23import lattice_dsp as ld
24
25
26def artifact_dir() -> Path:
27 path = Path(os.environ.get("LATTICE_DSP_ARTIFACT_DIR", "reports/example-artifacts"))
28 path.mkdir(parents=True, exist_ok=True)
29 return path
30
31
32def exact_rational_tail(n_terms: int) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
33 true_poles = np.array([-0.42, 0.18, 0.76], dtype=float)
34 true_weights = np.array([1.25, -0.7, 0.4], dtype=float)
35 n = np.arange(n_terms, dtype=float)
36 tail = sum(weight * pole**n for weight, pole in zip(true_weights, true_poles, strict=True))
37 return np.asarray(tail, dtype=float), true_poles, true_weights
38
39
40def write_summary(path: Path, rows: list[dict[str, object]]) -> None:
41 with path.open("w", newline="", encoding="utf-8") as f:
42 writer = csv.DictWriter(f, fieldnames=list(rows[0]))
43 writer.writeheader()
44 writer.writerows(rows)
45
46
47def main() -> None:
48 out_dir = artifact_dir()
49 rows = cols = 48
50 rank = 3
51 tail, true_poles, true_weights = exact_rational_tail(rows + cols - 1)
52 criteria = ld.FiniteNehariCandidateCriteria(
53 max_tail_error=1e-8,
54 max_rational_error=1e-8,
55 max_pole_radius=0.99,
56 )
57
58 certificate = ld.finite_aak_siso_certificate(
59 tail, rank=rank, rows=rows, cols=cols, criteria=criteria
60 )
61 candidate = certificate["candidate"]
62 selected_poles = np.asarray(candidate["poles"])
63
64 summary = [
65 {
66 "rank": certificate["rank"],
67 "sigma_next": certificate["sigma_next"],
68 "rank_svd_error": certificate["rank_svd_error"],
69 "schmidt_left_residual": certificate["schmidt_left_residual"],
70 "schmidt_right_residual": certificate["schmidt_right_residual"],
71 "hankelized_tail_error": candidate["hankelized_tail_error"],
72 "rational_error": candidate["rational_error"],
73 "max_pole_radius": candidate["max_pole_radius"],
74 "accepted": candidate["accepted"],
75 }
76 ]
77 summary_path = out_dir / "aak_siso_certificate_summary.csv"
78 write_summary(summary_path, summary)
79
80 print("finite Hankel matrix:", f"{rows} x {cols}")
81 print("target rank:", rank)
82 print("true poles:", np.round(true_poles, 8).tolist())
83 print("true weights:", np.round(true_weights, 8).tolist())
84 print(
85 "leading singular values:", np.round(certificate["hankel_singular_values"][:8], 8).tolist()
86 )
87 print("sigma_next:", f"{certificate['sigma_next']:.6e}")
88 print("rank-r SVD error:", f"{certificate['rank_svd_error']:.6e}")
89 print("left Schmidt residual:", f"{certificate['schmidt_left_residual']:.3e}")
90 print("right Schmidt residual:", f"{certificate['schmidt_right_residual']:.3e}")
91 print("candidate tail error:", f"{candidate['hankelized_tail_error']:.3e}")
92 print("candidate rational error:", f"{candidate['rational_error']:.3e}")
93 print("candidate pole radius:", f"{candidate['max_pole_radius']:.4f}")
94 print("candidate accepted:", candidate["accepted"])
95 print("recovered poles:", np.round(np.sort(np.real(selected_poles)), 8).tolist())
96 print(f"wrote {summary_path}")
97
98 try:
99 import matplotlib.pyplot as plt
100 except Exception:
101 print("matplotlib is not installed; skipped figures")
102 return
103
104 singular_values = np.asarray(certificate["hankel_singular_values"], dtype=float)
105 fig, ax = plt.subplots(figsize=(8.5, 4.8))
106 ax.semilogy(np.arange(1, 13), singular_values[:12], marker="o")
107 ax.axvline(rank + 1, linestyle="--", linewidth=1.0, label="first neglected singular value")
108 ax.set_title("Finite AAK/Nehari certificate: Hankel singular values")
109 ax.set_xlabel("index")
110 ax.set_ylabel("singular value")
111 ax.grid(True, alpha=0.3)
112 ax.legend()
113 fig.tight_layout()
114 path = out_dir / "aak_certificate_singular_values.png"
115 fig.savefig(path, dpi=160)
116 print(f"wrote {path}")
117
118 fig2, ax2 = plt.subplots(figsize=(8.8, 4.8))
119 ax2.plot(certificate["left_schmidt_vector"], marker="o", label="left Schmidt vector")
120 ax2.plot(certificate["right_schmidt_vector"], marker="s", label="right Schmidt vector")
121 ax2.set_title("Critical finite Schmidt pair")
122 ax2.set_xlabel("coefficient index")
123 ax2.set_ylabel("amplitude")
124 ax2.grid(True, alpha=0.3)
125 ax2.legend()
126 fig2.tight_layout()
127 path2 = out_dir / "aak_certificate_schmidt_pair.png"
128 fig2.savefig(path2, dpi=160)
129 print(f"wrote {path2}")
130
131 rational_tail = np.asarray(candidate["rational_tail"], dtype=float)
132 fig3, ax3 = plt.subplots(figsize=(9.2, 4.8))
133 n = np.arange(tail.size)
134 ax3.plot(n, tail, label="true tail", linewidth=2.0)
135 ax3.plot(n, rational_tail, "--", label="rank-3 rational candidate")
136 ax3.set_title("Exact rank-3 rational tail recovery")
137 ax3.set_xlabel("tail index")
138 ax3.set_ylabel("coefficient")
139 ax3.grid(True, alpha=0.3)
140 ax3.legend()
141 fig3.tight_layout()
142 path3 = out_dir / "aak_certificate_tail_recovery.png"
143 fig3.savefig(path3, dpi=160)
144 print(f"wrote {path3}")
145
146 fig4, ax4 = plt.subplots(figsize=(5.8, 5.8))
147 circle = plt.Circle((0, 0), 1.0, fill=False, linestyle="--")
148 ax4.add_artist(circle)
149 ax4.scatter(np.real(true_poles), np.imag(true_poles), marker="o", label="true poles")
150 ax4.scatter(
151 np.real(selected_poles), np.imag(selected_poles), marker="x", label="recovered poles"
152 )
153 ax4.set_aspect("equal", adjustable="box")
154 ax4.set_xlim(-1.1, 1.1)
155 ax4.set_ylim(-1.1, 1.1)
156 ax4.set_xlabel("real")
157 ax4.set_ylabel("imaginary")
158 ax4.set_title("Stable rational candidate poles")
159 ax4.grid(True, alpha=0.3)
160 ax4.legend()
161 fig4.tight_layout()
162 path4 = out_dir / "aak_certificate_poles.png"
163 fig4.savefig(path4, dpi=160)
164 print(f"wrote {path4}")
165
166
167if __name__ == "__main__":
168 main()