oxicuda_sparse/preconditioner/
ick.rs1use crate::error::{SparseError, SparseResult};
28use crate::host_csr::HostCsr;
29
30#[derive(Debug, Clone)]
33pub struct IncompleteCholeskyK {
34 l: HostCsr,
36 level: usize,
38}
39
40impl IncompleteCholeskyK {
41 #[inline]
43 pub fn l_factor(&self) -> &HostCsr {
44 &self.l
45 }
46
47 #[inline]
49 pub fn level(&self) -> usize {
50 self.level
51 }
52
53 pub fn apply(&self, r: &[f64]) -> Vec<f64> {
64 let n = self.l.nrows;
65 let mut y = vec![0.0f64; n];
66
67 for i in 0..n {
69 let start = self.l.row_ptr[i];
70 let end = self.l.row_ptr[i + 1];
71 let mut sum = if i < r.len() { r[i] } else { 0.0 };
72 let mut diag = 1.0;
73 for kk in start..end {
74 let j = self.l.col_indices[kk];
75 if j < i {
76 sum -= self.l.values[kk] * y[j];
77 } else if j == i {
78 diag = self.l.values[kk];
79 }
80 }
81 y[i] = sum / diag;
82 }
83
84 let mut z = vec![0.0f64; n];
87 for i in (0..n).rev() {
88 let start = self.l.row_ptr[i];
89 let end = self.l.row_ptr[i + 1];
90 let mut diag = 1.0;
91 for kk in start..end {
92 if self.l.col_indices[kk] == i {
93 diag = self.l.values[kk];
94 }
95 }
96 z[i] = y[i] / diag;
97 for kk in start..end {
99 let j = self.l.col_indices[kk];
100 if j < i {
101 y[j] -= self.l.values[kk] * z[i];
102 }
103 }
104 }
105 z
106 }
107}
108
109pub fn ic_k(a: &HostCsr, k: usize) -> SparseResult<IncompleteCholeskyK> {
125 if a.nrows != a.ncols {
126 return Err(SparseError::DimensionMismatch(format!(
127 "IC(k) requires a square matrix, got {}x{}",
128 a.nrows, a.ncols
129 )));
130 }
131 let n = a.nrows;
132 if n == 0 {
133 return Err(SparseError::InvalidArgument(
134 "cannot factor an empty matrix".to_string(),
135 ));
136 }
137
138 let lower_pattern = ic_k_symbolic(a, k);
140
141 let values = ic_k_numeric(a, &lower_pattern, n)?;
143
144 let mut row_ptr = vec![0usize; n + 1];
145 let mut col_indices = Vec::new();
146 let mut out_values = Vec::new();
147 for i in 0..n {
148 for &(col, val) in &lower_pattern[i] {
149 col_indices.push(col);
150 out_values.push(values[&(i, col)]);
151 let _ = val;
152 }
153 row_ptr[i + 1] = col_indices.len();
154 }
155
156 let l = HostCsr::new(n, n, row_ptr, col_indices, out_values)?;
157 Ok(IncompleteCholeskyK { l, level: k })
158}
159
160#[derive(Clone, Copy)]
162struct LevEntry {
163 col: usize,
164 level: usize,
165}
166
167fn ic_k_symbolic(a: &HostCsr, k: usize) -> Vec<Vec<(usize, usize)>> {
174 let n = a.nrows;
175
176 let mut rows: Vec<Vec<LevEntry>> = Vec::with_capacity(n);
180 {
181 let mut sets: Vec<std::collections::BTreeMap<usize, usize>> =
182 vec![std::collections::BTreeMap::new(); n];
183 for i in 0..n {
184 let start = a.row_ptr[i];
185 let end = a.row_ptr[i + 1];
186 sets[i].insert(i, 0);
187 for kk in start..end {
188 let j = a.col_indices[kk];
189 sets[i].insert(j, 0);
190 sets[j].insert(i, 0);
191 }
192 }
193 for set in sets {
194 rows.push(
195 set.into_iter()
196 .map(|(col, level)| LevEntry { col, level })
197 .collect(),
198 );
199 }
200 }
201
202 for i in 0..n {
207 let mut idx = 0;
208 loop {
209 if idx >= rows[i].len() {
210 break;
211 }
212 let m = rows[i][idx].col;
213 if m >= i {
214 break;
215 }
216 let lev_im = rows[i][idx].level;
217
218 let upper_m: Vec<(usize, usize)> = rows[m]
222 .iter()
223 .filter(|e| e.col > m)
224 .map(|e| (e.col, e.level))
225 .collect();
226
227 for (j, lev_mj) in upper_m {
228 if j >= i {
229 if j != i {
233 continue;
234 }
235 }
236 let new_level = lev_im + lev_mj + 1;
237 if new_level > k {
238 continue;
239 }
240 match rows[i].iter().position(|e| e.col == j) {
241 Some(pos) => {
242 if new_level < rows[i][pos].level {
243 rows[i][pos].level = new_level;
244 }
245 }
246 None => {
247 let insert_pos = rows[i]
248 .iter()
249 .position(|e| e.col > j)
250 .unwrap_or(rows[i].len());
251 rows[i].insert(
252 insert_pos,
253 LevEntry {
254 col: j,
255 level: new_level,
256 },
257 );
258 }
259 }
260 }
261 idx += 1;
262 }
263 }
264
265 rows.into_iter()
267 .enumerate()
268 .map(|(i, row)| {
269 row.into_iter()
270 .filter(|e| e.col <= i)
271 .map(|e| (e.col, e.level))
272 .collect()
273 })
274 .collect()
275}
276
277fn ic_k_numeric(
281 a: &HostCsr,
282 pattern: &[Vec<(usize, usize)>],
283 n: usize,
284) -> SparseResult<std::collections::HashMap<(usize, usize), f64>> {
285 let mut l: std::collections::HashMap<(usize, usize), f64> = std::collections::HashMap::new();
288
289 let row_cols: Vec<Vec<usize>> = pattern
291 .iter()
292 .map(|row| row.iter().map(|&(c, _)| c).collect())
293 .collect();
294
295 for i in 0..n {
296 for &(j, _lev) in &pattern[i] {
297 if j > i {
298 continue;
299 }
300 let mut sum = a.get(i, j).unwrap_or(0.0);
302 let ci = &row_cols[i];
303 let cj = &row_cols[j];
304 let (mut pi, mut pj) = (0usize, 0usize);
305 while pi < ci.len() && pj < cj.len() {
306 let mi = ci[pi];
307 let mj = cj[pj];
308 if mi >= j || mj >= j {
309 break;
310 }
311 match mi.cmp(&mj) {
312 std::cmp::Ordering::Less => pi += 1,
313 std::cmp::Ordering::Greater => pj += 1,
314 std::cmp::Ordering::Equal => {
315 let lim = l.get(&(i, mi)).copied().unwrap_or(0.0);
316 let ljm = l.get(&(j, mj)).copied().unwrap_or(0.0);
317 sum -= lim * ljm;
318 pi += 1;
319 pj += 1;
320 }
321 }
322 }
323
324 if i == j {
325 if sum <= 0.0 {
326 return Err(SparseError::SingularMatrix);
327 }
328 l.insert((i, j), sum.sqrt());
329 } else {
330 let ljj = l.get(&(j, j)).copied().unwrap_or(0.0);
331 if ljj == 0.0 {
332 return Err(SparseError::SingularMatrix);
333 }
334 l.insert((i, j), sum / ljj);
335 }
336 }
337 }
338
339 Ok(l)
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345 use crate::host_csr::{HostCsr, laplacian_1d, laplacian_2d};
346
347 fn spd_dense_like(n: usize) -> HostCsr {
350 let mut b = vec![0.0f64; n * n];
352 let mut state: u64 = 12345;
353 for v in b.iter_mut() {
354 state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
355 *v = ((state >> 33) as f64 / (1u64 << 31) as f64) - 1.0;
356 }
357 let mut dense = vec![0.0f64; n * n];
358 for i in 0..n {
359 for j in 0..n {
360 let mut acc = 0.0;
361 for m in 0..n {
362 acc += b[m * n + i] * b[m * n + j];
363 }
364 if i == j {
365 acc += n as f64;
366 }
367 dense[i * n + j] = acc;
368 }
369 }
370 dense_to_csr(&dense, n)
371 }
372
373 fn dense_to_csr(dense: &[f64], n: usize) -> HostCsr {
374 let mut row_ptr = vec![0usize; n + 1];
375 let mut col_indices = Vec::new();
376 let mut values = Vec::new();
377 for i in 0..n {
378 for j in 0..n {
379 let v = dense[i * n + j];
380 if v != 0.0 {
381 col_indices.push(j);
382 values.push(v);
383 }
384 }
385 row_ptr[i + 1] = col_indices.len();
386 }
387 HostCsr::new(n, n, row_ptr, col_indices, values).expect("valid")
388 }
389
390 fn llt(l: &HostCsr, i: usize, j: usize) -> f64 {
392 let ci_s = l.row_ptr[i];
394 let ci_e = l.row_ptr[i + 1];
395 let cj_s = l.row_ptr[j];
396 let cj_e = l.row_ptr[j + 1];
397 let mut acc = 0.0;
398 let (mut pi, mut pj) = (ci_s, cj_s);
399 while pi < ci_e && pj < cj_e {
400 let a = l.col_indices[pi];
401 let b = l.col_indices[pj];
402 match a.cmp(&b) {
403 std::cmp::Ordering::Less => pi += 1,
404 std::cmp::Ordering::Greater => pj += 1,
405 std::cmp::Ordering::Equal => {
406 acc += l.values[pi] * l.values[pj];
407 pi += 1;
408 pj += 1;
409 }
410 }
411 }
412 acc
413 }
414
415 #[test]
416 fn llt_matches_a_on_pattern_ic0() {
417 let a = laplacian_1d(8);
420 let fac = ic_k(&a, 0).expect("ic0");
421 let l = fac.l_factor();
422 for i in 0..l.nrows {
423 let s = l.row_ptr[i];
424 let e = l.row_ptr[i + 1];
425 for kk in s..e {
426 let j = l.col_indices[kk];
427 let recon = llt(l, i, j);
428 let aij = a.get(i, j).unwrap_or(0.0);
429 assert!(
430 (recon - aij).abs() < 1e-12,
431 "IC(0) pattern mismatch at ({i},{j}): {recon} vs {aij}"
432 );
433 }
434 }
435 }
436
437 #[test]
438 fn fill_pattern_monotone() {
439 let a = laplacian_2d(5, 5);
441 let p0 = ic_k_symbolic(&a, 0);
442 let p1 = ic_k_symbolic(&a, 1);
443 let p2 = ic_k_symbolic(&a, 2);
444 for i in 0..a.nrows {
445 let s0: std::collections::HashSet<usize> = p0[i].iter().map(|&(c, _)| c).collect();
446 let s1: std::collections::HashSet<usize> = p1[i].iter().map(|&(c, _)| c).collect();
447 let s2: std::collections::HashSet<usize> = p2[i].iter().map(|&(c, _)| c).collect();
448 assert!(s0.is_subset(&s1), "IC(0) not subset of IC(1) at row {i}");
449 assert!(s1.is_subset(&s2), "IC(1) not subset of IC(2) at row {i}");
450 }
451 }
452
453 #[test]
454 fn complete_fill_reconstructs_exactly() {
455 let n = 10;
457 let a = spd_dense_like(n);
458 let fac = ic_k(&a, n + 5).expect("complete cholesky");
459 let l = fac.l_factor();
460 for i in 0..n {
461 for j in 0..n {
462 let recon = llt(l, i, j);
463 let aij = a.get(i, j).unwrap_or(0.0);
464 assert!(
465 (recon - aij).abs() < 1e-9,
466 "complete reconstruction mismatch at ({i},{j}): {recon} vs {aij}"
467 );
468 }
469 }
470 }
471
472 #[test]
473 fn apply_is_exact_inverse_for_complete_factor() {
474 let n = 9;
476 let a = spd_dense_like(n);
477 let fac = ic_k(&a, n + 5).expect("complete");
478 let r: Vec<f64> = (0..n).map(|i| 1.0 + i as f64 * 0.3).collect();
479 let z = fac.apply(&r);
480 let az = a.matvec(&z);
481 for i in 0..n {
482 assert!(
483 (az[i] - r[i]).abs() < 1e-8,
484 "A·apply(r) != r at {i}: {} vs {}",
485 az[i],
486 r[i]
487 );
488 }
489 }
490
491 #[test]
492 fn apply_solves_laplacian_with_complete_fill() {
493 let n = 12;
495 let a = laplacian_1d(n);
496 let fac = ic_k(&a, n + 1).expect("complete");
497 let r = vec![1.0f64; n];
498 let z = fac.apply(&r);
499 let az = a.matvec(&z);
500 for i in 0..n {
501 assert!((az[i] - r[i]).abs() < 1e-9);
502 }
503 }
504
505 #[test]
506 fn non_spd_errors() {
507 let a = HostCsr::new(
509 2,
510 2,
511 vec![0, 2, 4],
512 vec![0, 1, 0, 1],
513 vec![1.0, 2.0, 2.0, 1.0],
514 )
515 .expect("valid");
516 assert!(matches!(ic_k(&a, 5), Err(SparseError::SingularMatrix)));
517 }
518
519 #[test]
520 fn rejects_non_square() {
521 let a = HostCsr::new(2, 3, vec![0, 1, 2], vec![0, 1], vec![1.0, 1.0]).expect("valid");
522 assert!(matches!(
523 ic_k(&a, 0),
524 Err(SparseError::DimensionMismatch(_))
525 ));
526 }
527
528 #[test]
529 fn ic0_pattern_equals_lower_triangle() {
530 let a = laplacian_2d(4, 4);
532 let p0 = ic_k_symbolic(&a, 0);
533 for (i, prow) in p0.iter().enumerate() {
534 let cols: Vec<usize> = prow.iter().map(|&(c, _)| c).collect();
535 let mut expected: Vec<usize> = Vec::new();
537 let s = a.row_ptr[i];
538 let e = a.row_ptr[i + 1];
539 for kk in s..e {
540 let j = a.col_indices[kk];
541 if j <= i {
542 expected.push(j);
543 }
544 }
545 if !expected.contains(&i) {
546 expected.push(i);
547 }
548 expected.sort_unstable();
549 assert_eq!(cols, expected, "row {i}");
550 }
551 }
552}