1use core::fmt::Debug;
40
41use dyn_stack::{MemStack, StackReq};
42use faer::matrix_free::{BiLinOp, BiPrecond, LinOp, Precond};
43use faer::{Conj, MatMut, MatRef, Par};
44use faer_traits::{ComplexField, Index};
45
46pub mod apply;
47pub mod numeric;
48
49pub use numeric::Ict;
50
51pub use crate::ilutp::{FillControl, RowNorm};
53
54#[derive(Debug, Clone, Copy, PartialEq)]
58pub struct IctParams {
59 pub drop_tol: f64,
62 pub fill: FillControl,
64 pub norm: RowNorm,
66}
67
68impl Default for IctParams {
69 fn default() -> Self {
70 Self {
71 drop_tol: 1e-3,
72 fill: FillControl::Factor(5.0),
73 norm: RowNorm::Two,
74 }
75 }
76}
77
78impl IctParams {
79 pub(crate) fn validate(&self) -> Result<(), IctError> {
80 if !self.drop_tol.is_finite() || self.drop_tol < 0.0 {
81 return Err(IctError::InvalidDropTol);
82 }
83 if let FillControl::Factor(f) = self.fill
84 && (!f.is_finite() || f <= 0.0)
85 {
86 return Err(IctError::InvalidFillControl);
87 }
88 Ok(())
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
94pub enum IctError {
95 NonSquareMatrix { nrows: usize, ncols: usize },
97 NotPositiveDefinite { col: usize },
100 PatternMismatch,
102 InvalidDropTol,
104 InvalidFillControl,
106}
107
108impl core::fmt::Display for IctError {
109 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
110 match self {
111 Self::NonSquareMatrix { nrows, ncols } => {
112 write!(f, "matrix must be square but is {nrows}x{ncols}")
113 }
114 Self::NotPositiveDefinite { col } => {
115 write!(f, "encountered a non-positive pivot at column {col}")
116 }
117 Self::PatternMismatch => f.write_str("refactorisation dimension does not match"),
118 Self::InvalidDropTol => f.write_str("drop_tol must be finite and non-negative"),
119 Self::InvalidFillControl => f.write_str("fill factor must be finite and positive"),
120 }
121 }
122}
123
124impl core::error::Error for IctError {}
125
126impl<I, T> LinOp<T> for Ict<I, T>
127where
128 I: Index,
129 T: ComplexField + Debug + Sync,
130{
131 fn apply_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
132 StackReq::EMPTY
133 }
134
135 fn nrows(&self) -> usize {
136 self.dim()
137 }
138
139 fn ncols(&self) -> usize {
140 self.dim()
141 }
142
143 fn apply(&self, mut out: MatMut<'_, T>, rhs: MatRef<'_, T>, par: Par, _stack: &mut MemStack) {
144 out.copy_from(rhs);
145 apply::solve_in_place(self, Conj::No, out, par);
146 }
147
148 fn conj_apply(
149 &self,
150 mut out: MatMut<'_, T>,
151 rhs: MatRef<'_, T>,
152 par: Par,
153 _stack: &mut MemStack,
154 ) {
155 out.copy_from(rhs);
156 apply::solve_in_place(self, Conj::Yes, out, par);
157 }
158}
159
160impl<I, T> Precond<T> for Ict<I, T>
161where
162 I: Index,
163 T: ComplexField + Debug + Sync,
164{
165 fn apply_in_place_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
166 StackReq::EMPTY
167 }
168
169 fn apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, _stack: &mut MemStack) {
170 apply::solve_in_place(self, Conj::No, rhs, par);
171 }
172
173 fn conj_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, _stack: &mut MemStack) {
174 apply::solve_in_place(self, Conj::Yes, rhs, par);
175 }
176}
177
178impl<I, T> BiLinOp<T> for Ict<I, T>
179where
180 I: Index,
181 T: ComplexField + Debug + Sync,
182{
183 fn transpose_apply_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
184 StackReq::EMPTY
185 }
186
187 fn transpose_apply(
188 &self,
189 mut out: MatMut<'_, T>,
190 rhs: MatRef<'_, T>,
191 par: Par,
192 _stack: &mut MemStack,
193 ) {
194 out.copy_from(rhs);
196 apply::solve_in_place(self, Conj::Yes, out, par);
197 }
198
199 fn adjoint_apply(
200 &self,
201 mut out: MatMut<'_, T>,
202 rhs: MatRef<'_, T>,
203 par: Par,
204 _stack: &mut MemStack,
205 ) {
206 out.copy_from(rhs);
208 apply::solve_in_place(self, Conj::No, out, par);
209 }
210}
211
212impl<I, T> BiPrecond<T> for Ict<I, T>
213where
214 I: Index,
215 T: ComplexField + Debug + Sync,
216{
217 fn transpose_apply_in_place_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
218 StackReq::EMPTY
219 }
220
221 fn transpose_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, _stack: &mut MemStack) {
222 apply::solve_in_place(self, Conj::Yes, rhs, par);
223 }
224
225 fn adjoint_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, _stack: &mut MemStack) {
226 apply::solve_in_place(self, Conj::No, rhs, par);
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use faer::sparse::{SparseColMat, Triplet};
234 use faer::{Mat, MatRef, mat};
235
236 fn assert_close(lhs: MatRef<'_, f64>, rhs: MatRef<'_, f64>, tol: f64) {
237 assert_eq!(lhs.nrows(), rhs.nrows());
238 assert_eq!(lhs.ncols(), rhs.ncols());
239 for j in 0..lhs.ncols() {
240 for i in 0..lhs.nrows() {
241 let diff = (*lhs.get(i, j) - *rhs.get(i, j)).abs();
242 assert!(
243 diff <= tol,
244 "mismatch at ({i}, {j}): lhs={}, rhs={}, diff={diff}",
245 *lhs.get(i, j),
246 *rhs.get(i, j),
247 );
248 }
249 }
250 }
251
252 fn to_dense(a: &SparseColMat<usize, f64>) -> Mat<f64> {
253 let n = a.nrows();
254 let mut out = Mat::<f64>::zeros(n, a.ncols());
255 let a_ref = a.as_ref();
256 for j in 0..a.ncols() {
257 let rows = a_ref.symbolic().row_idx_of_col_raw(j);
258 let vals = a_ref.val_of_col(j);
259 for (r, v) in rows.iter().zip(vals.iter()) {
260 *out.as_mut().get_mut(*r, j) = *v;
261 }
262 }
263 out
264 }
265
266 fn tridiagonal(n: usize, diag: f64, off: f64) -> SparseColMat<usize, f64> {
267 let mut triplets = Vec::new();
268 for i in 0..n {
269 triplets.push(Triplet::new(i, i, diag));
270 if i > 0 {
271 triplets.push(Triplet::new(i, i - 1, off));
272 triplets.push(Triplet::new(i - 1, i, off));
273 }
274 }
275 SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
276 }
277
278 fn laplacian_2d(grid: usize) -> SparseColMat<usize, f64> {
279 let n = grid * grid;
280 let mut triplets = Vec::new();
281 for gy in 0..grid {
282 for gx in 0..grid {
283 let idx = gy * grid + gx;
284 triplets.push(Triplet::new(idx, idx, 4.0));
285 if gx > 0 {
286 triplets.push(Triplet::new(idx, idx - 1, -1.0));
287 }
288 if gx + 1 < grid {
289 triplets.push(Triplet::new(idx, idx + 1, -1.0));
290 }
291 if gy > 0 {
292 triplets.push(Triplet::new(idx, idx - grid, -1.0));
293 }
294 if gy + 1 < grid {
295 triplets.push(Triplet::new(idx, idx + grid, -1.0));
296 }
297 }
298 }
299 SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
300 }
301
302 fn exact_params(n: usize) -> IctParams {
305 IctParams {
306 drop_tol: 0.0,
307 fill: FillControl::PerRow(n),
308 norm: RowNorm::Two,
309 }
310 }
311
312 #[test]
313 fn exact_keep_inverts_tridiagonal() {
314 let a = tridiagonal(7, 4.0, -1.0);
315 let pc = Ict::try_new_with_params(a.as_ref(), exact_params(7)).unwrap();
316 let a_dense = to_dense(&a);
317 let x_true = mat![[1.0], [-2.0], [3.0], [-1.0], [0.5], [2.0], [-1.5_f64]];
318 let mut rhs = (&a_dense * &x_true).to_owned();
319 pc.apply_in_place(rhs.as_mut(), Par::Seq, MemStack::new(&mut []));
320 assert_close(rhs.as_ref(), x_true.as_ref(), 1e-10);
321 }
322
323 #[test]
324 fn exact_keep_reconstructs_laplacian() {
325 let a = laplacian_2d(4);
327 let n = a.nrows();
328 let pc = Ict::try_new_with_params(a.as_ref(), exact_params(n)).unwrap();
329 let a_dense = to_dense(&a);
330 let x_true = Mat::<f64>::from_fn(n, 1, |i, _| (i % 5) as f64 - 2.0);
331 let mut rhs = (&a_dense * &x_true).to_owned();
332 pc.apply_in_place(rhs.as_mut(), Par::Seq, MemStack::new(&mut []));
333 assert_close(rhs.as_ref(), x_true.as_ref(), 1e-8);
334 }
335
336 #[test]
337 fn reduces_residual_on_laplacian() {
338 let a = laplacian_2d(8);
339 let n = a.nrows();
340 let pc = Ict::try_new(a.as_ref()).unwrap();
341 let a_dense = to_dense(&a);
342 let b = Mat::<f64>::from_fn(n, 1, |i, _| (i % 7) as f64 - 3.0);
343 let mut x = b.clone();
344 pc.apply_in_place(x.as_mut(), Par::Seq, MemStack::new(&mut []));
345 let residual = &a_dense * &x - &b;
346 let b_norm: f64 = b.as_ref().col(0).iter().map(|v| v * v).sum::<f64>().sqrt();
347 let r_norm: f64 = residual
348 .as_ref()
349 .col(0)
350 .iter()
351 .map(|v| v * v)
352 .sum::<f64>()
353 .sqrt();
354 assert!(r_norm / b_norm < 0.5, "ICT residual ratio too large");
355 }
356
357 #[test]
358 fn symmetric_transpose_equals_apply() {
359 let a = tridiagonal(6, 4.0, -1.0);
360 let pc = Ict::try_new(a.as_ref()).unwrap();
361 let rhs = mat![[1.0_f64], [-2.0], [3.0], [0.5], [-1.0], [2.0]];
362 let mut fwd = rhs.clone();
363 pc.apply_in_place(fwd.as_mut(), Par::Seq, MemStack::new(&mut []));
364 let mut tr = rhs.clone();
365 pc.transpose_apply_in_place(tr.as_mut(), Par::Seq, MemStack::new(&mut []));
366 assert_close(fwd.as_ref(), tr.as_ref(), 1e-12);
367 }
368
369 #[test]
370 fn refactorize_matches_fresh() {
371 let a1 = tridiagonal(7, 4.0, -1.0);
372 let a2 = tridiagonal(7, 5.0, -1.5);
373 let fresh = Ict::try_new(a2.as_ref()).unwrap();
374 let mut reused = Ict::try_new(a1.as_ref()).unwrap();
375 reused.refactorize(a2.as_ref()).unwrap();
376 assert_eq!(fresh.l_values.len(), reused.l_values.len());
377 for (a, b) in fresh.l_values.iter().zip(reused.l_values.iter()) {
378 assert!((a - b).abs() < 1e-12);
379 }
380 }
381
382 #[test]
383 fn rejects_non_positive_definite() {
384 let a = mat_to_sparse(&[&[1.0, 2.0], &[2.0, 1.0]]);
386 assert_eq!(
387 Ict::try_new(a.as_ref()).unwrap_err(),
388 IctError::NotPositiveDefinite { col: 1 }
389 );
390 }
391
392 #[test]
393 fn rejects_invalid_params() {
394 let a = tridiagonal(3, 4.0, -1.0);
395 let bad = IctParams {
396 drop_tol: -1.0,
397 ..Default::default()
398 };
399 assert_eq!(
400 Ict::try_new_with_params(a.as_ref(), bad).unwrap_err(),
401 IctError::InvalidDropTol
402 );
403 }
404
405 fn mat_to_sparse(rows: &[&[f64]]) -> SparseColMat<usize, f64> {
406 let n = rows.len();
407 let mut triplets = Vec::new();
408 for (i, row) in rows.iter().enumerate() {
409 for (j, &v) in row.iter().enumerate() {
410 if v != 0.0 {
411 triplets.push(Triplet::new(i, j, v));
412 }
413 }
414 }
415 SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
416 }
417}