1use core::fmt::Debug;
35
36use dyn_stack::{MemStack, StackReq};
37use faer::matrix_free::{BiLinOp, BiPrecond, LinOp, Precond};
38use faer::{Conj, MatMut, MatRef, Par};
39use faer_traits::{ComplexField, Index};
40
41pub mod apply;
42pub mod numeric;
43pub mod symbolic;
44
45pub use numeric::Iluk;
46pub use symbolic::SymbolicIluk;
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct IlukParams {
51 pub level: usize,
53}
54
55impl Default for IlukParams {
56 fn default() -> Self {
57 Self { level: 1 }
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum IlukError {
64 NonSquareMatrix { nrows: usize, ncols: usize },
66 MissingDiagonal { col: usize },
68 UnsortedRowIndices { col: usize },
70 PatternMismatch,
73 ZeroPivot { col: usize },
75}
76
77impl core::fmt::Display for IlukError {
78 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
79 match self {
80 Self::NonSquareMatrix { nrows, ncols } => {
81 write!(f, "matrix must be square but is {nrows}x{ncols}")
82 }
83 Self::MissingDiagonal { col } => write!(f, "column {col} is missing its diagonal entry"),
84 Self::UnsortedRowIndices { col } => write!(f, "column {col} has unsorted row indices"),
85 Self::PatternMismatch => f.write_str("refactorisation pattern does not match symbolic"),
86 Self::ZeroPivot { col } => write!(f, "encountered a zero pivot at column {col}"),
87 }
88 }
89}
90
91impl core::error::Error for IlukError {}
92
93impl<I, T> LinOp<T> for Iluk<I, T>
94where
95 I: Index,
96 T: ComplexField + Debug + Sync,
97{
98 fn apply_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
99 StackReq::EMPTY
100 }
101
102 fn nrows(&self) -> usize {
103 self.dim()
104 }
105
106 fn ncols(&self) -> usize {
107 self.dim()
108 }
109
110 fn apply(&self, mut out: MatMut<'_, T>, rhs: MatRef<'_, T>, par: Par, _stack: &mut MemStack) {
111 out.copy_from(rhs);
112 apply::solve_in_place(self, Conj::No, out, par);
113 }
114
115 fn conj_apply(
116 &self,
117 mut out: MatMut<'_, T>,
118 rhs: MatRef<'_, T>,
119 par: Par,
120 _stack: &mut MemStack,
121 ) {
122 out.copy_from(rhs);
123 apply::solve_in_place(self, Conj::Yes, out, par);
124 }
125}
126
127impl<I, T> Precond<T> for Iluk<I, T>
128where
129 I: Index,
130 T: ComplexField + Debug + Sync,
131{
132 fn apply_in_place_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
133 StackReq::EMPTY
134 }
135
136 fn apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, _stack: &mut MemStack) {
137 apply::solve_in_place(self, Conj::No, rhs, par);
138 }
139
140 fn conj_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, _stack: &mut MemStack) {
141 apply::solve_in_place(self, Conj::Yes, rhs, par);
142 }
143}
144
145impl<I, T> BiLinOp<T> for Iluk<I, T>
146where
147 I: Index,
148 T: ComplexField + Debug + Sync,
149{
150 fn transpose_apply_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
151 StackReq::EMPTY
152 }
153
154 fn transpose_apply(
155 &self,
156 mut out: MatMut<'_, T>,
157 rhs: MatRef<'_, T>,
158 par: Par,
159 _stack: &mut MemStack,
160 ) {
161 out.copy_from(rhs);
162 apply::solve_transpose_in_place(self, Conj::No, out, par);
163 }
164
165 fn adjoint_apply(
166 &self,
167 mut out: MatMut<'_, T>,
168 rhs: MatRef<'_, T>,
169 par: Par,
170 _stack: &mut MemStack,
171 ) {
172 out.copy_from(rhs);
173 apply::solve_transpose_in_place(self, Conj::Yes, out, par);
174 }
175}
176
177impl<I, T> BiPrecond<T> for Iluk<I, T>
178where
179 I: Index,
180 T: ComplexField + Debug + Sync,
181{
182 fn transpose_apply_in_place_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
183 StackReq::EMPTY
184 }
185
186 fn transpose_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, _stack: &mut MemStack) {
187 apply::solve_transpose_in_place(self, Conj::No, rhs, par);
188 }
189
190 fn adjoint_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, _stack: &mut MemStack) {
191 apply::solve_transpose_in_place(self, Conj::Yes, rhs, par);
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use crate::ilu0::SymbolicIlu0;
199 use faer::sparse::{SparseColMat, Triplet};
200 use faer::{Mat, MatRef, mat};
201
202 fn assert_close(lhs: MatRef<'_, f64>, rhs: MatRef<'_, f64>, tol: f64) {
203 assert_eq!(lhs.nrows(), rhs.nrows());
204 assert_eq!(lhs.ncols(), rhs.ncols());
205 for j in 0..lhs.ncols() {
206 for i in 0..lhs.nrows() {
207 let diff = (*lhs.get(i, j) - *rhs.get(i, j)).abs();
208 assert!(
209 diff <= tol,
210 "mismatch at ({i}, {j}): lhs={}, rhs={}, diff={diff}",
211 *lhs.get(i, j),
212 *rhs.get(i, j),
213 );
214 }
215 }
216 }
217
218 fn to_dense(a: &SparseColMat<usize, f64>) -> Mat<f64> {
219 let n = a.nrows();
220 let mut out = Mat::<f64>::zeros(n, a.ncols());
221 let a_ref = a.as_ref();
222 for j in 0..a.ncols() {
223 let rows = a_ref.symbolic().row_idx_of_col_raw(j);
224 let vals = a_ref.val_of_col(j);
225 for (r, v) in rows.iter().zip(vals.iter()) {
226 *out.as_mut().get_mut(*r, j) = *v;
227 }
228 }
229 out
230 }
231
232 fn sparse_view_to_dense(a: faer::sparse::SparseColMatRef<'_, usize, f64>) -> Mat<f64> {
233 let mut dense = Mat::<f64>::zeros(a.nrows(), a.ncols());
234 for j in 0..a.ncols() {
235 let rows = a.symbolic().row_idx_of_col_raw(j);
236 let vals = a.val_of_col(j);
237 for (r, v) in rows.iter().zip(vals.iter()) {
238 *dense.as_mut().get_mut(*r, j) = *v;
239 }
240 }
241 dense
242 }
243
244 fn laplacian_2d(grid: usize) -> SparseColMat<usize, f64> {
245 let n = grid * grid;
246 let mut triplets = Vec::new();
247 for gy in 0..grid {
248 for gx in 0..grid {
249 let idx = gy * grid + gx;
250 triplets.push(Triplet::new(idx, idx, 4.0));
251 if gx > 0 {
252 triplets.push(Triplet::new(idx, idx - 1, -1.0));
253 }
254 if gx + 1 < grid {
255 triplets.push(Triplet::new(idx, idx + 1, -1.0));
256 }
257 if gy > 0 {
258 triplets.push(Triplet::new(idx, idx - grid, -1.0));
259 }
260 if gy + 1 < grid {
261 triplets.push(Triplet::new(idx, idx + grid, -1.0));
262 }
263 }
264 }
265 SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
266 }
267
268 fn tridiagonal(n: usize, diag: f64, sub: f64, sup: f64) -> SparseColMat<usize, f64> {
269 let mut triplets = Vec::new();
270 for i in 0..n {
271 triplets.push(Triplet::new(i, i, diag));
272 if i > 0 {
273 triplets.push(Triplet::new(i, i - 1, sub));
274 triplets.push(Triplet::new(i - 1, i, sup));
275 }
276 }
277 SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
278 }
279
280 #[test]
281 fn level_zero_matches_ilu0_pattern() {
282 let a = laplacian_2d(5);
283 let sk = SymbolicIluk::<usize>::try_new(a.as_ref().symbolic(), 0).unwrap();
284 let s0 = SymbolicIlu0::<usize>::try_new(a.as_ref().symbolic()).unwrap();
285 assert_eq!(sk.l_col_ptr, s0.l_col_ptr);
286 assert_eq!(sk.l_row_idx, s0.l_row_idx);
287 assert_eq!(sk.u_col_ptr, s0.u_col_ptr);
288 assert_eq!(sk.u_row_idx, s0.u_row_idx);
289 }
290
291 #[test]
292 fn level_one_grows_the_pattern() {
293 let a = laplacian_2d(5);
294 let s0 = SymbolicIluk::<usize>::try_new(a.as_ref().symbolic(), 0).unwrap();
295 let s1 = SymbolicIluk::<usize>::try_new(a.as_ref().symbolic(), 1).unwrap();
296 assert!(
297 s1.l_nnz() + s1.u_nnz() > s0.l_nnz() + s0.u_nnz(),
298 "ILU(1) should introduce fill over ILU(0)"
299 );
300 }
301
302 #[test]
303 fn factor_matches_a_on_its_own_pattern() {
304 let a = laplacian_2d(5);
306 let pc = Iluk::try_new(a.as_ref(), 1).unwrap();
307 let l = sparse_view_to_dense(pc.l_view());
308 let u = sparse_view_to_dense(pc.u_view());
309 let lu = &l * &u;
310 let a_dense = to_dense(&a);
311 let a_ref = a.as_ref();
312 for j in 0..a.ncols() {
313 for r in a_ref.symbolic().row_idx_of_col_raw(j) {
314 let i = *r;
315 let diff = (*lu.as_ref().get(i, j) - *a_dense.as_ref().get(i, j)).abs();
316 assert!(diff <= 1e-10, "L*U disagrees with A at ({i},{j}): {diff}");
317 }
318 }
319 }
320
321 #[test]
322 fn tridiagonal_is_exact() {
323 let a = tridiagonal(6, 4.0, -1.0, -1.0);
325 let pc = Iluk::try_new(a.as_ref(), 2).unwrap();
326 let a_dense = to_dense(&a);
327 let x_true = mat![[1.0], [-2.0], [3.0], [-1.0], [0.5], [2.0_f64]];
328 let mut rhs = (&a_dense * &x_true).to_owned();
329 pc.apply_in_place(rhs.as_mut(), Par::Seq, MemStack::new(&mut []));
330 assert_close(rhs.as_ref(), x_true.as_ref(), 1e-12);
331 }
332
333 #[test]
334 fn refactorize_matches_fresh() {
335 let a1 = laplacian_2d(4);
336 let a2 = {
337 let mut t = Vec::new();
339 let a1_ref = a1.as_ref();
340 for j in 0..a1.ncols() {
341 for (r, v) in a1_ref
342 .symbolic()
343 .row_idx_of_col_raw(j)
344 .iter()
345 .zip(a1_ref.val_of_col(j))
346 {
347 t.push(Triplet::new(*r, j, v * 1.5 + if *r == j { 0.5 } else { 0.0 }));
348 }
349 }
350 SparseColMat::try_new_from_triplets(a1.nrows(), a1.ncols(), &t).unwrap()
351 };
352 let fresh = Iluk::try_new(a2.as_ref(), 1).unwrap();
353 let mut reused = Iluk::try_new(a1.as_ref(), 1).unwrap();
354 reused.refactorize(a2.as_ref()).unwrap();
355 for (a, b) in fresh.l_values.iter().zip(reused.l_values.iter()) {
356 assert!((a - b).abs() < 1e-12);
357 }
358 for (a, b) in fresh.u_values.iter().zip(reused.u_values.iter()) {
359 assert!((a - b).abs() < 1e-12);
360 }
361 }
362
363 #[test]
364 fn reduces_residual_on_laplacian() {
365 let a = laplacian_2d(8);
366 let n = a.nrows();
367 let pc = Iluk::try_new(a.as_ref(), 1).unwrap();
368 let a_dense = to_dense(&a);
369 let b = Mat::<f64>::from_fn(n, 1, |i, _| (i % 7) as f64 - 3.0);
370 let mut x = b.clone();
371 pc.apply_in_place(x.as_mut(), Par::Seq, MemStack::new(&mut []));
372 let residual = &a_dense * &x - &b;
373 let b_norm: f64 = b.as_ref().col(0).iter().map(|v| v * v).sum::<f64>().sqrt();
374 let r_norm: f64 = residual
375 .as_ref()
376 .col(0)
377 .iter()
378 .map(|v| v * v)
379 .sum::<f64>()
380 .sqrt();
381 assert!(r_norm / b_norm < 0.5, "ILU(1) residual ratio too large");
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 assert_eq!(
392 Iluk::try_new(a.as_ref(), 1).unwrap_err(),
393 IlukError::NonSquareMatrix { nrows: 3, ncols: 4 }
394 );
395 }
396}