Eigen — Implementation¶
File Structure¶
eigen/
├── tiny_eigen.h (15 lines — eigenvalue API)
├── tiny_eigen.c (210 lines — eigenvalue implementation)
├── tiny_modal.h (24 lines — modal analysis API)
└── tiny_modal.c (157 lines — modal analysis implementation)
Dependencies: tiny_cfloat.h, tiny_decomp.h, <math.h>.
Eigenvalue Pipeline¶
2×2 Closed-Form¶
Simplest path — no iteration, no workspace:
float tr = a00 + a11;
float det = (a00 - a11)*(a00 - a11) + 4*a01*a10;
if (det >= 0) { // real eigenvalues
lam = (tr ± sqrt(det)) / 2;
} else { // complex conjugate pair
re = tr / 2;
im = sqrt(-det) / 2;
}
Used as the base case for deflation in the Francis QR iteration.
Schur Extraction¶
After Francis QR iteration, \(A\) is in real Schur form (quasi-triangular):
Real Schur (n=4):
┌───┬───┬───┬───┐
│ x │ x │ x │ x │
│ 0 │ x │ x │ x │ ← sub-diagonal elements are 0 (or nearly 0)
│ 0 │ 0 │ x │ x │
│ 0 │ 0 │ x │ x │ ← 2×2 block → complex conjugate pair
└───┴───┴───┴───┘
The function walks the diagonal:
- If
T[k+1][k] ≈ 0→ real eigenvalue atT[k][k] - If
T[k+1][k] ≠ 0→ \(2 \times 2\) block → extract complex conjugate pair viatiny_eig_2x2_f32
General Eigendecomposition¶
Algorithm flow:
1. Allocate workspace (n×n matrix + n-vector + n² scratch)
2. Copy A → H (row-major, contiguous)
3. Hessenberg reduction on H (Householder reflections)
4. Francis double-shift QR on H (implicit shift, Wilkinson shift)
5. Extract eigenvalues from H (quasi-triangular)
6. Return eigenvalues in eigs[]
Workspace layout:
| Section | Size | Purpose |
|---|---|---|
| H (work matrix) | \(n \times n\) floats | Copy of A, transformed in-place |
| Hessenberg scratch | \(n\) floats | Householder vector store |
| QR scratch | \(n^2\) floats | Rotation accumulators |
Modal Analysis¶
Pole Conversion¶
// Continuous-time pole from discrete eigenvalue λ:
p = ln(λ) * fs
// Natural frequency and damping:
ω_n = |p| // rad/s
f = ω_n / (2π) // Hz
ζ = -Re(p) / |p| // damping ratio (0 = undamped, 1 = critically damped)
MAC Computation¶
- Pair: direct dot product of two vectors, \(O(n_{\text{dof}})\)
- Matrix: \(n_1 \times n_2\) pairwise combinations, \(O(n_1 \cdot n_2 \cdot n_{\text{dof}})\)
Stable Pole Filtering¶
A pole is considered "stable" when:
\[\zeta < \zeta_{\max} \quad \land \quad f_{\min} < f < f_{\max}\]
The function counts how many poles satisfy these criteria — caller then extracts the corresponding subset.
Numerical Considerations¶
- Francis QR uses Wilkinson shifts for accelerated convergence — shifts are computed from the bottom-right \(2 \times 2\) sub-matrix
- Deflation threshold: \(10^{-14} \cdot \max(1, |T_{kk}| + |T_{k+1,k+1}|)\)
- Real matrices only: The algorithm preserves real arithmetic — no complex overhead until eigenvalue extraction
- Float32 precision limits eigenvalue accuracy to \(\sim 10^{-4}\) for typical SSI matrices (condition \(\sim 10^6\))