cjc_runtime/
sparse_eigen.rs1use crate::sparse::SparseCsr;
8use cjc_repro::kahan_sum_f64;
9
10#[derive(Debug, Clone)]
16pub struct LanczosResult {
17 pub eigenvalues: Vec<f64>,
19 pub eigenvectors: Vec<Vec<f64>>,
21}
22
23pub fn lanczos_eigsh(matrix: &SparseCsr, k: usize, max_iter: usize) -> LanczosResult {
38 let n = matrix.nrows;
39 assert_eq!(n, matrix.ncols, "lanczos_eigsh: matrix must be square");
40 assert!(k > 0 && k <= n, "lanczos_eigsh: k must be in 1..=n");
41
42 let iters = max_iter.min(n);
43
44 let mut v_prev = vec![0.0_f64; n];
46 let inv_sqrt_n = 1.0 / (n as f64).sqrt();
48 let mut v_curr = vec![inv_sqrt_n; n];
49
50 let mut alpha = Vec::with_capacity(iters); let mut beta = Vec::with_capacity(iters); for j in 0..iters {
55 let w_raw = matrix.matvec(&v_curr).unwrap();
57 let mut w = w_raw;
58
59 let dot_products: Vec<f64> = v_curr.iter().zip(w.iter())
61 .map(|(&a, &b)| a * b)
62 .collect();
63 let alpha_j = kahan_sum_f64(&dot_products);
64 alpha.push(alpha_j);
65
66 let beta_prev = if j > 0 { beta[j - 1] } else { 0.0 };
68 for i in 0..n {
69 w[i] -= alpha_j * v_curr[i] + beta_prev * v_prev[i];
70 }
71
72 let norm_sq: Vec<f64> = w.iter().map(|&x| x * x).collect();
74 let beta_j = kahan_sum_f64(&norm_sq).sqrt();
75
76 if beta_j < 1e-14 {
77 break;
79 }
80 beta.push(beta_j);
81
82 v_prev = v_curr;
84 v_curr = w.iter().map(|&x| x / beta_j).collect();
85 }
86
87 let (eigenvalues, _) = tridiagonal_qr(&alpha, &beta);
89
90 let mut sorted_evals: Vec<f64> = eigenvalues;
92 sorted_evals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
93
94 let k_actual = k.min(sorted_evals.len());
95 let top_k: Vec<f64> = sorted_evals[sorted_evals.len() - k_actual..].to_vec();
96
97 LanczosResult {
98 eigenvalues: top_k,
99 eigenvectors: vec![], }
101}
102
103#[derive(Debug, Clone)]
110pub struct ArnoldiResult {
111 pub eigenvalues_real: Vec<f64>,
113 pub eigenvalues_imag: Vec<f64>,
115}
116
117pub fn arnoldi_eigs(matrix: &SparseCsr, k: usize) -> ArnoldiResult {
128 let n = matrix.nrows;
129 assert_eq!(n, matrix.ncols, "arnoldi_eigs: matrix must be square");
130 let m = k.min(n);
131
132 let mut q: Vec<Vec<f64>> = Vec::with_capacity(m + 1);
134 let mut h: Vec<Vec<f64>> = vec![vec![0.0; m]; m + 1]; let mut q0 = vec![0.0_f64; n];
138 q0[0] = 1.0;
139 q.push(q0);
140
141 for j in 0..m {
142 let mut w = matrix.matvec(&q[j]).unwrap();
144
145 for i in 0..=j {
147 let dots: Vec<f64> = q[i].iter().zip(w.iter())
148 .map(|(&a, &b)| a * b)
149 .collect();
150 h[i][j] = kahan_sum_f64(&dots);
151 for l in 0..n {
152 w[l] -= h[i][j] * q[i][l];
153 }
154 }
155
156 let norm_sq: Vec<f64> = w.iter().map(|&x| x * x).collect();
158 let h_next = kahan_sum_f64(&norm_sq).sqrt();
159
160 if j + 1 < m + 1 {
161 h[j + 1][j] = h_next;
162 }
163
164 if h_next < 1e-14 {
165 break;
166 }
167
168 let q_next: Vec<f64> = w.iter().map(|&x| x / h_next).collect();
170 q.push(q_next);
171 }
172
173 let actual_m = (q.len() - 1).min(m);
176 let eigenvalues_real: Vec<f64> = (0..actual_m).map(|i| h[i][i]).collect();
177 let eigenvalues_imag = vec![0.0_f64; actual_m];
178
179 ArnoldiResult {
180 eigenvalues_real,
181 eigenvalues_imag,
182 }
183}
184
185fn tridiagonal_qr(diag: &[f64], offdiag: &[f64]) -> (Vec<f64>, usize) {
198 let n = diag.len();
199 if n == 0 {
200 return (vec![], 0);
201 }
202 if n == 1 {
203 return (vec![diag[0]], 0);
204 }
205
206 let mut d = diag.to_vec();
207 let mut e = offdiag.to_vec();
208 while e.len() < n {
210 e.push(0.0);
211 }
212
213 let max_iter = 30 * n;
214 let mut total_iters = 0;
215
216 for l_start in 0..n {
218 let mut iters_this = 0;
219 loop {
220 let mut m = l_start;
222 while m < n - 1 {
223 let tst = d[m].abs() + d[m + 1].abs();
224 if e[m].abs() <= 1e-15 * tst {
225 break;
226 }
227 m += 1;
228 }
229 if m == l_start {
230 break; }
232
233 iters_this += 1;
234 total_iters += 1;
235 if iters_this > max_iter {
236 break;
237 }
238
239 let mut g = (d[l_start + 1] - d[l_start]) / (2.0 * e[l_start]);
241 let r = g.hypot(1.0);
242 g = d[m] - d[l_start] + e[l_start] / (g + r.copysign(g));
243
244 let mut s = 1.0;
245 let mut c = 1.0;
246 let mut p = 0.0;
247 let mut early_break = false;
248
249 for i in (l_start..m).rev() {
251 let f = s * e[i];
252 let b = c * e[i];
253 let r = f.hypot(g);
254 e[i + 1] = r;
255 if r.abs() < 1e-30 {
256 d[i + 1] -= p;
258 e[m] = 0.0;
259 early_break = true;
260 break;
261 }
262 s = f / r;
263 c = g / r;
264 g = d[i + 1] - p;
265 let r2 = (d[i] - g) * s + 2.0 * c * b;
266 p = s * r2;
267 d[i + 1] = g + p;
268 g = c * r2 - b;
269 }
270
271 if !early_break {
272 d[l_start] -= p;
273 e[l_start] = g;
274 e[m] = 0.0;
275 }
276 }
277 }
278
279 (d, total_iters)
280}
281
282#[cfg(test)]
287mod tests {
288 use super::*;
289 use crate::sparse::SparseCoo;
290
291 fn identity_csr(n: usize) -> SparseCsr {
292 let mut values = Vec::new();
293 let mut row_indices = Vec::new();
294 let mut col_indices = Vec::new();
295 for i in 0..n {
296 values.push(1.0);
297 row_indices.push(i);
298 col_indices.push(i);
299 }
300 let coo = SparseCoo::new(values, row_indices, col_indices, n, n);
301 SparseCsr::from_coo(&coo)
302 }
303
304 fn diagonal_csr(diag: &[f64]) -> SparseCsr {
305 let n = diag.len();
306 let mut values = Vec::new();
307 let mut row_indices = Vec::new();
308 let mut col_indices = Vec::new();
309 for (i, &d) in diag.iter().enumerate() {
310 values.push(d);
311 row_indices.push(i);
312 col_indices.push(i);
313 }
314 let coo = SparseCoo::new(values, row_indices, col_indices, n, n);
315 SparseCsr::from_coo(&coo)
316 }
317
318 #[test]
319 fn test_lanczos_identity() {
320 let mat = identity_csr(5);
321 let result = lanczos_eigsh(&mat, 3, 20);
322 for &ev in &result.eigenvalues {
324 assert!((ev - 1.0).abs() < 1e-10, "expected 1.0, got {ev}");
325 }
326 }
327
328 #[test]
329 fn test_lanczos_diagonal() {
330 let mat = diagonal_csr(&[1.0, 2.0, 3.0, 4.0, 5.0]);
331 let result = lanczos_eigsh(&mat, 2, 50);
332 assert!(result.eigenvalues.len() >= 2);
334 let top = &result.eigenvalues;
335 assert!((top[top.len() - 1] - 5.0).abs() < 1e-8,
336 "expected 5.0, got {}", top[top.len() - 1]);
337 }
338
339 #[test]
340 fn test_arnoldi_identity() {
341 let mat = identity_csr(4);
342 let result = arnoldi_eigs(&mat, 4);
343 for &ev in &result.eigenvalues_real {
345 assert!((ev - 1.0).abs() < 1e-10, "expected ~1.0, got {ev}");
346 }
347 }
348
349 #[test]
350 fn test_tridiagonal_qr_simple() {
351 let (evals, _) = tridiagonal_qr(&[3.0, 3.0], &[1.0]);
353 let mut sorted = evals.clone();
354 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
355 assert!((sorted[0] - 2.0).abs() < 1e-10, "expected 2.0, got {}", sorted[0]);
356 assert!((sorted[1] - 4.0).abs() < 1e-10, "expected 4.0, got {}", sorted[1]);
357 }
358
359 #[test]
360 fn test_lanczos_deterministic() {
361 let mat = diagonal_csr(&[1.0, 3.0, 5.0, 7.0, 9.0]);
362 let r1 = lanczos_eigsh(&mat, 3, 30);
363 let r2 = lanczos_eigsh(&mat, 3, 30);
364 assert_eq!(r1.eigenvalues.len(), r2.eigenvalues.len());
365 for (a, b) in r1.eigenvalues.iter().zip(r2.eigenvalues.iter()) {
366 assert_eq!(a.to_bits(), b.to_bits(), "non-deterministic eigenvalue");
367 }
368 }
369}