amari_functional/spectral/
eigenvalue.rs

1//! Eigenvalue computation algorithms.
2//!
3//! This module provides algorithms for computing eigenvalues and
4//! eigenvectors of linear operators.
5
6use crate::error::{FunctionalError, Result};
7use crate::operator::{LinearOperator, MatrixOperator};
8use amari_core::Multivector;
9
10/// An eigenvalue with optional algebraic multiplicity.
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct Eigenvalue {
13    /// The eigenvalue λ.
14    pub value: f64,
15    /// Algebraic multiplicity (if known).
16    pub multiplicity: Option<usize>,
17}
18
19impl Eigenvalue {
20    /// Create a new eigenvalue.
21    pub fn new(value: f64) -> Self {
22        Self {
23            value,
24            multiplicity: None,
25        }
26    }
27
28    /// Create an eigenvalue with known multiplicity.
29    pub fn with_multiplicity(value: f64, multiplicity: usize) -> Self {
30        Self {
31            value,
32            multiplicity: Some(multiplicity),
33        }
34    }
35}
36
37/// An eigenvalue-eigenvector pair.
38#[derive(Debug, Clone)]
39pub struct Eigenpair<V> {
40    /// The eigenvalue λ.
41    pub eigenvalue: Eigenvalue,
42    /// The eigenvector v satisfying Tv = λv.
43    pub eigenvector: V,
44}
45
46impl<V> Eigenpair<V> {
47    /// Create a new eigenpair.
48    pub fn new(eigenvalue: f64, eigenvector: V) -> Self {
49        Self {
50            eigenvalue: Eigenvalue::new(eigenvalue),
51            eigenvector,
52        }
53    }
54}
55
56/// Power method for computing the dominant eigenvalue.
57///
58/// Iteratively computes x_{k+1} = Ax_k / ||Ax_k|| to find the
59/// eigenvector corresponding to the largest eigenvalue (in absolute value).
60///
61/// # Arguments
62///
63/// * `matrix` - The matrix operator
64/// * `initial` - Initial guess for the eigenvector
65/// * `max_iterations` - Maximum number of iterations
66/// * `tolerance` - Convergence tolerance
67///
68/// # Returns
69///
70/// The dominant eigenpair (λ, v) where λ is the largest eigenvalue.
71pub fn power_method<const P: usize, const Q: usize, const R: usize>(
72    matrix: &MatrixOperator<P, Q, R>,
73    initial: Option<&Multivector<P, Q, R>>,
74    max_iterations: usize,
75    tolerance: f64,
76) -> Result<Eigenpair<Multivector<P, Q, R>>> {
77    let n = MatrixOperator::<P, Q, R>::DIM;
78
79    // Initialize with provided vector or default
80    let mut v = if let Some(init) = initial {
81        init.clone()
82    } else {
83        // Default: vector with all 1s normalized
84        let coeffs: Vec<f64> = (0..n).map(|_| 1.0 / (n as f64).sqrt()).collect();
85        Multivector::<P, Q, R>::from_slice(&coeffs)
86    };
87
88    let mut eigenvalue = 0.0;
89
90    for iter in 0..max_iterations {
91        // Compute Av
92        let av = matrix.apply(&v)?;
93
94        // Compute Rayleigh quotient: λ = v^T Av / v^T v
95        let v_coeffs = v.to_vec();
96        let av_coeffs = av.to_vec();
97        let numerator: f64 = v_coeffs
98            .iter()
99            .zip(av_coeffs.iter())
100            .map(|(a, b)| a * b)
101            .sum();
102        let denominator: f64 = v_coeffs.iter().map(|x| x * x).sum();
103
104        if denominator.abs() < 1e-15 {
105            return Err(FunctionalError::numerical_instability(
106                "power method",
107                "Vector norm became zero",
108            ));
109        }
110
111        let new_eigenvalue = numerator / denominator;
112
113        // Check convergence
114        if (new_eigenvalue - eigenvalue).abs() < tolerance && iter > 0 {
115            return Ok(Eigenpair::new(new_eigenvalue, v));
116        }
117        eigenvalue = new_eigenvalue;
118
119        // Normalize Av for next iteration
120        let norm: f64 = av_coeffs.iter().map(|x| x * x).sum::<f64>().sqrt();
121        if norm < 1e-15 {
122            return Err(FunctionalError::numerical_instability(
123                "power method",
124                "Zero vector encountered",
125            ));
126        }
127        v = &av * (1.0 / norm);
128    }
129
130    Ok(Eigenpair::new(eigenvalue, v))
131}
132
133/// Inverse iteration for computing eigenvalue near a target.
134///
135/// Uses (A - σI)^{-1} power iteration to find the eigenvalue closest to σ.
136///
137/// # Arguments
138///
139/// * `matrix` - The matrix operator
140/// * `shift` - The target value σ (shift)
141/// * `initial` - Initial guess for the eigenvector
142/// * `max_iterations` - Maximum number of iterations
143/// * `tolerance` - Convergence tolerance
144///
145/// # Returns
146///
147/// The eigenpair (λ, v) where λ is closest to σ.
148pub fn inverse_iteration<const P: usize, const Q: usize, const R: usize>(
149    matrix: &MatrixOperator<P, Q, R>,
150    shift: f64,
151    initial: Option<&Multivector<P, Q, R>>,
152    max_iterations: usize,
153    tolerance: f64,
154) -> Result<Eigenpair<Multivector<P, Q, R>>> {
155    let n = MatrixOperator::<P, Q, R>::DIM;
156
157    // Compute (A - σI)
158    let identity = MatrixOperator::<P, Q, R>::identity();
159    let shifted = matrix.add(&identity.scale(-shift))?;
160
161    // For small matrices, use direct solve via LU decomposition
162    // For now, approximate with a few Gauss-Seidel iterations
163
164    let mut v = if let Some(init) = initial {
165        init.clone()
166    } else {
167        let coeffs: Vec<f64> = (0..n).map(|_| 1.0 / (n as f64).sqrt()).collect();
168        Multivector::<P, Q, R>::from_slice(&coeffs)
169    };
170
171    for _ in 0..max_iterations {
172        // Solve (A - σI)w = v using iterative refinement
173        let w = solve_linear_system(&shifted, &v, 50, tolerance)?;
174
175        // Normalize
176        let w_coeffs = w.to_vec();
177        let norm: f64 = w_coeffs.iter().map(|x| x * x).sum::<f64>().sqrt();
178        if norm < 1e-15 {
179            break;
180        }
181        v = &w * (1.0 / norm);
182    }
183
184    // Compute the eigenvalue via Rayleigh quotient on original matrix
185    let av = matrix.apply(&v)?;
186    let v_coeffs = v.to_vec();
187    let av_coeffs = av.to_vec();
188    let numerator: f64 = v_coeffs
189        .iter()
190        .zip(av_coeffs.iter())
191        .map(|(a, b)| a * b)
192        .sum();
193    let denominator: f64 = v_coeffs.iter().map(|x| x * x).sum();
194
195    let eigenvalue = if denominator.abs() < 1e-15 {
196        shift
197    } else {
198        numerator / denominator
199    };
200
201    Ok(Eigenpair::new(eigenvalue, v))
202}
203
204/// Compute all eigenvalues of a symmetric matrix.
205///
206/// Uses the Jacobi eigenvalue algorithm for symmetric matrices.
207///
208/// # Arguments
209///
210/// * `matrix` - A symmetric matrix operator
211/// * `max_iterations` - Maximum number of Jacobi rotations
212/// * `tolerance` - Convergence tolerance
213///
214/// # Returns
215///
216/// A vector of eigenvalues sorted by magnitude.
217pub fn compute_eigenvalues<const P: usize, const Q: usize, const R: usize>(
218    matrix: &MatrixOperator<P, Q, R>,
219    max_iterations: usize,
220    tolerance: f64,
221) -> Result<Vec<Eigenvalue>> {
222    if !matrix.is_symmetric(tolerance) {
223        return Err(FunctionalError::invalid_parameters(
224            "compute_eigenvalues requires a symmetric matrix",
225        ));
226    }
227
228    let n = matrix.rows();
229    let mut a = matrix.clone();
230
231    // Jacobi eigenvalue algorithm
232    for _ in 0..max_iterations {
233        // Find largest off-diagonal element
234        let mut max_val = 0.0;
235        let mut p = 0;
236        let mut q = 1;
237
238        for i in 0..n {
239            for j in (i + 1)..n {
240                let val = a.get(i, j).abs();
241                if val > max_val {
242                    max_val = val;
243                    p = i;
244                    q = j;
245                }
246            }
247        }
248
249        // Check convergence
250        if max_val < tolerance {
251            break;
252        }
253
254        // Compute Jacobi rotation angle
255        let app = a.get(p, p);
256        let aqq = a.get(q, q);
257        let apq = a.get(p, q);
258
259        let theta = if (aqq - app).abs() < 1e-15 {
260            std::f64::consts::FRAC_PI_4
261        } else {
262            0.5 * ((2.0 * apq) / (aqq - app)).atan()
263        };
264
265        let c = theta.cos();
266        let s = theta.sin();
267
268        // Apply Jacobi rotation
269        a = apply_jacobi_rotation(&a, p, q, c, s)?;
270    }
271
272    // Extract eigenvalues from diagonal
273    let mut eigenvalues: Vec<Eigenvalue> = (0..n).map(|i| Eigenvalue::new(a.get(i, i))).collect();
274
275    // Sort by absolute value (descending)
276    eigenvalues.sort_by(|a, b| {
277        b.value
278            .abs()
279            .partial_cmp(&a.value.abs())
280            .unwrap_or(std::cmp::Ordering::Equal)
281    });
282
283    Ok(eigenvalues)
284}
285
286/// Apply a Jacobi rotation to a matrix.
287fn apply_jacobi_rotation<const P: usize, const Q: usize, const R: usize>(
288    a: &MatrixOperator<P, Q, R>,
289    p: usize,
290    q: usize,
291    c: f64,
292    s: f64,
293) -> Result<MatrixOperator<P, Q, R>> {
294    let n = a.rows();
295    let mut result = a.clone();
296
297    for i in 0..n {
298        if i != p && i != q {
299            let aip = a.get(i, p);
300            let aiq = a.get(i, q);
301            result.set(i, p, c * aip - s * aiq);
302            result.set(p, i, c * aip - s * aiq);
303            result.set(i, q, s * aip + c * aiq);
304            result.set(q, i, s * aip + c * aiq);
305        }
306    }
307
308    let app = a.get(p, p);
309    let aqq = a.get(q, q);
310    let apq = a.get(p, q);
311
312    result.set(p, p, c * c * app - 2.0 * c * s * apq + s * s * aqq);
313    result.set(q, q, s * s * app + 2.0 * c * s * apq + c * c * aqq);
314    result.set(p, q, 0.0);
315    result.set(q, p, 0.0);
316
317    Ok(result)
318}
319
320/// Solve a linear system Ax = b using Gauss-Seidel iteration.
321fn solve_linear_system<const P: usize, const Q: usize, const R: usize>(
322    a: &MatrixOperator<P, Q, R>,
323    b: &Multivector<P, Q, R>,
324    max_iterations: usize,
325    _tolerance: f64,
326) -> Result<Multivector<P, Q, R>> {
327    let n = a.rows();
328    let b_coeffs = b.to_vec();
329    let mut x = vec![0.0; n];
330
331    for _ in 0..max_iterations {
332        for i in 0..n {
333            let mut sum = b_coeffs[i];
334            for j in 0..n {
335                if i != j {
336                    sum -= a.get(i, j) * x[j];
337                }
338            }
339            let aii = a.get(i, i);
340            if aii.abs() > 1e-15 {
341                x[i] = sum / aii;
342            }
343        }
344    }
345
346    Ok(Multivector::<P, Q, R>::from_slice(&x))
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    #[test]
354    fn test_eigenvalue_creation() {
355        let e = Eigenvalue::new(3.0);
356        assert!((e.value - 3.0).abs() < 1e-10);
357        assert!(e.multiplicity.is_none());
358
359        let e2 = Eigenvalue::with_multiplicity(2.0, 2);
360        assert_eq!(e2.multiplicity, Some(2));
361    }
362
363    #[test]
364    fn test_power_method_identity() {
365        let id: MatrixOperator<2, 0, 0> = MatrixOperator::identity();
366        let result = power_method(&id, None, 100, 1e-10).unwrap();
367
368        // Eigenvalue of identity is 1
369        assert!((result.eigenvalue.value - 1.0).abs() < 0.1);
370    }
371
372    #[test]
373    fn test_power_method_diagonal() {
374        // Create a diagonal matrix with eigenvalues 4, 3, 2, 1
375        let diag: MatrixOperator<2, 0, 0> =
376            MatrixOperator::diagonal(&[4.0, 3.0, 2.0, 1.0]).unwrap();
377        let result = power_method(&diag, None, 100, 1e-10).unwrap();
378
379        // Dominant eigenvalue should be 4
380        assert!((result.eigenvalue.value - 4.0).abs() < 0.1);
381    }
382
383    #[test]
384    fn test_jacobi_eigenvalues_identity() {
385        let id: MatrixOperator<2, 0, 0> = MatrixOperator::identity();
386        let eigenvalues = compute_eigenvalues(&id, 100, 1e-10).unwrap();
387
388        // All eigenvalues of identity are 1
389        for e in &eigenvalues {
390            assert!((e.value - 1.0).abs() < 1e-8);
391        }
392    }
393
394    #[test]
395    fn test_jacobi_eigenvalues_diagonal() {
396        let diag: MatrixOperator<2, 0, 0> =
397            MatrixOperator::diagonal(&[4.0, 3.0, 2.0, 1.0]).unwrap();
398        let eigenvalues = compute_eigenvalues(&diag, 100, 1e-10).unwrap();
399
400        // Eigenvalues should be 4, 3, 2, 1 (sorted by magnitude descending)
401        assert!((eigenvalues[0].value - 4.0).abs() < 1e-8);
402        assert!((eigenvalues[1].value - 3.0).abs() < 1e-8);
403        assert!((eigenvalues[2].value - 2.0).abs() < 1e-8);
404        assert!((eigenvalues[3].value - 1.0).abs() < 1e-8);
405    }
406
407    #[test]
408    fn test_eigenpair_creation() {
409        let v = Multivector::<2, 0, 0>::from_slice(&[1.0, 0.0, 0.0, 0.0]);
410        let pair = Eigenpair::new(2.0, v);
411        assert!((pair.eigenvalue.value - 2.0).abs() < 1e-10);
412    }
413}