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

\[\sigma_{r+1}.\]

It also checks the finite Schmidt-pair equations

\[H v_{r+1}=\sigma_{r+1}u_{r+1},\qquad H^T u_{r+1}=\sigma_{r+1}v_{r+1}.\]

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

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