#![allow(dead_code)]
use oxicuda_blas::GpuFloat;
use crate::error::{SparseError, SparseResult};
use crate::format::CsrMatrix;
use crate::handle::SparseHandle;
fn to_f64<T: GpuFloat>(val: T) -> f64 {
if T::SIZE == 4 {
f32::from_bits(val.to_bits_u64() as u32) as f64
} else {
f64::from_bits(val.to_bits_u64())
}
}
fn from_f64<T: GpuFloat>(val: f64) -> T {
if T::SIZE == 4 {
T::from_bits_u64(u64::from((val as f32).to_bits()))
} else {
T::from_bits_u64(val.to_bits())
}
}
fn div_gpu_float<T: GpuFloat>(a: T, b: T) -> T {
from_f64::<T>(to_f64(a) / to_f64(b))
}
fn mul_gpu_float<T: GpuFloat>(a: T, b: T) -> T {
from_f64::<T>(to_f64(a) * to_f64(b))
}
fn sub_gpu_float<T: GpuFloat>(a: T, b: T) -> T {
from_f64::<T>(to_f64(a) - to_f64(b))
}
#[derive(Default)]
pub struct IlukConfig {
pub fill_level: usize,
}
pub struct IlukFactorization<T: GpuFloat> {
pub lu: CsrMatrix<T>,
pub diag_inv: Vec<T>,
pub fill_level: usize,
}
struct SymbolicEntry {
col: usize,
level: usize,
}
fn iluk_symbolic(
row_ptr: &[i32],
col_idx: &[i32],
n: usize,
fill_level: usize,
) -> SparseResult<(Vec<i32>, Vec<i32>, Vec<usize>)> {
let mut rows: Vec<Vec<SymbolicEntry>> = Vec::with_capacity(n);
for i in 0..n {
let start = row_ptr[i] as usize;
let end = row_ptr[i + 1] as usize;
let mut row_entries: Vec<SymbolicEntry> = Vec::with_capacity(end - start);
for &cj in &col_idx[start..end] {
row_entries.push(SymbolicEntry {
col: cj as usize,
level: 0,
});
}
row_entries.sort_by_key(|e| e.col);
rows.push(row_entries);
}
for i in 0..n {
let mut k_idx = 0;
loop {
if k_idx >= rows[i].len() {
break;
}
let k = rows[i][k_idx].col;
if k >= i {
break;
}
let lev_ik = rows[i][k_idx].level;
let diag_pos = rows[k].iter().position(|e| e.col == k);
if diag_pos.is_none() {
k_idx += 1;
continue;
}
let row_k_entries: Vec<(usize, usize)> = rows[k]
.iter()
.filter(|e| e.col > k)
.map(|e| (e.col, e.level))
.collect();
for (j, lev_kj) in row_k_entries {
let new_level = lev_ik + lev_kj + 1;
if new_level > fill_level {
continue;
}
let existing = rows[i].iter().position(|e| e.col == j);
match existing {
Some(pos) => {
if new_level < rows[i][pos].level {
rows[i][pos].level = new_level;
}
}
None => {
let insert_pos = rows[i]
.iter()
.position(|e| e.col > j)
.unwrap_or(rows[i].len());
rows[i].insert(
insert_pos,
SymbolicEntry {
col: j,
level: new_level,
},
);
}
}
}
k_idx += 1;
}
}
let mut out_row_ptr = vec![0i32; n + 1];
let mut out_col_idx = Vec::new();
let mut out_levels = Vec::new();
for (i, row_entries) in rows.iter().enumerate() {
for entry in row_entries {
out_col_idx.push(entry.col as i32);
out_levels.push(entry.level);
}
out_row_ptr[i + 1] = out_col_idx.len() as i32;
}
Ok((out_row_ptr, out_col_idx, out_levels))
}
fn iluk_numeric<T: GpuFloat>(
sym_row_ptr: &[i32],
sym_col_idx: &[i32],
orig_row_ptr: &[i32],
orig_col_idx: &[i32],
orig_values: &[T],
n: usize,
) -> SparseResult<Vec<T>> {
let nnz = sym_col_idx.len();
let mut values = vec![T::gpu_zero(); nnz];
for i in 0..n {
let orig_start = orig_row_ptr[i] as usize;
let orig_end = orig_row_ptr[i + 1] as usize;
let sym_start = sym_row_ptr[i] as usize;
let sym_end = sym_row_ptr[i + 1] as usize;
let mut sym_k = sym_start;
for orig_k in orig_start..orig_end {
let col = orig_col_idx[orig_k];
while sym_k < sym_end && sym_col_idx[sym_k] < col {
sym_k += 1;
}
if sym_k < sym_end && sym_col_idx[sym_k] == col {
values[sym_k] = orig_values[orig_k];
}
}
}
for i in 0..n {
let row_start = sym_row_ptr[i] as usize;
let row_end = sym_row_ptr[i + 1] as usize;
for nz in row_start..row_end {
let k = sym_col_idx[nz] as usize;
if k >= i {
break;
}
let k_start = sym_row_ptr[k] as usize;
let k_end = sym_row_ptr[k + 1] as usize;
let diag_pos = find_col_in_row(&sym_col_idx[k_start..k_end], k as i32);
let diag_pos = match diag_pos {
Some(pos) => k_start + pos,
None => return Err(SparseError::SingularMatrix),
};
let a_kk = values[diag_pos];
if a_kk == T::gpu_zero() {
return Err(SparseError::SingularMatrix);
}
let ratio = div_gpu_float(values[nz], a_kk);
values[nz] = ratio;
for k_nz in (diag_pos + 1)..k_end {
let j = sym_col_idx[k_nz];
if let Some(ij_off) = find_col_in_row(&sym_col_idx[row_start..row_end], j) {
let ij_pos = row_start + ij_off;
let update = mul_gpu_float(ratio, values[k_nz]);
values[ij_pos] = sub_gpu_float(values[ij_pos], update);
}
}
}
}
Ok(values)
}
impl<T: GpuFloat> IlukFactorization<T> {
pub fn compute(
_handle: &SparseHandle,
matrix: &CsrMatrix<T>,
config: &IlukConfig,
) -> SparseResult<Self> {
if matrix.rows() != matrix.cols() {
return Err(SparseError::DimensionMismatch(format!(
"ILU(k) requires square matrix, got {}x{}",
matrix.rows(),
matrix.cols()
)));
}
let n = matrix.rows() as usize;
if n == 0 {
return Err(SparseError::InvalidArgument(
"cannot factor an empty matrix".to_string(),
));
}
let (h_row_ptr, h_col_idx, h_values) = matrix.to_host()?;
let (sym_row_ptr, sym_col_idx, _levels) =
iluk_symbolic(&h_row_ptr, &h_col_idx, n, config.fill_level)?;
let factored_values = iluk_numeric::<T>(
&sym_row_ptr,
&sym_col_idx,
&h_row_ptr,
&h_col_idx,
&h_values,
n,
)?;
let mut diag_inv = vec![T::gpu_zero(); n];
for i in 0..n {
let start = sym_row_ptr[i] as usize;
let end = sym_row_ptr[i + 1] as usize;
let diag_pos = find_col_in_row(&sym_col_idx[start..end], i as i32);
match diag_pos {
Some(pos) => {
let diag_val = factored_values[start + pos];
if diag_val == T::gpu_zero() {
return Err(SparseError::SingularMatrix);
}
diag_inv[i] = div_gpu_float(T::gpu_one(), diag_val);
}
None => return Err(SparseError::SingularMatrix),
}
}
let nnz = sym_col_idx.len() as u32;
if nnz == 0 {
return Err(SparseError::ZeroNnz);
}
let lu = CsrMatrix::from_host(
matrix.rows(),
matrix.cols(),
&sym_row_ptr,
&sym_col_idx,
&factored_values,
)?;
Ok(Self {
lu,
diag_inv,
fill_level: config.fill_level,
})
}
pub fn apply(&self, r: &[T], z: &mut [T]) -> SparseResult<()> {
let n = self.lu.rows() as usize;
if r.len() != n || z.len() != n {
return Err(SparseError::DimensionMismatch(format!(
"vector length mismatch: r={}, z={}, expected {}",
r.len(),
z.len(),
n
)));
}
let (h_row_ptr, h_col_idx, h_values) = self.lu.to_host()?;
let mut y = vec![T::gpu_zero(); n];
for i in 0..n {
let start = h_row_ptr[i] as usize;
let end = h_row_ptr[i + 1] as usize;
let mut sum = r[i];
for nz in start..end {
let j = h_col_idx[nz] as usize;
if j >= i {
break;
}
let update = mul_gpu_float(h_values[nz], y[j]);
sum = sub_gpu_float(sum, update);
}
y[i] = sum;
}
for i in (0..n).rev() {
let start = h_row_ptr[i] as usize;
let end = h_row_ptr[i + 1] as usize;
let mut sum = y[i];
for nz in start..end {
let j = h_col_idx[nz] as usize;
if j <= i {
continue;
}
let update = mul_gpu_float(h_values[nz], z[j]);
sum = sub_gpu_float(sum, update);
}
z[i] = mul_gpu_float(sum, self.diag_inv[i]);
}
Ok(())
}
}
fn find_col_in_row(col_slice: &[i32], target_col: i32) -> Option<usize> {
col_slice.iter().position(|&c| c == target_col)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn iluk_config_default() {
let cfg = IlukConfig::default();
assert_eq!(cfg.fill_level, 0);
}
#[test]
fn symbolic_identity_no_fill() {
let row_ptr = vec![0, 1, 2, 3];
let col_idx = vec![0, 1, 2];
let (sym_rp, sym_ci, levels) =
iluk_symbolic(&row_ptr, &col_idx, 3, 5).expect("test: symbolic should succeed");
assert_eq!(sym_rp, row_ptr);
assert_eq!(sym_ci, col_idx);
assert!(levels.iter().all(|&l| l == 0));
}
#[test]
fn symbolic_tridiagonal_fill_level_0() {
let row_ptr = vec![0, 2, 5, 7];
let col_idx = vec![0, 1, 0, 1, 2, 1, 2];
let (sym_rp, sym_ci, _) =
iluk_symbolic(&row_ptr, &col_idx, 3, 0).expect("test: symbolic should succeed");
assert_eq!(sym_rp, row_ptr);
assert_eq!(sym_ci, col_idx);
}
#[test]
fn symbolic_tridiagonal_fill_level_1() {
let row_ptr = vec![0, 2, 5, 8, 10];
let col_idx = vec![0, 1, 0, 1, 2, 1, 2, 3, 2, 3];
let (sym_rp, sym_ci, _) =
iluk_symbolic(&row_ptr, &col_idx, 4, 1).expect("test: symbolic should succeed");
let orig_nnz = col_idx.len();
let sym_nnz = sym_ci.len();
assert!(sym_nnz >= orig_nnz);
assert_eq!(sym_rp.len(), 5);
assert_eq!(sym_rp[0], 0);
assert_eq!(sym_rp[4], sym_nnz as i32);
}
#[test]
fn numeric_identity() {
let row_ptr = vec![0, 1, 2, 3];
let col_idx = vec![0, 1, 2];
let values: Vec<f64> = vec![2.0, 3.0, 4.0];
let result = iluk_numeric::<f64>(&row_ptr, &col_idx, &row_ptr, &col_idx, &values, 3);
assert!(result.is_ok());
let vals = result.expect("test: numeric should succeed");
assert!((vals[0] - 2.0).abs() < 1e-12);
assert!((vals[1] - 3.0).abs() < 1e-12);
assert!((vals[2] - 4.0).abs() < 1e-12);
}
#[test]
fn numeric_singular_detection() {
let row_ptr = vec![0, 2, 4];
let col_idx = vec![0, 1, 0, 1];
let values: Vec<f64> = vec![0.0, 1.0, 1.0, 2.0];
let result = iluk_numeric::<f64>(&row_ptr, &col_idx, &row_ptr, &col_idx, &values, 2);
assert!(matches!(result, Err(SparseError::SingularMatrix)));
}
#[test]
fn numeric_tridiagonal_f32() {
let row_ptr = vec![0, 2, 5, 7];
let col_idx = vec![0, 1, 0, 1, 2, 1, 2];
let values: Vec<f32> = vec![4.0, -1.0, -1.0, 4.0, -1.0, -1.0, 4.0];
let result = iluk_numeric::<f32>(&row_ptr, &col_idx, &row_ptr, &col_idx, &values, 3);
assert!(result.is_ok());
let vals = result.expect("test: numeric should succeed");
assert!(to_f64(vals[0]).abs() > 1e-6);
assert!((to_f64(vals[2]) - (-0.25)).abs() < 1e-5);
}
#[test]
fn find_col_works() {
let cols = [0, 2, 5, 7];
assert_eq!(find_col_in_row(&cols, 2), Some(1));
assert_eq!(find_col_in_row(&cols, 3), None);
assert_eq!(find_col_in_row(&cols, 7), Some(3));
}
#[test]
fn symbolic_empty_row() {
let row_ptr = vec![0, 1, 3, 4];
let col_idx = vec![0, 0, 1, 2];
let (sym_rp, sym_ci, _) =
iluk_symbolic(&row_ptr, &col_idx, 3, 0).expect("test: symbolic should succeed");
assert_eq!(sym_rp.len(), 4);
assert_eq!(sym_ci.len() as i32, sym_rp[3]);
}
}