1#![allow(dead_code)]
19
20use oxicuda_blas::GpuFloat;
21
22use crate::error::{SparseError, SparseResult};
23use crate::format::CsrMatrix;
24use crate::handle::SparseHandle;
25
26fn to_f64<T: GpuFloat>(val: T) -> f64 {
31 if T::SIZE == 4 {
32 f32::from_bits(val.to_bits_u64() as u32) as f64
33 } else {
34 f64::from_bits(val.to_bits_u64())
35 }
36}
37
38fn from_f64<T: GpuFloat>(val: f64) -> T {
39 if T::SIZE == 4 {
40 T::from_bits_u64(u64::from((val as f32).to_bits()))
41 } else {
42 T::from_bits_u64(val.to_bits())
43 }
44}
45
46fn div_gpu_float<T: GpuFloat>(a: T, b: T) -> T {
47 from_f64::<T>(to_f64(a) / to_f64(b))
48}
49
50fn mul_gpu_float<T: GpuFloat>(a: T, b: T) -> T {
51 from_f64::<T>(to_f64(a) * to_f64(b))
52}
53
54fn sub_gpu_float<T: GpuFloat>(a: T, b: T) -> T {
55 from_f64::<T>(to_f64(a) - to_f64(b))
56}
57
58#[derive(Default)]
64pub struct IlukConfig {
65 pub fill_level: usize,
67}
68
69pub struct IlukFactorization<T: GpuFloat> {
79 pub lu: CsrMatrix<T>,
83 pub diag_inv: Vec<T>,
85 pub fill_level: usize,
87}
88
89struct SymbolicEntry {
95 col: usize,
96 level: usize,
97}
98
99fn iluk_symbolic(
104 row_ptr: &[i32],
105 col_idx: &[i32],
106 n: usize,
107 fill_level: usize,
108) -> SparseResult<(Vec<i32>, Vec<i32>, Vec<usize>)> {
109 let mut rows: Vec<Vec<SymbolicEntry>> = Vec::with_capacity(n);
111
112 for i in 0..n {
113 let start = row_ptr[i] as usize;
114 let end = row_ptr[i + 1] as usize;
115 let mut row_entries: Vec<SymbolicEntry> = Vec::with_capacity(end - start);
116 for &cj in &col_idx[start..end] {
117 row_entries.push(SymbolicEntry {
118 col: cj as usize,
119 level: 0,
120 });
121 }
122 row_entries.sort_by_key(|e| e.col);
124 rows.push(row_entries);
125 }
126
127 for i in 0..n {
129 let mut k_idx = 0;
131 loop {
132 if k_idx >= rows[i].len() {
133 break;
134 }
135 let k = rows[i][k_idx].col;
136 if k >= i {
137 break;
138 }
139 let lev_ik = rows[i][k_idx].level;
140
141 let diag_pos = rows[k].iter().position(|e| e.col == k);
143 if diag_pos.is_none() {
144 k_idx += 1;
145 continue;
146 }
147
148 let row_k_entries: Vec<(usize, usize)> = rows[k]
150 .iter()
151 .filter(|e| e.col > k)
152 .map(|e| (e.col, e.level))
153 .collect();
154
155 for (j, lev_kj) in row_k_entries {
156 let new_level = lev_ik + lev_kj + 1;
157 if new_level > fill_level {
158 continue;
159 }
160
161 let existing = rows[i].iter().position(|e| e.col == j);
163 match existing {
164 Some(pos) => {
165 if new_level < rows[i][pos].level {
167 rows[i][pos].level = new_level;
168 }
169 }
170 None => {
171 let insert_pos = rows[i]
173 .iter()
174 .position(|e| e.col > j)
175 .unwrap_or(rows[i].len());
176 rows[i].insert(
177 insert_pos,
178 SymbolicEntry {
179 col: j,
180 level: new_level,
181 },
182 );
183 }
184 }
185 }
186
187 k_idx += 1;
188 }
189 }
190
191 let mut out_row_ptr = vec![0i32; n + 1];
193 let mut out_col_idx = Vec::new();
194 let mut out_levels = Vec::new();
195
196 for (i, row_entries) in rows.iter().enumerate() {
197 for entry in row_entries {
198 out_col_idx.push(entry.col as i32);
199 out_levels.push(entry.level);
200 }
201 out_row_ptr[i + 1] = out_col_idx.len() as i32;
202 }
203
204 Ok((out_row_ptr, out_col_idx, out_levels))
205}
206
207fn iluk_numeric<T: GpuFloat>(
218 sym_row_ptr: &[i32],
219 sym_col_idx: &[i32],
220 orig_row_ptr: &[i32],
221 orig_col_idx: &[i32],
222 orig_values: &[T],
223 n: usize,
224) -> SparseResult<Vec<T>> {
225 let nnz = sym_col_idx.len();
226 let mut values = vec![T::gpu_zero(); nnz];
227
228 for i in 0..n {
230 let orig_start = orig_row_ptr[i] as usize;
231 let orig_end = orig_row_ptr[i + 1] as usize;
232 let sym_start = sym_row_ptr[i] as usize;
233 let sym_end = sym_row_ptr[i + 1] as usize;
234
235 let mut sym_k = sym_start;
236 for orig_k in orig_start..orig_end {
237 let col = orig_col_idx[orig_k];
238 while sym_k < sym_end && sym_col_idx[sym_k] < col {
240 sym_k += 1;
241 }
242 if sym_k < sym_end && sym_col_idx[sym_k] == col {
243 values[sym_k] = orig_values[orig_k];
244 }
245 }
246 }
247
248 for i in 0..n {
250 let row_start = sym_row_ptr[i] as usize;
251 let row_end = sym_row_ptr[i + 1] as usize;
252
253 for nz in row_start..row_end {
255 let k = sym_col_idx[nz] as usize;
256 if k >= i {
257 break;
258 }
259
260 let k_start = sym_row_ptr[k] as usize;
262 let k_end = sym_row_ptr[k + 1] as usize;
263 let diag_pos = find_col_in_row(&sym_col_idx[k_start..k_end], k as i32);
264 let diag_pos = match diag_pos {
265 Some(pos) => k_start + pos,
266 None => return Err(SparseError::SingularMatrix),
267 };
268
269 let a_kk = values[diag_pos];
270 if a_kk == T::gpu_zero() {
271 return Err(SparseError::SingularMatrix);
272 }
273
274 let ratio = div_gpu_float(values[nz], a_kk);
276 values[nz] = ratio;
277
278 for k_nz in (diag_pos + 1)..k_end {
280 let j = sym_col_idx[k_nz];
281 if let Some(ij_off) = find_col_in_row(&sym_col_idx[row_start..row_end], j) {
282 let ij_pos = row_start + ij_off;
283 let update = mul_gpu_float(ratio, values[k_nz]);
284 values[ij_pos] = sub_gpu_float(values[ij_pos], update);
285 }
286 }
287 }
288 }
289
290 Ok(values)
291}
292
293impl<T: GpuFloat> IlukFactorization<T> {
298 pub fn compute(
311 _handle: &SparseHandle,
312 matrix: &CsrMatrix<T>,
313 config: &IlukConfig,
314 ) -> SparseResult<Self> {
315 if matrix.rows() != matrix.cols() {
316 return Err(SparseError::DimensionMismatch(format!(
317 "ILU(k) requires square matrix, got {}x{}",
318 matrix.rows(),
319 matrix.cols()
320 )));
321 }
322
323 let n = matrix.rows() as usize;
324 if n == 0 {
325 return Err(SparseError::InvalidArgument(
326 "cannot factor an empty matrix".to_string(),
327 ));
328 }
329
330 let (h_row_ptr, h_col_idx, h_values) = matrix.to_host()?;
331
332 let (sym_row_ptr, sym_col_idx, _levels) =
334 iluk_symbolic(&h_row_ptr, &h_col_idx, n, config.fill_level)?;
335
336 let factored_values = iluk_numeric::<T>(
338 &sym_row_ptr,
339 &sym_col_idx,
340 &h_row_ptr,
341 &h_col_idx,
342 &h_values,
343 n,
344 )?;
345
346 let mut diag_inv = vec![T::gpu_zero(); n];
348 for i in 0..n {
349 let start = sym_row_ptr[i] as usize;
350 let end = sym_row_ptr[i + 1] as usize;
351 let diag_pos = find_col_in_row(&sym_col_idx[start..end], i as i32);
352 match diag_pos {
353 Some(pos) => {
354 let diag_val = factored_values[start + pos];
355 if diag_val == T::gpu_zero() {
356 return Err(SparseError::SingularMatrix);
357 }
358 diag_inv[i] = div_gpu_float(T::gpu_one(), diag_val);
359 }
360 None => return Err(SparseError::SingularMatrix),
361 }
362 }
363
364 let nnz = sym_col_idx.len() as u32;
365 if nnz == 0 {
366 return Err(SparseError::ZeroNnz);
367 }
368
369 let lu = CsrMatrix::from_host(
370 matrix.rows(),
371 matrix.cols(),
372 &sym_row_ptr,
373 &sym_col_idx,
374 &factored_values,
375 )?;
376
377 Ok(Self {
378 lu,
379 diag_inv,
380 fill_level: config.fill_level,
381 })
382 }
383
384 pub fn apply(&self, r: &[T], z: &mut [T]) -> SparseResult<()> {
397 let n = self.lu.rows() as usize;
398 if r.len() != n || z.len() != n {
399 return Err(SparseError::DimensionMismatch(format!(
400 "vector length mismatch: r={}, z={}, expected {}",
401 r.len(),
402 z.len(),
403 n
404 )));
405 }
406
407 let (h_row_ptr, h_col_idx, h_values) = self.lu.to_host()?;
408
409 let mut y = vec![T::gpu_zero(); n];
412 for i in 0..n {
413 let start = h_row_ptr[i] as usize;
414 let end = h_row_ptr[i + 1] as usize;
415 let mut sum = r[i];
416
417 for nz in start..end {
418 let j = h_col_idx[nz] as usize;
419 if j >= i {
420 break;
421 }
422 let update = mul_gpu_float(h_values[nz], y[j]);
423 sum = sub_gpu_float(sum, update);
424 }
425 y[i] = sum;
426 }
427
428 for i in (0..n).rev() {
431 let start = h_row_ptr[i] as usize;
432 let end = h_row_ptr[i + 1] as usize;
433 let mut sum = y[i];
434
435 for nz in start..end {
436 let j = h_col_idx[nz] as usize;
437 if j <= i {
438 continue;
439 }
440 let update = mul_gpu_float(h_values[nz], z[j]);
441 sum = sub_gpu_float(sum, update);
442 }
443 z[i] = mul_gpu_float(sum, self.diag_inv[i]);
444 }
445
446 Ok(())
447 }
448}
449
450fn find_col_in_row(col_slice: &[i32], target_col: i32) -> Option<usize> {
455 col_slice.iter().position(|&c| c == target_col)
456}
457
458#[cfg(test)]
463mod tests {
464 use super::*;
465
466 #[test]
467 fn iluk_config_default() {
468 let cfg = IlukConfig::default();
469 assert_eq!(cfg.fill_level, 0);
470 }
471
472 #[test]
473 fn symbolic_identity_no_fill() {
474 let row_ptr = vec![0, 1, 2, 3];
476 let col_idx = vec![0, 1, 2];
477 let (sym_rp, sym_ci, levels) =
478 iluk_symbolic(&row_ptr, &col_idx, 3, 5).expect("test: symbolic should succeed");
479 assert_eq!(sym_rp, row_ptr);
480 assert_eq!(sym_ci, col_idx);
481 assert!(levels.iter().all(|&l| l == 0));
482 }
483
484 #[test]
485 fn symbolic_tridiagonal_fill_level_0() {
486 let row_ptr = vec![0, 2, 5, 7];
491 let col_idx = vec![0, 1, 0, 1, 2, 1, 2];
492 let (sym_rp, sym_ci, _) =
493 iluk_symbolic(&row_ptr, &col_idx, 3, 0).expect("test: symbolic should succeed");
494 assert_eq!(sym_rp, row_ptr);
496 assert_eq!(sym_ci, col_idx);
497 }
498
499 #[test]
500 fn symbolic_tridiagonal_fill_level_1() {
501 let row_ptr = vec![0, 2, 5, 8, 10];
504 let col_idx = vec![0, 1, 0, 1, 2, 1, 2, 3, 2, 3];
505 let (sym_rp, sym_ci, _) =
506 iluk_symbolic(&row_ptr, &col_idx, 4, 1).expect("test: symbolic should succeed");
507 let orig_nnz = col_idx.len();
509 let sym_nnz = sym_ci.len();
510 assert!(sym_nnz >= orig_nnz);
511 assert_eq!(sym_rp.len(), 5);
513 assert_eq!(sym_rp[0], 0);
514 assert_eq!(sym_rp[4], sym_nnz as i32);
515 }
516
517 #[test]
518 fn numeric_identity() {
519 let row_ptr = vec![0, 1, 2, 3];
521 let col_idx = vec![0, 1, 2];
522 let values: Vec<f64> = vec![2.0, 3.0, 4.0];
523 let result = iluk_numeric::<f64>(&row_ptr, &col_idx, &row_ptr, &col_idx, &values, 3);
524 assert!(result.is_ok());
525 let vals = result.expect("test: numeric should succeed");
526 assert!((vals[0] - 2.0).abs() < 1e-12);
527 assert!((vals[1] - 3.0).abs() < 1e-12);
528 assert!((vals[2] - 4.0).abs() < 1e-12);
529 }
530
531 #[test]
532 fn numeric_singular_detection() {
533 let row_ptr = vec![0, 2, 4];
535 let col_idx = vec![0, 1, 0, 1];
536 let values: Vec<f64> = vec![0.0, 1.0, 1.0, 2.0];
537 let result = iluk_numeric::<f64>(&row_ptr, &col_idx, &row_ptr, &col_idx, &values, 2);
538 assert!(matches!(result, Err(SparseError::SingularMatrix)));
539 }
540
541 #[test]
542 fn numeric_tridiagonal_f32() {
543 let row_ptr = vec![0, 2, 5, 7];
548 let col_idx = vec![0, 1, 0, 1, 2, 1, 2];
549 let values: Vec<f32> = vec![4.0, -1.0, -1.0, 4.0, -1.0, -1.0, 4.0];
550 let result = iluk_numeric::<f32>(&row_ptr, &col_idx, &row_ptr, &col_idx, &values, 3);
551 assert!(result.is_ok());
552 let vals = result.expect("test: numeric should succeed");
553 assert!(to_f64(vals[0]).abs() > 1e-6);
555 assert!((to_f64(vals[2]) - (-0.25)).abs() < 1e-5);
557 }
558
559 #[test]
560 fn find_col_works() {
561 let cols = [0, 2, 5, 7];
562 assert_eq!(find_col_in_row(&cols, 2), Some(1));
563 assert_eq!(find_col_in_row(&cols, 3), None);
564 assert_eq!(find_col_in_row(&cols, 7), Some(3));
565 }
566
567 #[test]
568 fn symbolic_empty_row() {
569 let row_ptr = vec![0, 1, 3, 4];
572 let col_idx = vec![0, 0, 1, 2];
573 let (sym_rp, sym_ci, _) =
574 iluk_symbolic(&row_ptr, &col_idx, 3, 0).expect("test: symbolic should succeed");
575 assert_eq!(sym_rp.len(), 4);
576 assert_eq!(sym_ci.len() as i32, sym_rp[3]);
577 }
578}