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