amari_functional/spectral/
decomposition.rs1use crate::error::{FunctionalError, Result};
7use crate::operator::MatrixOperator;
8use crate::spectral::eigenvalue::{compute_eigenvalues, power_method, Eigenpair, Eigenvalue};
9use amari_core::Multivector;
10
11#[derive(Debug, Clone)]
19pub struct SpectralDecomposition<const P: usize, const Q: usize, const R: usize> {
20 eigenpairs: Vec<Eigenpair<Multivector<P, Q, R>>>,
22 is_complete: bool,
24}
25
26impl<const P: usize, const Q: usize, const R: usize> SpectralDecomposition<P, Q, R> {
27 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 pub fn eigenpairs(&self) -> &[Eigenpair<Multivector<P, Q, R>>] {
38 &self.eigenpairs
39 }
40
41 pub fn eigenvalues(&self) -> Vec<Eigenvalue> {
43 self.eigenpairs.iter().map(|p| p.eigenvalue).collect()
44 }
45
46 pub fn eigenvectors(&self) -> Vec<&Multivector<P, Q, R>> {
48 self.eigenpairs.iter().map(|p| &p.eigenvector).collect()
49 }
50
51 pub fn is_complete(&self) -> bool {
53 self.is_complete
54 }
55
56 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 let inner_product: f64 = v_coeffs
68 .iter()
69 .zip(x_coeffs.iter())
70 .map(|(a, b)| a * b)
71 .sum();
72
73 result = result.add(&(&pair.eigenvector * (pair.eigenvalue.value * inner_product)));
75 }
76
77 result
78 }
79
80 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 let inner_product: f64 = v_coeffs
95 .iter()
96 .zip(x_coeffs.iter())
97 .map(|(a, b)| a * b)
98 .sum();
99
100 let f_lambda = f(pair.eigenvalue.value);
102 result = result.add(&(&pair.eigenvector * (f_lambda * inner_product)));
103 }
104
105 result
106 }
107
108 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 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 pub fn is_positive_definite(&self) -> bool {
144 self.eigenpairs.iter().all(|p| p.eigenvalue.value > 1e-15)
145 }
146
147 pub fn is_positive_semidefinite(&self) -> bool {
149 self.eigenpairs.iter().all(|p| p.eigenvalue.value >= -1e-15)
150 }
151}
152
153pub 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 (i, eigenvalue) in eigenvalues.iter().enumerate() {
185 if used_eigenvalues[i] {
186 continue;
187 }
188
189 let shift = eigenvalue.value;
191
192 let mut initial_coeffs = vec![0.0; n];
194 initial_coeffs[i] = 1.0;
195
196 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 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 let eigenvector = if shift.abs() > tolerance {
225 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 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 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 assert!((decomp.spectral_radius() - 4.0).abs() < 1e-6);
285
286 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 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 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 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 assert!(!decomp.is_positive_definite());
326 }
327}