1use core::fmt::Debug;
61
62use dyn_stack::{MemStack, StackReq};
63use faer::matrix_free::{BiLinOp, BiPrecond, LinOp, Precond};
64use faer::{Conj, MatMut, MatRef, Par};
65use faer_traits::{ComplexField, Index};
66
67pub mod apply;
68pub mod numeric;
69pub mod symbolic;
70
71pub use numeric::Ilu0;
72pub use symbolic::SymbolicIlu0;
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum Ilu0Error {
77 NonSquareMatrix { nrows: usize, ncols: usize },
79 MissingDiagonal { col: usize },
81 UnsortedRowIndices { col: usize },
83 PatternMismatch,
86 ZeroPivot { col: usize },
88}
89
90impl core::fmt::Display for Ilu0Error {
91 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
92 match self {
93 Self::NonSquareMatrix { nrows, ncols } => {
94 write!(f, "matrix must be square but is {nrows}x{ncols}")
95 }
96 Self::MissingDiagonal { col } => {
97 write!(f, "column {col} is missing its diagonal entry")
98 }
99 Self::UnsortedRowIndices { col } => {
100 write!(f, "column {col} has unsorted row indices")
101 }
102 Self::PatternMismatch => f.write_str("refactorisation pattern does not match symbolic"),
103 Self::ZeroPivot { col } => write!(f, "encountered a zero pivot at column {col}"),
104 }
105 }
106}
107
108impl core::error::Error for Ilu0Error {}
109
110impl<I, T> LinOp<T> for Ilu0<I, T>
111where
112 I: Index,
113 T: ComplexField + Debug + Sync,
114{
115 fn apply_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
116 StackReq::EMPTY
117 }
118
119 fn nrows(&self) -> usize {
120 self.dim()
121 }
122
123 fn ncols(&self) -> usize {
124 self.dim()
125 }
126
127 fn apply(&self, mut out: MatMut<'_, T>, rhs: MatRef<'_, T>, par: Par, _stack: &mut MemStack) {
128 out.copy_from(rhs);
129 apply::solve_in_place(self, Conj::No, out, par);
130 }
131
132 fn conj_apply(
133 &self,
134 mut out: MatMut<'_, T>,
135 rhs: MatRef<'_, T>,
136 par: Par,
137 _stack: &mut MemStack,
138 ) {
139 out.copy_from(rhs);
140 apply::solve_in_place(self, Conj::Yes, out, par);
141 }
142}
143
144impl<I, T> Precond<T> for Ilu0<I, T>
145where
146 I: Index,
147 T: ComplexField + Debug + Sync,
148{
149 fn apply_in_place_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
150 StackReq::EMPTY
151 }
152
153 fn apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, _stack: &mut MemStack) {
154 apply::solve_in_place(self, Conj::No, rhs, par);
155 }
156
157 fn conj_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, _stack: &mut MemStack) {
158 apply::solve_in_place(self, Conj::Yes, rhs, par);
159 }
160}
161
162impl<I, T> BiLinOp<T> for Ilu0<I, T>
163where
164 I: Index,
165 T: ComplexField + Debug + Sync,
166{
167 fn transpose_apply_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
168 StackReq::EMPTY
169 }
170
171 fn transpose_apply(
172 &self,
173 mut out: MatMut<'_, T>,
174 rhs: MatRef<'_, T>,
175 par: Par,
176 _stack: &mut MemStack,
177 ) {
178 out.copy_from(rhs);
179 apply::solve_transpose_in_place(self, Conj::No, out, par);
180 }
181
182 fn adjoint_apply(
183 &self,
184 mut out: MatMut<'_, T>,
185 rhs: MatRef<'_, T>,
186 par: Par,
187 _stack: &mut MemStack,
188 ) {
189 out.copy_from(rhs);
190 apply::solve_transpose_in_place(self, Conj::Yes, out, par);
191 }
192}
193
194impl<I, T> BiPrecond<T> for Ilu0<I, T>
195where
196 I: Index,
197 T: ComplexField + Debug + Sync,
198{
199 fn transpose_apply_in_place_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
200 StackReq::EMPTY
201 }
202
203 fn transpose_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, _stack: &mut MemStack) {
204 apply::solve_transpose_in_place(self, Conj::No, rhs, par);
205 }
206
207 fn adjoint_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, _stack: &mut MemStack) {
208 apply::solve_transpose_in_place(self, Conj::Yes, rhs, par);
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use faer::sparse::{SparseColMat, Triplet};
216 use faer::{Mat, MatRef, mat};
217
218 fn assert_close(lhs: MatRef<'_, f64>, rhs: MatRef<'_, f64>, tol: f64) {
219 assert_eq!(lhs.nrows(), rhs.nrows());
220 assert_eq!(lhs.ncols(), rhs.ncols());
221 for j in 0..lhs.ncols() {
222 for i in 0..lhs.nrows() {
223 let diff = (*lhs.get(i, j) - *rhs.get(i, j)).abs();
224 assert!(
225 diff <= tol,
226 "mismatch at ({i}, {j}): lhs={}, rhs={}, diff={diff}",
227 *lhs.get(i, j),
228 *rhs.get(i, j),
229 );
230 }
231 }
232 }
233
234 fn laplacian_2d(grid: usize) -> SparseColMat<usize, f64> {
237 let n = grid * grid;
238 let mut triplets: Vec<Triplet<usize, usize, f64>> = Vec::new();
239 for gy in 0..grid {
240 for gx in 0..grid {
241 let idx = gy * grid + gx;
242 triplets.push(Triplet::new(idx, idx, 4.0));
243 if gx > 0 {
244 triplets.push(Triplet::new(idx, idx - 1, -1.0));
245 }
246 if gx + 1 < grid {
247 triplets.push(Triplet::new(idx, idx + 1, -1.0));
248 }
249 if gy > 0 {
250 triplets.push(Triplet::new(idx, idx - grid, -1.0));
251 }
252 if gy + 1 < grid {
253 triplets.push(Triplet::new(idx, idx + grid, -1.0));
254 }
255 }
256 }
257 SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
258 }
259
260 fn to_dense(a: &SparseColMat<usize, f64>) -> Mat<f64> {
262 let n = a.nrows();
263 let mut out = Mat::<f64>::zeros(n, a.ncols());
264 let a_ref = a.as_ref();
265 for j in 0..a.ncols() {
266 let rows = a_ref.symbolic().row_idx_of_col_raw(j);
267 let vals = a_ref.val_of_col(j);
268 for (r, v) in rows.iter().zip(vals.iter()) {
269 *out.as_mut().get_mut(*r, j) = *v;
270 }
271 }
272 out
273 }
274
275 fn tridiagonal_csc(n: usize, diag: f64, off: f64) -> SparseColMat<usize, f64> {
276 let mut triplets = Vec::new();
277 for i in 0..n {
278 triplets.push(Triplet::new(i, i, diag));
279 if i > 0 {
280 triplets.push(Triplet::new(i, i - 1, off));
281 triplets.push(Triplet::new(i - 1, i, off));
282 }
283 }
284 SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
285 }
286
287 #[test]
288 fn ilu0_tridiagonal_matches_exact_inverse() {
289 let a = tridiagonal_csc(5, 4.0, -1.0);
292 let pc = Ilu0::try_new(a.as_ref()).unwrap();
293
294 let a_dense = to_dense(&a);
295 let x_true = mat![[1.0], [-2.0], [3.0], [-1.0], [0.5_f64]];
296 let mut rhs = (&a_dense * &x_true).to_owned();
297
298 pc.apply_in_place(rhs.as_mut(), Par::Seq, MemStack::new(&mut []));
299 assert_close(rhs.as_ref(), x_true.as_ref(), 1e-12);
300 }
301
302 fn sparse_view_to_dense(a: faer::sparse::SparseColMatRef<'_, usize, f64>) -> Mat<f64> {
303 let mut dense = Mat::<f64>::zeros(a.nrows(), a.ncols());
304 for j in 0..a.ncols() {
305 let rows = a.symbolic().row_idx_of_col_raw(j);
306 let vals = a.val_of_col(j);
307 for (r, v) in rows.iter().zip(vals.iter()) {
308 *dense.as_mut().get_mut(*r, j) = *v;
309 }
310 }
311 dense
312 }
313
314 #[test]
315 fn ilu0_factor_satisfies_pattern_equation() {
316 let a = laplacian_2d(4);
318 let pc = Ilu0::try_new(a.as_ref()).unwrap();
319
320 let l_dense = sparse_view_to_dense(pc.l_view());
321 let u_dense = sparse_view_to_dense(pc.u_view());
322 let lu_dense = &l_dense * &u_dense;
323 let a_dense = to_dense(&a);
324
325 let a_ref = a.as_ref();
326 for j in 0..a.ncols() {
327 for r in a_ref.symbolic().row_idx_of_col_raw(j) {
328 let i = *r;
329 let diff = (*lu_dense.as_ref().get(i, j) - *a_dense.as_ref().get(i, j)).abs();
330 assert!(diff <= 1e-12, "L*U disagrees with A at ({i},{j}): {diff}");
331 }
332 }
333 }
334
335 #[test]
336 fn ilu0_reduces_residual_significantly() {
337 let a = laplacian_2d(8);
340 let n = a.nrows();
341 let pc = Ilu0::try_new(a.as_ref()).unwrap();
342 let a_dense = to_dense(&a);
343
344 let b = Mat::<f64>::from_fn(n, 1, |i, _| (i % 7) as f64 - 3.0);
345 let mut x = b.clone();
346 pc.apply_in_place(x.as_mut(), Par::Seq, MemStack::new(&mut []));
347
348 let residual = &a_dense * &x - &b;
349 let b_norm: f64 = b.as_ref().col(0).iter().map(|v| v * v).sum::<f64>().sqrt();
350 let r_norm: f64 = residual
351 .as_ref()
352 .col(0)
353 .iter()
354 .map(|v| v * v)
355 .sum::<f64>()
356 .sqrt();
357 assert!(
359 r_norm / b_norm < 0.5,
360 "ILU(0) residual ratio {r_norm}/{b_norm} too large"
361 );
362 }
363
364 #[test]
365 fn refactorize_matches_fresh_construction() {
366 let a1 = tridiagonal_csc(7, 4.0, -1.0);
367 let a2 = tridiagonal_csc(7, 5.0, -2.0);
368
369 let pc_fresh = Ilu0::try_new(a2.as_ref()).unwrap();
370
371 let mut pc_reused = Ilu0::try_new(a1.as_ref()).unwrap();
372 pc_reused.refactorize(a2.as_ref()).unwrap();
373
374 assert_eq!(pc_fresh.l_values.len(), pc_reused.l_values.len());
375 assert_eq!(pc_fresh.u_values.len(), pc_reused.u_values.len());
376 for (a, b) in pc_fresh.l_values.iter().zip(pc_reused.l_values.iter()) {
377 assert!((a - b).abs() < 1e-14);
378 }
379 for (a, b) in pc_fresh.u_values.iter().zip(pc_reused.u_values.iter()) {
380 assert!((a - b).abs() < 1e-14);
381 }
382 }
383
384 #[test]
385 fn rejects_non_square() {
386 let mut triplets = Vec::new();
387 for i in 0..3 {
388 triplets.push(Triplet::new(i, i, 1.0));
389 }
390 let a = SparseColMat::<usize, f64>::try_new_from_triplets(3, 4, &triplets).unwrap();
391 let err = Ilu0::try_new(a.as_ref()).unwrap_err();
392 assert_eq!(err, Ilu0Error::NonSquareMatrix { nrows: 3, ncols: 4 });
393 }
394
395 #[test]
396 fn rejects_missing_diagonal() {
397 let triplets = vec![
399 Triplet::new(0, 0, 1.0),
400 Triplet::new(0, 1, 2.0),
401 Triplet::new(2, 1, 3.0),
402 Triplet::new(2, 2, 4.0_f64),
403 ];
404 let a = SparseColMat::<usize, f64>::try_new_from_triplets(3, 3, &triplets).unwrap();
405 let err = Ilu0::try_new(a.as_ref()).unwrap_err();
406 assert_eq!(err, Ilu0Error::MissingDiagonal { col: 1 });
407 }
408
409 #[test]
410 fn rejects_pattern_mismatch_on_refactorize() {
411 let a1 = tridiagonal_csc(5, 4.0, -1.0);
412 let a2 = tridiagonal_csc(6, 4.0, -1.0);
413 let mut pc = Ilu0::try_new(a1.as_ref()).unwrap();
414 let err = pc.refactorize(a2.as_ref()).unwrap_err();
415 assert_eq!(err, Ilu0Error::PatternMismatch);
416 }
417
418 #[test]
419 fn transpose_apply_inverts_transposed_system() {
420 let a = tridiagonal_csc(6, 4.0, -1.0);
421 let pc = Ilu0::try_new(a.as_ref()).unwrap();
422 let a_dense = to_dense(&a);
423
424 let x_true = mat![[1.0], [-2.0], [3.0], [-1.0], [0.5], [2.0_f64]];
425 let rhs = a_dense.transpose() * &x_true;
426
427 let mut out = rhs.clone();
428 pc.transpose_apply_in_place(out.as_mut(), Par::Seq, MemStack::new(&mut []));
429 assert_close(out.as_ref(), x_true.as_ref(), 1e-12);
431 }
432}