~/blog
Eigenvalue Computation: From Math Derivation to Native C++
The Algebraic Illusion
Every engineer learns eigenvalue computation the exact same way in introductory linear algebra: write down the matrix , construct the characteristic polynomial by computing , factor the polynomial to find its roots , and solve the resulting homogeneous system for the null space basis vectors.
On paper, this method feels satisfying and complete. In production software, it is an absolute disaster.
The breakdown happens for two distinct reasons: algebraic impossibility and catastrophic numerical instability. First, by the Abel-Ruffini theorem, there is no general algebraic closed-form solution for polynomial roots of degree . Second, even for modest matrices, computing eigenvalues by finding the roots of an expanded characteristic polynomial is notoriously ill-conditioned. A classic example is Wilkinson's polynomial: shifting a single coefficient in a 20th-degree polynomial by as little as can shift its roots from distinct integers into widely scattered complex numbers.
Numerical linear algebra solves eigensystems by discarding root-finding entirely. Instead of expanding polynomials, production solvers rely on iterative orthogonal similarity transformations that preserve the spectrum while progressively driving off-diagonal elements toward zero.
Understanding how we transition from pencil-and-paper derivations to production-grade LAPACK routines and SIMD-vectorized C++ is essential for building fast, stable systems in robotics, graphics, computer vision, and machine learning.
Spectral Mechanics and Mathematical Invariants
Let denote a real square matrix representing a linear transformation on the vector space . A non-zero vector is an eigenvector of associated with eigenvalue if and only if:
A non-trivial solution exists if and only if the linear operator is singular, which requires its determinant to vanish:
The characteristic polynomial is an -th degree monic polynomial in . By the Fundamental Theorem of Algebra, has exactly complex roots (counting algebraic multiplicity , which is the multiplicity of as a root of ).
Two fundamental algebraic invariants tie the operator directly to its spectrum:
-
Trace Invariant: The sum of all diagonal elements equals the sum of all eigenvalues:
-
Determinant Invariant: The determinant equals the product of all eigenvalues:
Structural Matrix Taxonomy
The matrix structure determines the spectral nature and whether the transformation can be decoupled into orthogonal principal components:
- Real Symmetric Matrices (): The spectral theorem guarantees that all eigenvalues are strictly real. Eigenvectors corresponding to distinct eigenvalues are mutually orthogonal. Consequently, admits an orthogonal diagonalization , where is an orthogonal matrix () whose columns are normalized eigenvectors, and .
- Real Non-Symmetric Matrices (): Eigenvalues can appear as complex conjugate pairs , accompanied by complex conjugate eigenvector pairs .
- Defective Matrices: A matrix is defective if it possesses an eigenvalue whose geometric multiplicity is strictly less than its algebraic multiplicity : Defective matrices lack a complete basis of linearly independent eigenvectors, making them non-diagonalizable. Their canonical reduction requires the Jordan normal form with non-trivial Jordan blocks.
Step-by-Step Analytical Derivations
Working through three classical matrix topologies demonstrates how real distinct, defective, and complex spectra behave when evaluated analytically.
1. Distinct Real Spectrum ()
Consider the non-symmetric matrix:
Formulating the characteristic equation:
Factoring gives , yielding two distinct real eigenvalues: and .
To compute the eigenspace for :
Choosing gives the basis vector . Normalizing under the Euclidean norm :
To compute the eigenspace for :
Choosing gives . Normalizing under :
Checking trace and determinant invariants:
2. Defective Operator with Repeated Roots ()
Consider the matrix:
Evaluating the characteristic equation :
The spectrum consists of with algebraic multiplicity , and with algebraic multiplicity .
For :
From row 2, . Substituting into row 3 gives . Setting yields the eigenvector basis vector .
For the repeated root :
Row 2 forces . Substituting into row 1 and row 3 leaves .
Setting gives .
Because there is only one free variable, the geometric multiplicity is . Since , matrix is defective. No invertible matrix exists such that is purely diagonal; can only be reduced to a Jordan form containing a Jordan block.
3. Planar Rotation Matrix with Complex Conjugate Spectrum
Consider the standard planar rotation matrix:
The characteristic equation:
For :
Setting yields the unnormalized vector . Normalizing with the Hermitian inner product :
For , taking the complex conjugate gives:
Because rotation preserves vector lengths while continuously changing their directions, no real vector can maintain its direction under . The eigenvectors exist strictly in the complexified vector space .
Numerical Algorithms for Dense Eigensystems
Because characteristic polynomial root-finding fails for general matrices, practical algorithms rely on iterative transformations.
Power Iteration: Isolating the Dominant Mode
Power iteration finds the largest eigenvalue in absolute value () and its corresponding eigenvector.
Starting with an initial vector with non-zero component along the dominant eigenvector , the recurrence relation applies repeatedly:
As , the sequence aligns with the dominant eigenvector . The dominant eigenvalue is then extracted using the Rayleigh quotient:
The asymptotic error at iteration scales as:
When the spectral gap is narrow (), Power Iteration converges very slowly. In such scenarios, acceleration methods like Rayleigh Quotient Iteration or spectral shifts are required.
The QR Algorithm: Extracting the Full Spectrum
The QR algorithm extracts the entire spectrum of a matrix by executing iterative orthogonal similarity transformations.
At step :
- Decompose the current matrix into an orthogonal matrix and an upper triangular matrix :
- Recombine them in reverse order to form :
Substituting into the second equation:
Because is an orthogonal similarity transformation of , both matrices have identical eigenvalues across all iterations. As , converges to an upper triangular Schur matrix (or quasi-triangular real Schur form with blocks for complex conjugate pairs), where the eigenvalues sit directly on the main diagonal.
In production numerical libraries (LAPACK, Eigen3), this raw QR loop is structured into an optimized pipeline:
Code Implementations Across Four Software Paradigms
Let us implement and compare eigensystem routines across four different software layers: symbolic CAS, pure NumPy algorithms, production SciPy/LAPACK, and native SIMD-accelerated C++ with Eigen3.
1. Exact Symbolic Computation via SymPy
SymPy provides exact algebraic derivations without floating-point rounding errors:
import sympy as sp
# Define the 3x3 matrix from our analytical derivation
A = sp.Matrix([
[ 1, 2, 2],
[ 0, 2, 1],
[-1, 2, 2]
])
lam = sp.Symbol('lambda')
# 1. Compute characteristic polynomial symbolically
char_poly = A.charpoly(lam)
print("Characteristic Polynomial:", char_poly.as_expr())
# 2. Extract exact eigenvalues and algebraic multiplicities
evals = A.eigenvals()
print("Eigenvalues (Value: Multiplicity):", evals)
# 3. Extract exact eigenspaces (value, multiplicity, [basis vectors])
evecs = A.eigenvects()
for val, mult, basis in evecs:
print(f"Eigenvalue {val} (Alg Multiplicity: {mult}, Geo Multiplicity: {len(basis)}):")
for vec in basis:
print(" Basis Vector:", vec.T)Running this script outputs:
Characteristic Polynomial: lambda**3 - 5*lambda**2 + 8*lambda - 4
Eigenvalues (Value: Multiplicity): {1: 1, 2: 2}
Eigenvalue 1 (Alg Multiplicity: 1, Geo Multiplicity: 1):
Basis Vector: Matrix([[0, -1, 1]])
Eigenvalue 2 (Alg Multiplicity: 2, Geo Multiplicity: 1):
Basis Vector: Matrix([[2, 1, 0]])The output confirms our manual derivation: has algebraic multiplicity 2 but only a 1-dimensional eigenspace, proving defectivity.
2. Custom Iterative Solvers in NumPy
The following script implements basic Power Iteration and the QR Algorithm directly from scratch:
import numpy as np
def power_iteration(A: np.ndarray, num_iterations: int = 100) -> tuple[float, np.ndarray]:
np.random.seed(42)
b_k = np.random.rand(A.shape[1])
b_k = b_k / np.linalg.norm(b_k)
for _ in range(num_iterations):
b_k1 = np.dot(A, b_k)
norm = np.linalg.norm(b_k1)
if norm == 0:
break
b_k = b_k1 / norm
dominant_eigenvalue = float(np.dot(b_k.T, np.dot(A, b_k)) / np.dot(b_k.T, b_k))
return dominant_eigenvalue, b_k
def qr_algorithm(A: np.ndarray, num_iterations: int = 50) -> tuple[np.ndarray, np.ndarray]:
A_k = np.copy(A).astype(float)
n = A.shape[0]
Q_accum = np.eye(n)
for _ in range(num_iterations):
Q, R = np.linalg.qr(A_k)
A_k = R @ Q
Q_accum = Q_accum @ Q
eigenvalues = np.diag(A_k)
return eigenvalues, Q_accum
# Real symmetric test matrix
A_sym = np.array([
[ 6.0, -2.0, 1.0],
[-2.0, 3.0, -2.0],
[ 1.0, -2.0, 6.0]
], dtype=float)
dom_val, dom_vec = power_iteration(A_sym, num_iterations=100)
qr_vals, qr_evecs = qr_algorithm(A_sym, num_iterations=50)
print(f"Power Iteration Dominant Eigenvalue: {dom_val:.6f}")
print(f"Power Iteration Dominant Eigenvector: {dom_vec}")
print(f"QR Algorithm Extracted Spectrum: {np.sort(qr_vals)[::-1]}")Running this code produces:
Power Iteration Dominant Eigenvalue: 8.000000
Power Iteration Dominant Eigenvector: [ 0.70710678 -0. -0.70710678]
QR Algorithm Extracted Spectrum: [8. 5. 2.]Power Iteration isolates the dominant mode , while the QR algorithm extracts the entire spectrum .
3. Production LAPACK Drivers via NumPy and SciPy
High-level Python libraries wrap Fortran LAPACK routines (dgeev for general non-symmetric matrices, dsyev for symmetric matrices, and dggev for generalized eigensystems):
import numpy as np
import scipy.linalg as la
# 1. Real Symmetric Matrix -> dsyev (faster, guaranteed orthogonal real vectors)
A_sym = np.array([[6.0, -2.0, 1.0], [-2.0, 3.0, -2.0], [1.0, -2.0, 6.0]])
evals_sym, evecs_sym = la.eigh(A_sym)
print("Symmetric Eigenvalues (la.eigh -> dsyev):", evals_sym)
# 2. General Non-Symmetric Matrix -> dgeev
A_gen = np.array([[4.0, 1.0], [2.0, 3.0]])
evals_gen, evecs_gen = la.eig(A_gen)
print("General Eigenvalues (la.eig -> dgeev):", evals_gen.real)
# 3. Planar Rotation Matrix (Complex Spectrum)
D_rot = np.array([[0.0, -1.0], [1.0, 0.0]])
evals_rot, evecs_rot = la.eig(D_rot)
print("Rotation Matrix Complex Eigenvalues:", evals_rot)
# 4. Generalized Eigensystem: A * v = lambda * B * v
B_metric = np.array([[2.0, 1.0], [1.0, 2.0]])
gen_evals, gen_evecs = la.eig(A_gen, B_metric)
print("Generalized Eigenvalues:", gen_evals.real)Running this script yields:
Symmetric Eigenvalues (la.eigh -> dsyev): [2. 5. 8.]
General Eigenvalues (la.eig -> dgeev): [5. 2.]
Rotation Matrix Complex Eigenvalues: [0.+1.j 0.-1.j]
Generalized Eigenvalues: [1. 3.]For defective matrices, numerical eigensolvers return duplicate eigenvectors that are linearly dependent within machine precision tolerances (). When complex conjugate roots appear, NumPy automatically promotes real floating-point buffers to complex128.
4. Native C++ with Eigen3 and SIMD Vectorization
In latency-critical applications (such as robotics state estimation, physics engines, and high-frequency trading), Python's FFI overhead is prohibitive. Compiled C++ using Eigen3 executes matrix decomposition directly on CPU registers:
#include <iostream>
#include <complex>
#include <Eigen/Dense>
#include <Eigen/Eigenvalues>
int main() {
// Stack-allocated 3x3 double-precision matrix
Eigen::Matrix3d A;
A << 6.0, -2.0, 1.0,
-2.0, 3.0, -2.0,
1.0, -2.0, 6.0;
// SelfAdjointEigenSolver exploits symmetry for maximum performance
Eigen::SelfAdjointEigenSolver<Eigen::Matrix3d> solver(A);
if (solver.info() != Eigen::Success) {
std::cerr << "Eigensolver computation failed to converge!" << std::endl;
return 1;
}
Eigen::Vector3d eigenvalues = solver.eigenvalues();
Eigen::Matrix3d eigenvectors = solver.eigenvectors();
std::cout << "Computed Real Eigenvalues:\n" << eigenvalues << "\n\n";
std::cout << "Computed Orthogonal Eigenvectors (Columns):\n" << eigenvectors << "\n\n";
// Verify residual norm: ||A * v - lambda * v||_2 for dominant mode
double lambda_dom = eigenvalues[2]; // Sorted in increasing order
Eigen::Vector3d v_dom = eigenvectors.col(2);
Eigen::Vector3d residual = A * v_dom - lambda_dom * v_dom;
std::cout << "Residual Norm ||A*v - lambda*v||_2: " << residual.norm() << std::endl;
return 0;
}Compiling with aggressive compiler flags:
g++ -O3 -march=native -DNDEBUG -I/usr/include/eigen3 eigen_demo.cpp -o eigen_demo
./eigen_demoOutput:
Computed Real Eigenvalues:
2
5
8
Computed Orthogonal Eigenvectors (Columns):
0.408248 -0.57735 0.707107
0.816497 0.57735 -0.000000
-0.408248 0.57735 0.707107
Residual Norm ||A*v - lambda*v||_2: 0Architectural Comparison and Hardware Performance
The choice of execution paradigm introduces fundamental trade-offs across mathematical precision, memory allocation, and instruction-level parallelism.
| Execution Paradigm | Primary Engine | Precision & Type | Computational Complexity | Defective Matrix Handling | Target Applications |
|---|---|---|---|---|---|
| Symbolic CAS (SymPy) | Python object tree / AST | Exact rationals / radicals | Exponential | Full Jordan canonical form | Analytical proofs, edge-case analysis |
| Manual Hand Algebra | Paper / Determinant expansion | Exact fractions / surds | Human-limited () | Manual null space derivation | Theoretical coursework, verification |
| Custom Python Loops | NumPy array primitives | IEEE 754 Float64 | Power: , QR: | Fails on defectivity without shifts | Algorithm prototyping, education |
| Production LAPACK | C/Fortran (dgeev/dsyev) | Hardware Float32/Float64 | Optimized | Duplicated collinear eigenvectors | Offline ML, data analysis, SciPy |
| Native C++ (Eigen3) | Expression templates + SIMD | Float32/Float64/Complex | Hardware-vectorized | Specialized solver classes | Real-time robotics, physics, HFT |
The Foreign Function Interface (FFI) Tax
When computing eigensystems for massive matrices (), NumPy and SciPy perform almost identically to native C++ because the execution time is entirely dominated by compute-intensive Level-3 BLAS operations inside the LAPACK routine.
However, for low-dimensional systems () evaluated millions of times inside tight real-time loops (e.g., orientation kinematics, extended Kalman filtering, surface normal estimation):
- Python FFI Overhead: Every call to
np.linalg.eigorscipy.linalg.eighpays for Python function call overhead, argument type checking, C-FFI pointer marshaling, temporary buffer allocations, and GIL acquisition. This overhead often requires 1.5 to 5 microseconds per call. - Native Eigen3 Stack Execution: A fixed-size matrix (
Eigen::Matrix3d) allocates its 9doublevalues directly on the stack ( bytes). The compiler inlines the solver, unrolls all transformation loops, and generates direct vectorized SIMD instructions (vfmadd213pd), executing the entire eigensystem extraction in under 40 nanoseconds.
When to Use Which Tool
Selecting the right eigensystem strategy depends directly on your system's scale, sparsity, and latency budget:
- Use SymPy when deriving theoretical proofs, checking algebraic vs. geometric multiplicities, or verifying exact rational eigenspaces of small symbolic operators.
- Use Custom Iterative Solvers when studying convergence dynamics, spectral shifts, or testing specialized numerical preconditions.
- Use NumPy / SciPy (
scipy.linalg.eigh/eig) for data pipelines, offline ML research, PCA, clustering, and general scientific computing where development velocity outweighs sub-microsecond latency. - Use Native C++ (Eigen3) for real-time control, robotics kinematics, physical simulations, and embedded hardware where deterministic execution and stack-allocated SIMD performance are non-negotiable.
When matrices become sparse and high-dimensional (, such as web graphs or graph neural networks), dense solvers become completely intractable. In those regimes, the focus shifts to Krylov subspace methods: Arnoldi iteration for non-symmetric matrices and Lanczos factorization for symmetric systems, computing only the extremal eigenvalues without ever materializing the full dense operator.