solvers/preconditioners/
ilu.rs

1//! ILU(0) preconditioner
2//!
3//! Incomplete LU factorization with no fill-in.
4//! Effective for many sparse systems, especially those arising from discretization.
5
6use crate::sparse::CsrMatrix;
7use crate::traits::{ComplexField, Preconditioner};
8use ndarray::Array1;
9use num_traits::FromPrimitive;
10
11/// ILU(0) preconditioner
12///
13/// Computes an incomplete LU factorization where the sparsity pattern
14/// of L and U is the same as the original matrix.
15#[derive(Debug, Clone)]
16pub struct IluPreconditioner<T: ComplexField> {
17    /// Lower triangular factor (stored in CSR format conceptually)
18    l_values: Vec<T>,
19    l_col_indices: Vec<usize>,
20    l_row_ptrs: Vec<usize>,
21    /// Upper triangular factor
22    u_values: Vec<T>,
23    u_col_indices: Vec<usize>,
24    u_row_ptrs: Vec<usize>,
25    /// Diagonal of U (for fast access)
26    u_diag: Vec<T>,
27    /// Matrix dimension
28    n: usize,
29}
30
31impl<T: ComplexField> IluPreconditioner<T> {
32    /// Create ILU(0) preconditioner from a CSR matrix
33    pub fn from_csr(matrix: &CsrMatrix<T>) -> Self {
34        let n = matrix.num_rows;
35
36        // For ILU(0), we use the same sparsity pattern as A
37        // L is strictly lower triangular with unit diagonal
38        // U is upper triangular
39
40        // Initialize with a copy of the matrix
41        let mut values = matrix.values.clone();
42        let col_indices = matrix.col_indices.clone();
43        let row_ptrs = matrix.row_ptrs.clone();
44
45        // Perform incomplete factorization
46        for i in 0..n {
47            // Skip diagonal detection - we process all entries below
48
49            // For each row k < i where a_ik != 0
50            for idx in row_ptrs[i]..row_ptrs[i + 1] {
51                let k = col_indices[idx];
52                if k >= i {
53                    break;
54                }
55
56                // Find u_kk (diagonal of U in row k)
57                let mut u_kk = T::zero();
58                for k_idx in row_ptrs[k]..row_ptrs[k + 1] {
59                    if col_indices[k_idx] == k {
60                        u_kk = values[k_idx];
61                        break;
62                    }
63                }
64
65                if u_kk.norm() < T::Real::from_f64(1e-30).unwrap() {
66                    continue;
67                }
68
69                // Compute l_ik = a_ik / u_kk
70                let l_ik = values[idx] * u_kk.inv();
71                values[idx] = l_ik;
72
73                // Update a_ij for j > k where both a_ij and u_kj exist
74                for j_idx in row_ptrs[i]..row_ptrs[i + 1] {
75                    let j = col_indices[j_idx];
76                    if j <= k {
77                        continue;
78                    }
79
80                    // Find u_kj
81                    for k_j_idx in row_ptrs[k]..row_ptrs[k + 1] {
82                        if col_indices[k_j_idx] == j {
83                            values[j_idx] = values[j_idx] - l_ik * values[k_j_idx];
84                            break;
85                        }
86                    }
87                }
88            }
89        }
90
91        // Extract L and U separately
92        let mut l_values = Vec::new();
93        let mut l_col_indices = Vec::new();
94        let mut l_row_ptrs = vec![0];
95
96        let mut u_values = Vec::new();
97        let mut u_col_indices = Vec::new();
98        let mut u_row_ptrs = vec![0];
99        let mut u_diag = vec![T::one(); n];
100
101        for i in 0..n {
102            for idx in row_ptrs[i]..row_ptrs[i + 1] {
103                let j = col_indices[idx];
104                let val = values[idx];
105
106                if j < i {
107                    // Lower triangular (L)
108                    l_values.push(val);
109                    l_col_indices.push(j);
110                } else {
111                    // Upper triangular (U)
112                    u_values.push(val);
113                    u_col_indices.push(j);
114                    if j == i {
115                        u_diag[i] = val;
116                    }
117                }
118            }
119            l_row_ptrs.push(l_values.len());
120            u_row_ptrs.push(u_values.len());
121        }
122
123        Self {
124            l_values,
125            l_col_indices,
126            l_row_ptrs,
127            u_values,
128            u_col_indices,
129            u_row_ptrs,
130            u_diag,
131            n,
132        }
133    }
134}
135
136impl<T: ComplexField> Preconditioner<T> for IluPreconditioner<T> {
137    fn apply(&self, r: &Array1<T>) -> Array1<T> {
138        let mut y = r.clone();
139
140        // Forward substitution: Ly = r (L has unit diagonal)
141        for i in 0..self.n {
142            for idx in self.l_row_ptrs[i]..self.l_row_ptrs[i + 1] {
143                let j = self.l_col_indices[idx];
144                let l_ij = self.l_values[idx];
145                y[i] = y[i] - l_ij * y[j];
146            }
147        }
148
149        // Backward substitution: Ux = y
150        let mut x = y;
151        for i in (0..self.n).rev() {
152            for idx in self.u_row_ptrs[i]..self.u_row_ptrs[i + 1] {
153                let j = self.u_col_indices[idx];
154                if j > i {
155                    let u_ij = self.u_values[idx];
156                    x[i] = x[i] - u_ij * x[j];
157                }
158            }
159
160            let u_ii = self.u_diag[i];
161            if u_ii.norm() > T::Real::from_f64(1e-30).unwrap() {
162                x[i] *= u_ii.inv();
163            }
164        }
165
166        x
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use crate::iterative::{GmresConfig, gmres, gmres_preconditioned};
174    use ndarray::array;
175    use num_complex::Complex64;
176
177    #[test]
178    fn test_ilu_preconditioner() {
179        // Simple tridiagonal matrix
180        let dense = array![
181            [
182                Complex64::new(4.0, 0.0),
183                Complex64::new(-1.0, 0.0),
184                Complex64::new(0.0, 0.0)
185            ],
186            [
187                Complex64::new(-1.0, 0.0),
188                Complex64::new(4.0, 0.0),
189                Complex64::new(-1.0, 0.0)
190            ],
191            [
192                Complex64::new(0.0, 0.0),
193                Complex64::new(-1.0, 0.0),
194                Complex64::new(4.0, 0.0)
195            ],
196        ];
197
198        let matrix = CsrMatrix::from_dense(&dense, 1e-15);
199        let precond = IluPreconditioner::from_csr(&matrix);
200
201        let r = array![
202            Complex64::new(1.0, 0.0),
203            Complex64::new(2.0, 0.0),
204            Complex64::new(3.0, 0.0)
205        ];
206
207        let result = precond.apply(&r);
208
209        // Verify that M*result ≈ r (where M = L*U)
210        // For ILU(0) on this matrix, M should be close to A
211        let check = matrix.matvec(&result);
212        for i in 0..3 {
213            // Allow some error since ILU is approximate
214            assert!(
215                (check[i] - r[i]).norm() < 0.5,
216                "ILU should approximately invert: got {:?} expected {:?}",
217                check[i],
218                r[i]
219            );
220        }
221    }
222
223    #[test]
224    fn test_ilu_with_gmres() {
225        // Larger system where preconditioning helps
226        let n = 10;
227        let mut dense = ndarray::Array2::from_elem((n, n), Complex64::new(0.0, 0.0));
228
229        // Tridiagonal matrix
230        for i in 0..n {
231            dense[[i, i]] = Complex64::new(4.0, 0.0);
232            if i > 0 {
233                dense[[i, i - 1]] = Complex64::new(-1.0, 0.0);
234            }
235            if i < n - 1 {
236                dense[[i, i + 1]] = Complex64::new(-1.0, 0.0);
237            }
238        }
239
240        let matrix = CsrMatrix::from_dense(&dense, 1e-15);
241        let precond = IluPreconditioner::from_csr(&matrix);
242
243        let b = Array1::from_iter((0..n).map(|i| Complex64::new((i as f64).sin(), 0.0)));
244
245        let config = GmresConfig {
246            max_iterations: 50,
247            restart: 10,
248            tolerance: 1e-10,
249            print_interval: 0,
250        };
251
252        // Solve without preconditioning
253        let sol_no_precond = gmres(&matrix, &b, &config);
254
255        // Solve with preconditioning
256        let sol_precond = gmres_preconditioned(&matrix, &precond, &b, &config);
257
258        // Both should converge
259        assert!(sol_no_precond.converged);
260        assert!(sol_precond.converged);
261
262        // Preconditioned should need fewer iterations (usually)
263        // For this well-conditioned problem, difference may be small
264        assert!(
265            sol_precond.iterations <= sol_no_precond.iterations + 5,
266            "Preconditioning should not hurt: {} vs {}",
267            sol_precond.iterations,
268            sol_no_precond.iterations
269        );
270    }
271}