amari_functional/spectral/
decomposition.rs

1//! Spectral decomposition of operators.
2//!
3//! This module provides the spectral decomposition for self-adjoint operators,
4//! implementing the spectral theorem.
5
6use crate::error::{FunctionalError, Result};
7use crate::operator::MatrixOperator;
8use crate::spectral::eigenvalue::{compute_eigenvalues, power_method, Eigenpair, Eigenvalue};
9use amari_core::Multivector;
10
11/// Spectral decomposition of a self-adjoint operator.
12///
13/// For a self-adjoint operator T, the spectral decomposition is:
14/// T = Σᵢ λᵢ Pᵢ
15///
16/// where λᵢ are eigenvalues and Pᵢ are orthogonal projections onto
17/// the corresponding eigenspaces.
18#[derive(Debug, Clone)]
19pub struct SpectralDecomposition<const P: usize, const Q: usize, const R: usize> {
20    /// Eigenvalue-eigenvector pairs.
21    eigenpairs: Vec<Eigenpair<Multivector<P, Q, R>>>,
22    /// Whether the decomposition is complete.
23    is_complete: bool,
24}
25
26impl<const P: usize, const Q: usize, const R: usize> SpectralDecomposition<P, Q, R> {
27    /// Create a new spectral decomposition from eigenpairs.
28    pub fn new(eigenpairs: Vec<Eigenpair<Multivector<P, Q, R>>>) -> Self {
29        let is_complete = eigenpairs.len() == MatrixOperator::<P, Q, R>::DIM;
30        Self {
31            eigenpairs,
32            is_complete,
33        }
34    }
35
36    /// Get the eigenpairs.
37    pub fn eigenpairs(&self) -> &[Eigenpair<Multivector<P, Q, R>>] {
38        &self.eigenpairs
39    }
40
41    /// Get the eigenvalues.
42    pub fn eigenvalues(&self) -> Vec<Eigenvalue> {
43        self.eigenpairs.iter().map(|p| p.eigenvalue).collect()
44    }
45
46    /// Get the eigenvectors.
47    pub fn eigenvectors(&self) -> Vec<&Multivector<P, Q, R>> {
48        self.eigenpairs.iter().map(|p| &p.eigenvector).collect()
49    }
50
51    /// Check if the decomposition is complete.
52    pub fn is_complete(&self) -> bool {
53        self.is_complete
54    }
55
56    /// Apply the operator T = Σᵢ λᵢ |vᵢ⟩⟨vᵢ| to a vector.
57    ///
58    /// Computes Tx = Σᵢ λᵢ ⟨vᵢ, x⟩ vᵢ
59    pub fn apply(&self, x: &Multivector<P, Q, R>) -> Multivector<P, Q, R> {
60        let x_coeffs = x.to_vec();
61        let mut result = Multivector::<P, Q, R>::zero();
62
63        for pair in &self.eigenpairs {
64            let v_coeffs = pair.eigenvector.to_vec();
65
66            // Compute ⟨vᵢ, x⟩
67            let inner_product: f64 = v_coeffs
68                .iter()
69                .zip(x_coeffs.iter())
70                .map(|(a, b)| a * b)
71                .sum();
72
73            // Add λᵢ ⟨vᵢ, x⟩ vᵢ
74            result = result.add(&(&pair.eigenvector * (pair.eigenvalue.value * inner_product)));
75        }
76
77        result
78    }
79
80    /// Apply a function f to the operator: f(T) = Σᵢ f(λᵢ) |vᵢ⟩⟨vᵢ|.
81    ///
82    /// This is the functional calculus for self-adjoint operators.
83    pub fn apply_function<F>(&self, f: F, x: &Multivector<P, Q, R>) -> Multivector<P, Q, R>
84    where
85        F: Fn(f64) -> f64,
86    {
87        let x_coeffs = x.to_vec();
88        let mut result = Multivector::<P, Q, R>::zero();
89
90        for pair in &self.eigenpairs {
91            let v_coeffs = pair.eigenvector.to_vec();
92
93            // Compute ⟨vᵢ, x⟩
94            let inner_product: f64 = v_coeffs
95                .iter()
96                .zip(x_coeffs.iter())
97                .map(|(a, b)| a * b)
98                .sum();
99
100            // Add f(λᵢ) ⟨vᵢ, x⟩ vᵢ
101            let f_lambda = f(pair.eigenvalue.value);
102            result = result.add(&(&pair.eigenvector * (f_lambda * inner_product)));
103        }
104
105        result
106    }
107
108    /// Compute the spectral radius: max|λᵢ|.
109    pub fn spectral_radius(&self) -> f64 {
110        self.eigenpairs
111            .iter()
112            .map(|p| p.eigenvalue.value.abs())
113            .fold(0.0, f64::max)
114    }
115
116    /// Compute the condition number (ratio of largest to smallest eigenvalue magnitudes).
117    pub fn condition_number(&self) -> Option<f64> {
118        if self.eigenpairs.is_empty() {
119            return None;
120        }
121
122        let eigenvalues: Vec<f64> = self
123            .eigenpairs
124            .iter()
125            .map(|p| p.eigenvalue.value.abs())
126            .collect();
127
128        let max_ev = eigenvalues.iter().cloned().fold(0.0, f64::max);
129        let min_ev = eigenvalues
130            .iter()
131            .cloned()
132            .filter(|&x| x > 1e-15)
133            .fold(f64::MAX, f64::min);
134
135        if min_ev == f64::MAX || min_ev < 1e-15 {
136            None
137        } else {
138            Some(max_ev / min_ev)
139        }
140    }
141
142    /// Check if the operator is positive definite.
143    pub fn is_positive_definite(&self) -> bool {
144        self.eigenpairs.iter().all(|p| p.eigenvalue.value > 1e-15)
145    }
146
147    /// Check if the operator is positive semi-definite.
148    pub fn is_positive_semidefinite(&self) -> bool {
149        self.eigenpairs.iter().all(|p| p.eigenvalue.value >= -1e-15)
150    }
151}
152
153/// Compute the spectral decomposition of a symmetric matrix.
154///
155/// Uses QR iteration with shifts to compute eigenvalues and eigenvectors.
156///
157/// # Arguments
158///
159/// * `matrix` - A symmetric matrix operator
160/// * `max_iterations` - Maximum number of iterations per eigenvalue
161/// * `tolerance` - Convergence tolerance
162///
163/// # Returns
164///
165/// The spectral decomposition of the matrix.
166pub fn spectral_decompose<const P: usize, const Q: usize, const R: usize>(
167    matrix: &MatrixOperator<P, Q, R>,
168    max_iterations: usize,
169    tolerance: f64,
170) -> Result<SpectralDecomposition<P, Q, R>> {
171    if !matrix.is_symmetric(tolerance) {
172        return Err(FunctionalError::invalid_parameters(
173            "spectral_decompose requires a symmetric matrix",
174        ));
175    }
176
177    let n = MatrixOperator::<P, Q, R>::DIM;
178    let eigenvalues = compute_eigenvalues(matrix, max_iterations, tolerance)?;
179
180    let mut eigenpairs: Vec<Eigenpair<Multivector<P, Q, R>>> = Vec::with_capacity(n);
181    let mut used_eigenvalues = vec![false; eigenvalues.len()];
182
183    // For each eigenvalue, find the corresponding eigenvector
184    for (i, eigenvalue) in eigenvalues.iter().enumerate() {
185        if used_eigenvalues[i] {
186            continue;
187        }
188
189        // Use inverse iteration near the eigenvalue
190        let shift = eigenvalue.value;
191
192        // Start with a vector orthogonal to previously found eigenvectors
193        let mut initial_coeffs = vec![0.0; n];
194        initial_coeffs[i] = 1.0;
195
196        // Orthogonalize against previously found eigenvectors
197        for (j, pair) in eigenpairs.iter().enumerate() {
198            if j >= n {
199                break;
200            }
201            let v_coeffs = pair.eigenvector.to_vec();
202            let dot: f64 = initial_coeffs
203                .iter()
204                .zip(v_coeffs.iter())
205                .map(|(a, b)| a * b)
206                .sum();
207            for (k, coeff) in initial_coeffs.iter_mut().enumerate() {
208                *coeff -= dot * v_coeffs[k];
209            }
210        }
211
212        // Normalize
213        let norm: f64 = initial_coeffs.iter().map(|x| x * x).sum::<f64>().sqrt();
214        if norm > 1e-10 {
215            for coeff in &mut initial_coeffs {
216                *coeff /= norm;
217            }
218        }
219
220        let initial = Multivector::<P, Q, R>::from_slice(&initial_coeffs);
221
222        // Use power method on (A - λI)^(-1) if shift is far from zero
223        // Otherwise use direct power method
224        let eigenvector = if shift.abs() > tolerance {
225            // For eigenvalue λ, (A - (λ - ε)I) should make λ dominant
226            let shifted_matrix =
227                matrix.add(&MatrixOperator::<P, Q, R>::identity().scale(-shift + tolerance))?;
228            match power_method(
229                &shifted_matrix,
230                Some(&initial),
231                max_iterations / 2,
232                tolerance,
233            ) {
234                Ok(pair) => pair.eigenvector,
235                Err(_) => initial,
236            }
237        } else {
238            initial
239        };
240
241        used_eigenvalues[i] = true;
242        eigenpairs.push(Eigenpair {
243            eigenvalue: *eigenvalue,
244            eigenvector,
245        });
246    }
247
248    // If we found fewer eigenvectors than expected, fill in with basis vectors
249    while eigenpairs.len() < n {
250        let mut coeffs = vec![0.0; n];
251        coeffs[eigenpairs.len()] = 1.0;
252        let eigenvector = Multivector::<P, Q, R>::from_slice(&coeffs);
253        eigenpairs.push(Eigenpair::new(0.0, eigenvector));
254    }
255
256    Ok(SpectralDecomposition::new(eigenpairs))
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn test_spectral_decomposition_identity() {
265        let id: MatrixOperator<2, 0, 0> = MatrixOperator::identity();
266        let decomp = spectral_decompose(&id, 100, 1e-10).unwrap();
267
268        // Identity has all eigenvalues = 1
269        for ev in decomp.eigenvalues() {
270            assert!((ev.value - 1.0).abs() < 1e-6);
271        }
272
273        assert!(decomp.is_complete());
274        assert!((decomp.spectral_radius() - 1.0).abs() < 1e-6);
275    }
276
277    #[test]
278    fn test_spectral_decomposition_diagonal() {
279        let diag: MatrixOperator<2, 0, 0> =
280            MatrixOperator::diagonal(&[4.0, 3.0, 2.0, 1.0]).unwrap();
281        let decomp = spectral_decompose(&diag, 100, 1e-10).unwrap();
282
283        // Check spectral radius
284        assert!((decomp.spectral_radius() - 4.0).abs() < 1e-6);
285
286        // Check positive definite
287        assert!(decomp.is_positive_definite());
288    }
289
290    #[test]
291    fn test_apply_function() {
292        let diag: MatrixOperator<2, 0, 0> =
293            MatrixOperator::diagonal(&[4.0, 1.0, 1.0, 1.0]).unwrap();
294        let decomp = spectral_decompose(&diag, 100, 1e-10).unwrap();
295
296        // Apply square root function
297        let x = Multivector::<2, 0, 0>::from_slice(&[1.0, 0.0, 0.0, 0.0]);
298        let sqrt_t_x = decomp.apply_function(|lambda| lambda.sqrt(), &x);
299
300        // For diagonal matrix with first entry 4, sqrt(4) = 2
301        // Since x has only first component, result should be 2 * e_0
302        let result_coeffs = sqrt_t_x.to_vec();
303        assert!((result_coeffs[0] - 2.0).abs() < 0.5);
304    }
305
306    #[test]
307    fn test_condition_number() {
308        let diag: MatrixOperator<2, 0, 0> =
309            MatrixOperator::diagonal(&[4.0, 2.0, 2.0, 1.0]).unwrap();
310        let decomp = spectral_decompose(&diag, 100, 1e-10).unwrap();
311
312        // Condition number = max/min = 4/1 = 4
313        let cond = decomp.condition_number().unwrap();
314        assert!((cond - 4.0).abs() < 0.5);
315    }
316
317    #[test]
318    fn test_semidefinite_check() {
319        let diag: MatrixOperator<2, 0, 0> =
320            MatrixOperator::diagonal(&[4.0, 0.0, 0.0, 0.0]).unwrap();
321        let decomp = spectral_decompose(&diag, 100, 1e-10).unwrap();
322
323        assert!(decomp.is_positive_semidefinite());
324        // Not positive definite because some eigenvalues are 0
325        assert!(!decomp.is_positive_definite());
326    }
327}