Diagonal tangential Schur equals independent scalar Pick problems

Tutorial goal

Show that diagonal MIMO tangential data decompose into independent scalar Schur/Pick checks.

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 mirrors the diagonal-MIMO-equals-SISO runtime check in the interpolation setting. If tangential directions are coordinate vectors and the Schur function is diagonal, the MIMO Pick matrix splits into independent scalar Pick blocks. This gives a simple sanity check for the matrix-valued tangential machinery.

Key idea and equations

For a diagonal matrix Schur function

\[S(z)=\operatorname{diag}(s_1(z),\ldots,s_c(z)),\]

coordinate tangential data satisfy

\[S(z_i)e_k = s_k(z_i)e_k.\]

If the data are grouped by channel, the full Pick matrix is block diagonal, and each block is the scalar Pick matrix for one \(s_k\).

Causality and data use

This is an offline interpolation diagnostic. It does not filter a time stream; it checks that the MIMO Schur/Pick machinery reduces to independent scalar problems when there is no channel coupling.

What this example verifies

This verifies the reduction-to-scalar sanity check for interpolation data. When tangential directions are coordinate vectors and the Schur function is diagonal, the full MIMO Pick matrix should split into independent scalar Pick blocks with roundoff-level off-block error.

How to read the result

The full MIMO Pick matrix should match the scalar block-diagonal Pick matrix up to roundoff, and the diagonal constant solution should interpolate exactly.

Run command

python examples/diagonal_tangential_schur_equals_scalar.py

Source code

  1"""Diagonal tangential Schur data reduce to independent scalar Pick tests."""
  2
  3from __future__ import annotations
  4
  5import os
  6from pathlib import Path
  7
  8import numpy as np
  9
 10import lattice_dsp as ld
 11
 12
 13def _artifact_dir() -> Path:
 14    path = Path(os.environ.get("LATTICE_DSP_ARTIFACT_DIR", "reports/example-artifacts"))
 15    path.mkdir(parents=True, exist_ok=True)
 16    return path
 17
 18
 19def _save_figures(full_pick: np.ndarray, block_error: float, channel_min_eigs: list[float]) -> None:
 20    try:
 21        import matplotlib.pyplot as plt
 22    except ImportError:  # pragma: no cover - optional plotting dependency
 23        print("matplotlib is not installed; skipped figures")
 24        return
 25
 26    out_dir = _artifact_dir()
 27
 28    fig, ax = plt.subplots(figsize=(5.2, 4.5))
 29    im = ax.imshow(np.abs(full_pick))
 30    ax.set_xlabel("condition")
 31    ax.set_ylabel("condition")
 32    ax.set_title("Diagonal tangential data produce a block Pick matrix")
 33    fig.colorbar(im, ax=ax, shrink=0.82)
 34    fig.tight_layout()
 35    fig.savefig(out_dir / "diagonal_tangential_pick_matrix.png", dpi=160)
 36    plt.close(fig)
 37
 38    fig, ax = plt.subplots(figsize=(6.4, 3.6))
 39    ax.bar(np.arange(1, len(channel_min_eigs) + 1), channel_min_eigs)
 40    ax.axhline(0.0, linestyle="--", linewidth=1.0)
 41    ax.set_xlabel("channel")
 42    ax.set_ylabel("minimum scalar Pick eigenvalue")
 43    ax.set_title(f"Independent scalar feasibility checks; off-block max={block_error:.1e}")
 44    fig.tight_layout()
 45    fig.savefig(out_dir / "diagonal_scalar_pick_min_eigenvalues.png", dpi=160)
 46    plt.close(fig)
 47
 48
 49channels = 3
 50points_per_channel = 3
 51gains = np.array([0.25, -0.40 + 0.05j, 0.35j], dtype=np.complex128)
 52points_by_channel = [
 53    np.array([-0.20, 0.05 + 0.10j, 0.26j]),
 54    np.array([0.00, 0.18 - 0.12j, -0.30j]),
 55    np.array([0.12, -0.22 + 0.05j, 0.32j]),
 56]
 57
 58points: list[complex] = []
 59directions: list[np.ndarray] = []
 60values: list[np.ndarray] = []
 61for channel in range(channels):
 62    basis = np.eye(channels, dtype=np.complex128)[channel]
 63    for z in points_by_channel[channel]:
 64        points.append(complex(z))
 65        directions.append(basis.copy())
 66        values.append(gains[channel] * basis)
 67
 68full_data = ld.RightTangentialSchurData(np.array(points), np.array(directions), np.array(values))
 69full_pick = ld.right_tangential_pick_matrix(full_data)
 70
 71# Since the conditions are ordered by channel and directions are coordinate
 72# vectors, the full Pick matrix should be block diagonal with scalar Pick blocks.
 73channel_min_eigs: list[float] = []
 74block_diag = np.zeros_like(full_pick)
 75for channel in range(channels):
 76    sl = slice(channel * points_per_channel, (channel + 1) * points_per_channel)
 77    scalar_values = np.full((points_per_channel, 1), gains[channel], dtype=np.complex128)
 78    scalar_data = ld.RightTangentialSchurData(
 79        points_by_channel[channel], np.ones((points_per_channel, 1)), scalar_values
 80    )
 81    scalar_pick = ld.right_tangential_pick_matrix(scalar_data)
 82    block_diag[sl, sl] = scalar_pick
 83    channel_min_eigs.append(float(ld.pick_matrix_eigenvalues(scalar_pick)[0]))
 84
 85block_error = float(np.max(np.abs(full_pick - block_diag)))
 86constant_solution = np.diag(gains)
 87residual = ld.max_tangential_residual(full_data, constant_solution)
 88
 89print("channels:", channels)
 90print("points per channel:", points_per_channel)
 91print("total tangential conditions:", full_data.total_conditions)
 92print("diagonal Schur max singular value:", f"{np.max(np.abs(gains)):.6f}")
 93print("full Pick min eigenvalue:", f"{ld.pick_matrix_eigenvalues(full_pick)[0]:.6e}")
 94print("max |full Pick - scalar block diagonal Pick|:", f"{block_error:.3e}")
 95print("constant diagonal interpolation residual:", f"{residual:.3e}")
 96print("scalar channel min eigenvalues:", np.round(channel_min_eigs, 6))
 97print(
 98    "interpretation: diagonal MIMO tangential data decompose into independent scalar Schur/Pick problems"
 99)
100
101_save_figures(full_pick, block_error, channel_min_eigs)