Schur Decomposition¶
Schur decomposition factors a matrix into quasi-triangular form.
Basic Decomposition¶
1. linalg.schur¶
import numpy as np
from scipy import linalg
def main():
A = np.array([[1, 2, 3],
[0, 4, 5],
[0, 0, 6]])
T, Z = linalg.schur(A)
print("A =")
print(A)
print()
print("T (quasi-triangular):")
print(T)
print()
print("Z (unitary):")
print(Z)
print()
print("Verify Z @ T @ Z.T:")
print((Z @ T @ Z.T).round(10))
if __name__ == "__main__":
main()
2. Mathematical Form¶
\[A = ZTZ^H\]
where: - \(Z\) is unitary (\(Z^HZ = I\)) - \(T\) is upper quasi-triangular (upper triangular with possible 2×2 blocks on diagonal)
3. Real vs Complex¶
import numpy as np
from scipy import linalg
def main():
# Matrix with complex eigenvalues
A = np.array([[0, -1],
[1, 0]])
# Real Schur (2x2 blocks for complex eigenvalue pairs)
T_real, Z_real = linalg.schur(A, output='real')
print("Real Schur T:")
print(T_real)
print()
# Complex Schur (truly upper triangular)
T_complex, Z_complex = linalg.schur(A, output='complex')
print("Complex Schur T:")
print(T_complex)
if __name__ == "__main__":
main()
Eigenvalues from Schur¶
1. Diagonal Contains Eigenvalues¶
import numpy as np
from scipy import linalg
def main():
A = np.array([[4, 1, 0],
[1, 3, 1],
[0, 1, 2]])
T, Z = linalg.schur(A, output='complex')
print("Eigenvalues from Schur diagonal:")
print(np.diag(T))
print()
print("Eigenvalues direct:")
print(np.linalg.eigvals(A))
if __name__ == "__main__":
main()
2. Why Use Schur¶
import numpy as np
from scipy import linalg
def main():
# Schur is always computable, even for defective matrices
# Eigendecomposition may fail
# Defective matrix (eigenvalue with algebraic > geometric multiplicity)
A = np.array([[1, 1],
[0, 1]])
T, Z = linalg.schur(A)
print("Schur T:")
print(T)
print()
print("Eigenvalues from T diagonal:", np.diag(T))
if __name__ == "__main__":
main()
Sorted Schur¶
1. linalg.rsf2csf¶
import numpy as np
from scipy import linalg
def main():
A = np.array([[0, -1, 0],
[1, 0, 0],
[0, 0, 2]])
# Real Schur form
T_real, Z_real = linalg.schur(A, output='real')
print("Real Schur T:")
print(T_real)
print()
# Convert to complex Schur
T_complex, Z_complex = linalg.rsf2csf(T_real, Z_real)
print("Complex Schur T:")
print(T_complex.round(10))
if __name__ == "__main__":
main()
2. Ordering Eigenvalues¶
import numpy as np
from scipy import linalg
def main():
A = np.array([[1, 2, 3],
[0, 4, 5],
[0, 0, 6]])
# Get Schur form
T, Z = linalg.schur(A)
# Order by eigenvalue magnitude (custom sort)
def select(eigval):
return abs(eigval) < 3 # Select small eigenvalues first
T_sorted, Z_sorted = linalg.schur(A, sort=select)
print("Original T diagonal:", np.diag(T))
print("Sorted T diagonal: ", np.diag(T_sorted))
if __name__ == "__main__":
main()
Matrix Functions via Schur¶
1. Why Schur for Matrix Functions¶
import numpy as np
from scipy import linalg
def main():
A = np.array([[1, 2],
[0, 3]])
# Matrix exponential uses Schur internally
# exp(A) = Z @ exp(T) @ Z.H
T, Z = linalg.schur(A, output='complex')
# exp(T) is easier because T is triangular
exp_T = linalg.expm(T)
exp_A_via_schur = Z @ exp_T @ Z.conj().T
# Direct
exp_A_direct = linalg.expm(A)
print("exp(A) via Schur:")
print(exp_A_via_schur.real.round(6))
print()
print("exp(A) direct:")
print(exp_A_direct.round(6))
if __name__ == "__main__":
main()
2. Matrix Square Root¶
import numpy as np
from scipy import linalg
def main():
A = np.array([[4, 0],
[0, 9]])
T, Z = linalg.schur(A, output='complex')
# sqrt(T) for triangular is straightforward
sqrt_T = np.diag(np.sqrt(np.diag(T)))
sqrt_A = Z @ sqrt_T @ Z.conj().T
print("sqrt(A):")
print(sqrt_A.real.round(6))
print()
print("Verify sqrt(A) @ sqrt(A):")
print((sqrt_A @ sqrt_A).real.round(6))
if __name__ == "__main__":
main()
Sylvester Equation¶
1. Solving AX + XB = C¶
import numpy as np
from scipy import linalg
def main():
A = np.array([[1, 2],
[0, 3]])
B = np.array([[4, 0],
[0, 5]])
C = np.array([[1, 1],
[1, 1]])
# Solve AX + XB = C
X = linalg.solve_sylvester(A, B, C)
print("Solution X:")
print(X)
print()
print("Verify A @ X + X @ B:")
print(A @ X + X @ B)
if __name__ == "__main__":
main()
2. Schur-Based Solution¶
Sylvester equation solver uses Schur decomposition internally for numerical stability.
Applications¶
1. Control Theory¶
import numpy as np
from scipy import linalg
def main():
# System stability analysis
A = np.array([[-1, 1],
[0, -2]])
T, Z = linalg.schur(A, output='complex')
eigenvalues = np.diag(T)
print("System matrix eigenvalues:")
print(eigenvalues)
print()
if np.all(eigenvalues.real < 0):
print("System is stable (all eigenvalues have negative real parts)")
else:
print("System is unstable")
if __name__ == "__main__":
main()
2. Invariant Subspaces¶
import numpy as np
from scipy import linalg
def main():
A = np.array([[2, 1, 0],
[0, 3, 1],
[0, 0, 4]])
# Sort eigenvalues: put eigenvalue 2 first
T, Z = linalg.schur(A, sort=lambda x: x.real < 2.5)
print("Sorted Schur T:")
print(T)
print()
# First column of Z spans invariant subspace for eigenvalue 2
print("Invariant subspace (first column of Z):")
print(Z[:, 0])
if __name__ == "__main__":
main()
Summary¶
1. Functions¶
| Function | Description |
|---|---|
linalg.schur(A) |
Schur decomposition |
linalg.schur(A, output='complex') |
Complex Schur |
linalg.schur(A, sort=f) |
Sorted Schur |
linalg.rsf2csf(T, Z) |
Real to complex Schur |
2. Key Properties¶
- Always exists (unlike eigendecomposition)
- T is quasi-triangular (real) or triangular (complex)
- Eigenvalues on diagonal of T
- Useful for matrix functions and equations