Hessenberg Form¶
Hessenberg form reduces a matrix to almost-triangular form.
Mental Model
Hessenberg form is a halfway house between a full matrix and a triangular one -- it has zeros below the first subdiagonal. This near-triangular structure makes subsequent eigenvalue iterations converge much faster, which is why eigenvalue algorithms almost always start by reducing to Hessenberg form.
Basic Decomposition¶
1. linalg.hessenberg¶
```python import numpy as np from scipy import linalg
def main(): A = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]])
H = linalg.hessenberg(A)
print("A =")
print(A)
print()
print("H (upper Hessenberg):")
print(H.round(6))
if name == "main": main() ```
2. Mathematical Form¶
where:
- \(Q\) is unitary
- \(H\) is upper Hessenberg (zero below first subdiagonal)
3. Structure¶
Upper Hessenberg:
[* * * *]
[* * * *]
[0 * * *]
[0 0 * *]
With Transformation Matrix¶
1. Return Q¶
```python import numpy as np from scipy import linalg
def main(): A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
H, Q = linalg.hessenberg(A, calc_q=True)
print("H (Hessenberg form):")
print(H.round(6))
print()
print("Q (unitary):")
print(Q.round(6))
print()
print("Verify Q @ H @ Q.T:")
print((Q @ H @ Q.T).round(6))
if name == "main": main() ```
2. Verify Orthogonality¶
```python import numpy as np from scipy import linalg
def main(): A = np.random.randn(4, 4)
H, Q = linalg.hessenberg(A, calc_q=True)
print("Q.T @ Q (should be identity):")
print((Q.T @ Q).round(10))
if name == "main": main() ```
Eigenvalue Computation¶
1. Why Hessenberg¶
```python import numpy as np from scipy import linalg
def main(): n = 5 A = np.random.randn(n, n)
# Eigenvalue algorithms work faster on Hessenberg form
# QR iteration on H: O(n^2) per iteration vs O(n^3) on A
H = linalg.hessenberg(A)
# Eigenvalues are preserved
eig_A = np.sort(np.linalg.eigvals(A))
eig_H = np.sort(np.linalg.eigvals(H))
print("Eigenvalues of A:")
print(eig_A.round(6))
print()
print("Eigenvalues of H:")
print(eig_H.round(6))
if name == "main": main() ```
2. QR Iteration on Hessenberg¶
```python import numpy as np from scipy import linalg
def main(): A = np.array([[4, 1, 0], [1, 3, 1], [0, 1, 2]])
# Reduce to Hessenberg first
H = linalg.hessenberg(A)
# QR iteration (faster on Hessenberg)
Hk = H.copy()
for _ in range(30):
Q, R = np.linalg.qr(Hk)
Hk = R @ Q
print("Converged to quasi-triangular:")
print(Hk.round(6))
print()
print("Eigenvalues (diagonal):")
print(np.diag(Hk).round(6))
if name == "main": main() ```
Symmetric Case¶
1. Tridiagonal Form¶
```python import numpy as np from scipy import linalg
def main(): # Symmetric matrix A = np.array([[4, 1, 0, 0], [1, 4, 1, 0], [0, 1, 4, 1], [0, 0, 1, 4]])
H = linalg.hessenberg(A)
print("H for symmetric A (tridiagonal):")
print(H.round(6))
if name == "main": main() ```
2. Why Tridiagonal¶
For symmetric matrices, Hessenberg form becomes tridiagonal, enabling even faster algorithms.
Computational Cost¶
1. Complexity Analysis¶
```python import numpy as np from scipy import linalg import time
def main(): sizes = [100, 200, 400, 800]
print("Hessenberg reduction times:")
print(f"{'n':>6} | {'Time (sec)':>12}")
print("-" * 22)
for n in sizes:
A = np.random.randn(n, n)
start = time.perf_counter()
H = linalg.hessenberg(A)
elapsed = time.perf_counter() - start
print(f"{n:6d} | {elapsed:12.4f}")
if name == "main": main() ```
2. Reduction is O(n³)¶
Initial reduction: \(O(n^3)\)
Each QR step on H: \(O(n^2)\) instead of \(O(n^3)\)
Applications¶
1. Polynomial Eigenvalues¶
```python import numpy as np from scipy import linalg
def main(): # Roots of p(x) = x^3 - 6x^2 + 11x - 6 # Are eigenvalues of companion matrix
coeffs = [1, -6, 11, -6] # x^3 - 6x^2 + 11x - 6
# Companion matrix is already Hessenberg
C = np.array([[6, -11, 6],
[1, 0, 0],
[0, 1, 0]])
roots = np.linalg.eigvals(C)
print("Polynomial roots:")
print(np.sort(roots.real).round(6))
print()
print("Verify: (x-1)(x-2)(x-3) = x^3 - 6x^2 + 11x - 6")
if name == "main": main() ```
2. Transfer Function Analysis¶
```python import numpy as np from scipy import linalg
def main(): # State-space system: dx/dt = Ax A = np.array([[0, 1, 0], [0, 0, 1], [-6, -11, -6]])
H, Q = linalg.hessenberg(A, calc_q=True)
print("System in Hessenberg form:")
print(H.round(6))
print()
# Eigenvalues determine stability
eigs = np.linalg.eigvals(H)
print("Poles (eigenvalues):")
print(eigs.round(6))
if name == "main": main() ```
3. Matrix Exponential Preprocessing¶
```python import numpy as np from scipy import linalg
def main(): A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
# Hessenberg reduction can accelerate exp(A) computation
H, Q = linalg.hessenberg(A, calc_q=True)
# exp(A) = Q @ exp(H) @ Q.T
exp_H = linalg.expm(H)
exp_A = Q @ exp_H @ Q.T
print("exp(A) via Hessenberg:")
print(exp_A.round(6))
print()
print("exp(A) direct:")
print(linalg.expm(A).round(6))
if name == "main": main() ```
Summary¶
1. Functions¶
| Function | Description |
|---|---|
linalg.hessenberg(A) |
Hessenberg form |
linalg.hessenberg(A, calc_q=True) |
With transformation Q |
2. Key Properties¶
- \(H\) is upper Hessenberg (zeros below subdiagonal)
- Preserves eigenvalues
- Symmetric A → Tridiagonal H
- Speeds up QR iteration: \(O(n^2)\) vs \(O(n^3)\) per step
Exercises¶
Exercise 1.
Create a random \(6 \times 6\) matrix with np.random.seed(10). Compute its Hessenberg form \(H\) with the transformation matrix \(Q\). Verify that \(Q\) is orthogonal (i.e., \(\|Q^TQ - I\|_F < 10^{-12}\)) and that \(\|QHQ^T - A\|_F < 10^{-12}\).
Solution to Exercise 1
import numpy as np
from scipy import linalg
np.random.seed(10)
A = np.random.randn(6, 6)
H, Q = linalg.hessenberg(A, calc_q=True)
orth_error = np.linalg.norm(Q.T @ Q - np.eye(6))
recon_error = np.linalg.norm(Q @ H @ Q.T - A)
print(f"Orthogonality error: {orth_error:.2e}")
print(f"Reconstruction error: {recon_error:.2e}")
assert orth_error < 1e-12
assert recon_error < 1e-12
Exercise 2. Construct a \(5 \times 5\) symmetric tridiagonal matrix with 4 on the diagonal and 1 on the sub/super-diagonals. Compute its Hessenberg form and verify that the result is tridiagonal (all entries below the first subdiagonal are effectively zero, i.e., below \(10^{-12}\) in absolute value).
Solution to Exercise 2
import numpy as np
from scipy import linalg
A = np.diag([4]*5) + np.diag([1]*4, 1) + np.diag([1]*4, -1)
H = linalg.hessenberg(A)
# Check entries below first subdiagonal
is_tridiagonal = True
for i in range(2, 5):
for j in range(0, i - 1):
if abs(H[i, j]) > 1e-12:
is_tridiagonal = False
print("Hessenberg of symmetric matrix:")
print(H.round(10))
print(f"Is tridiagonal: {is_tridiagonal}")
Exercise 3.
Generate a random \(8 \times 8\) matrix with np.random.seed(7). Reduce it to Hessenberg form \(H\), then run 50 iterations of the QR algorithm on \(H\) (at each step compute $Q, R = $ QR of \(H_k\), then set \(H_{k+1} = RQ\)). Compare the diagonal of the final matrix with the eigenvalues from np.linalg.eigvals and print the maximum absolute error after sorting by real part.
Solution to Exercise 3
import numpy as np
from scipy import linalg
np.random.seed(7)
A = np.random.randn(8, 8)
H = linalg.hessenberg(A)
Hk = H.copy()
for _ in range(50):
Q, R = np.linalg.qr(Hk)
Hk = R @ Q
qr_eigs = np.sort_complex(np.diag(Hk))
true_eigs = np.sort_complex(np.linalg.eigvals(A))
# Sort by real part for comparison
qr_sorted = sorted(qr_eigs, key=lambda x: (x.real, x.imag))
true_sorted = sorted(true_eigs, key=lambda x: (x.real, x.imag))
max_error = max(abs(a - b) for a, b in zip(qr_sorted, true_sorted))
print(f"Max eigenvalue error: {max_error:.6f}")