scirs2_sparse/linalg/
preconditioners.rs

1//! Preconditioners for iterative solvers
2
3use crate::csr::CsrMatrix;
4use crate::error::{SparseError, SparseResult};
5use crate::linalg::interface::LinearOperator;
6use scirs2_core::numeric::{Float, NumAssign, SparseElement};
7use std::fmt::Debug;
8use std::iter::Sum;
9
10/// Jacobi (diagonal) preconditioner
11///
12/// This preconditioner uses the inverse of the diagonal of the matrix.
13/// M^(-1) = diag(1/a_11, 1/a_22, ..., 1/a_nn)
14pub struct JacobiPreconditioner<F> {
15    inv_diagonal: Vec<F>,
16    size: usize,
17}
18
19impl<F: Float + SparseElement> JacobiPreconditioner<F> {
20    /// Create a new Jacobi preconditioner from a sparse matrix
21    pub fn new(matrix: &CsrMatrix<F>) -> SparseResult<Self>
22    where
23        F: Debug,
24    {
25        let n = matrix.rows();
26        let mut inv_diagonal = vec![F::sparse_zero(); n];
27
28        // Extract diagonal elements
29        for (i, diag_inv) in inv_diagonal.iter_mut().enumerate().take(n) {
30            let diag_val = matrix.get(i, i);
31            if diag_val.abs() < F::epsilon() {
32                return Err(SparseError::ValueError(format!(
33                    "Zero diagonal element at position {i}"
34                )));
35            }
36            *diag_inv = F::sparse_one() / diag_val;
37        }
38
39        Ok(Self {
40            inv_diagonal,
41            size: n,
42        })
43    }
44
45    /// Create from diagonal values directly
46    pub fn from_diagonal(diagonal: Vec<F>) -> SparseResult<Self> {
47        let size = diagonal.len();
48        let mut inv_diagonal = vec![F::sparse_zero(); size];
49
50        for (i, &d) in diagonal.iter().enumerate() {
51            if d.abs() < F::epsilon() {
52                return Err(SparseError::ValueError(format!(
53                    "Zero _diagonal element at position {i}"
54                )));
55            }
56            inv_diagonal[i] = F::sparse_one() / d;
57        }
58
59        Ok(Self { inv_diagonal, size })
60    }
61}
62
63impl<F: Float + NumAssign + SparseElement> LinearOperator<F> for JacobiPreconditioner<F> {
64    fn shape(&self) -> (usize, usize) {
65        (self.size, self.size)
66    }
67
68    fn matvec(&self, x: &[F]) -> SparseResult<Vec<F>> {
69        if x.len() != self.size {
70            return Err(SparseError::DimensionMismatch {
71                expected: self.size,
72                found: x.len(),
73            });
74        }
75
76        Ok(x.iter()
77            .zip(&self.inv_diagonal)
78            .map(|(&xi, &di)| xi * di)
79            .collect())
80    }
81
82    fn has_adjoint(&self) -> bool {
83        true
84    }
85
86    fn rmatvec(&self, x: &[F]) -> SparseResult<Vec<F>> {
87        // For real diagonal matrices, adjoint is the same as forward operation
88        self.matvec(x)
89    }
90}
91
92/// Symmetric Successive Over-Relaxation (SSOR) preconditioner
93///
94/// This preconditioner uses the SSOR method with a relaxation parameter omega.
95/// M = (D + ωL) * D^(-1) * (D + ωU)
96pub struct SSORPreconditioner<F> {
97    matrix: CsrMatrix<F>,
98    omega: F,
99    diagonal: Vec<F>,
100}
101
102impl<F: Float + NumAssign + Sum + Debug + SparseElement + 'static> SSORPreconditioner<F> {
103    /// Create a new SSOR preconditioner
104    pub fn new(matrix: CsrMatrix<F>, omega: F) -> SparseResult<Self> {
105        if omega <= F::sparse_zero() || omega >= F::from(2.0).unwrap() {
106            return Err(SparseError::ValueError(
107                "Relaxation parameter omega must be in (0, 2)".to_string(),
108            ));
109        }
110
111        let n = matrix.rows();
112        let mut diagonal = vec![F::sparse_zero(); n];
113
114        // Extract diagonal elements
115        for (i, diag) in diagonal.iter_mut().enumerate().take(n) {
116            *diag = matrix.get(i, i);
117            if diag.abs() < F::epsilon() {
118                return Err(SparseError::ValueError(format!(
119                    "Zero diagonal element at position {i}"
120                )));
121            }
122        }
123
124        Ok(Self {
125            matrix,
126            omega,
127            diagonal,
128        })
129    }
130}
131
132impl<F: Float + NumAssign + Sum + Debug + SparseElement + 'static> LinearOperator<F>
133    for SSORPreconditioner<F>
134{
135    fn shape(&self) -> (usize, usize) {
136        self.matrix.shape()
137    }
138
139    fn matvec(&self, x: &[F]) -> SparseResult<Vec<F>> {
140        let n = self.matrix.rows();
141        if x.len() != n {
142            return Err(SparseError::DimensionMismatch {
143                expected: n,
144                found: x.len(),
145            });
146        }
147
148        // Forward sweep: solve (D + ωL)y = x
149        let mut y = vec![F::sparse_zero(); n];
150        for i in 0..n {
151            let mut sum = x[i];
152            let row_range = self.matrix.row_range(i);
153            let row_indices = &self.matrix.indices[row_range.clone()];
154            let row_data = &self.matrix.data[row_range];
155
156            for (idx, &j) in row_indices.iter().enumerate() {
157                if j < i {
158                    sum -= self.omega * row_data[idx] * y[j];
159                }
160            }
161            y[i] = sum / self.diagonal[i];
162        }
163
164        // Diagonal scaling: z = D^(-1) * y
165        let mut z = vec![F::sparse_zero(); n];
166        for i in 0..n {
167            z[i] = y[i] * self.diagonal[i];
168        }
169
170        // Backward sweep: solve (D + ωU)w = z
171        let mut w = vec![F::sparse_zero(); n];
172        for i in (0..n).rev() {
173            let mut sum = z[i];
174            let row_range = self.matrix.row_range(i);
175            let row_indices = &self.matrix.indices[row_range.clone()];
176            let row_data = &self.matrix.data[row_range];
177
178            for (idx, &j) in row_indices.iter().enumerate() {
179                if j > i {
180                    sum -= self.omega * row_data[idx] * w[j];
181                }
182            }
183            w[i] = sum / self.diagonal[i];
184        }
185
186        Ok(w)
187    }
188
189    fn has_adjoint(&self) -> bool {
190        false // SSOR is not generally self-adjoint
191    }
192}
193
194/// Incomplete LU factorization with zero fill-in (ILU(0))
195///
196/// This preconditioner computes an incomplete LU factorization
197/// where L and U have the same sparsity pattern as the original matrix.
198pub struct ILU0Preconditioner<F> {
199    l_data: Vec<F>,
200    u_data: Vec<F>,
201    l_indices: Vec<usize>,
202    u_indices: Vec<usize>,
203    l_indptr: Vec<usize>,
204    u_indptr: Vec<usize>,
205    n: usize,
206}
207
208impl<F: Float + NumAssign + Sum + Debug + SparseElement + 'static> ILU0Preconditioner<F> {
209    /// Create a new ILU(0) preconditioner
210    pub fn new(matrix: &CsrMatrix<F>) -> SparseResult<Self> {
211        let n = matrix.rows();
212
213        // Copy the _matrix data for modification
214        let mut data = matrix.data.clone();
215        let indices = matrix.indices.clone();
216        let indptr = matrix.indptr.clone();
217
218        // Perform ILU(0) factorization
219        for k in 0..n {
220            let k_diag_idx = find_diagonal_index(&indices, &indptr, k)?;
221            let k_diag = data[k_diag_idx];
222
223            if k_diag.abs() < F::epsilon() {
224                return Err(SparseError::ValueError(format!(
225                    "Zero pivot at position {k}"
226                )));
227            }
228
229            // Update rows k+1 to n-1
230            for i in (k + 1)..n {
231                let row_start = indptr[i];
232                let row_end = indptr[i + 1];
233
234                // Find position of k in row i
235                let mut k_pos = None;
236                for (&col, j) in indices[row_start..row_end].iter().zip(row_start..row_end) {
237                    if col == k {
238                        k_pos = Some(j);
239                        break;
240                    }
241                    if col > k {
242                        break;
243                    }
244                }
245
246                if let Some(ki_idx) = k_pos {
247                    // Compute multiplier
248                    let mult = data[ki_idx] / k_diag;
249                    data[ki_idx] = mult;
250
251                    // Update the rest of row i
252                    let k_row_start = indptr[k];
253                    let k_row_end = indptr[k + 1];
254
255                    for kj_idx in k_row_start..k_row_end {
256                        let j = indices[kj_idx];
257                        if j <= k {
258                            continue;
259                        }
260
261                        // Find position of j in row i
262                        for ij_idx in row_start..row_end {
263                            if indices[ij_idx] == j {
264                                data[ij_idx] = data[ij_idx] - mult * data[kj_idx];
265                                break;
266                            }
267                        }
268                    }
269                }
270            }
271        }
272
273        // Split into L and U parts
274        let mut l_data = Vec::new();
275        let mut u_data = Vec::new();
276        let mut l_indices = Vec::new();
277        let mut u_indices = Vec::new();
278        let mut l_indptr = vec![0];
279        let mut u_indptr = vec![0];
280
281        for i in 0..n {
282            let row_start = indptr[i];
283            let row_end = indptr[i + 1];
284
285            for j in row_start..row_end {
286                let col = indices[j];
287                let val = data[j];
288
289                match col.cmp(&i) {
290                    std::cmp::Ordering::Less => {
291                        // Lower triangular part
292                        l_indices.push(col);
293                        l_data.push(val);
294                    }
295                    std::cmp::Ordering::Equal => {
296                        // Diagonal (goes to U)
297                        u_indices.push(col);
298                        u_data.push(val);
299                    }
300                    std::cmp::Ordering::Greater => {
301                        // Upper triangular part
302                        u_indices.push(col);
303                        u_data.push(val);
304                    }
305                }
306            }
307
308            l_indptr.push(l_indices.len());
309            u_indptr.push(u_indices.len());
310        }
311
312        Ok(Self {
313            l_data,
314            u_data,
315            l_indices,
316            u_indices,
317            l_indptr,
318            u_indptr,
319            n,
320        })
321    }
322}
323
324impl<F: Float + NumAssign + Sum + Debug + SparseElement + 'static> LinearOperator<F>
325    for ILU0Preconditioner<F>
326{
327    fn shape(&self) -> (usize, usize) {
328        (self.n, self.n)
329    }
330
331    fn matvec(&self, x: &[F]) -> SparseResult<Vec<F>> {
332        if x.len() != self.n {
333            return Err(SparseError::DimensionMismatch {
334                expected: self.n,
335                found: x.len(),
336            });
337        }
338
339        // Solve Ly = x (forward substitution)
340        let mut y = vec![F::sparse_zero(); self.n];
341        for i in 0..self.n {
342            y[i] = x[i];
343            let row_start = self.l_indptr[i];
344            let row_end = self.l_indptr[i + 1];
345
346            for j in row_start..row_end {
347                let col = self.l_indices[j];
348                y[i] = y[i] - self.l_data[j] * y[col];
349            }
350        }
351
352        // Solve Uz = y (backward substitution)
353        let mut z = vec![F::sparse_zero(); self.n];
354        for i in (0..self.n).rev() {
355            z[i] = y[i];
356            let row_start = self.u_indptr[i];
357            let row_end = self.u_indptr[i + 1];
358
359            let mut diag_val = F::sparse_one();
360            for j in row_start..row_end {
361                let col = self.u_indices[j];
362                match col.cmp(&i) {
363                    std::cmp::Ordering::Equal => diag_val = self.u_data[j],
364                    std::cmp::Ordering::Greater => z[i] = z[i] - self.u_data[j] * z[col],
365                    std::cmp::Ordering::Less => {}
366                }
367            }
368
369            z[i] /= diag_val;
370        }
371
372        Ok(z)
373    }
374
375    fn has_adjoint(&self) -> bool {
376        false // ILU is not generally self-adjoint
377    }
378}
379
380// Helper function to find diagonal index in CSR format
381#[allow(dead_code)]
382fn find_diagonal_index(indices: &[usize], indptr: &[usize], row: usize) -> SparseResult<usize> {
383    let row_start = indptr[row];
384    let row_end = indptr[row + 1];
385
386    for (idx, &col) in indices[row_start..row_end]
387        .iter()
388        .enumerate()
389        .map(|(i, col)| (i + row_start, col))
390    {
391        if col == row {
392            return Ok(idx);
393        }
394    }
395
396    Err(SparseError::ValueError(format!(
397        "Missing diagonal element at row {row}"
398    )))
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404
405    #[test]
406    fn test_jacobi_preconditioner() {
407        // Create a diagonal matrix
408        let diagonal = vec![2.0, 3.0, 4.0];
409        let precond = JacobiPreconditioner::from_diagonal(diagonal).unwrap();
410
411        // Test application
412        let x = vec![2.0, 6.0, 12.0];
413        let result = precond.matvec(&x).unwrap();
414
415        // Should be [1.0, 2.0, 3.0]
416        assert!((result[0] - 1.0).abs() < 1e-10);
417        assert!((result[1] - 2.0).abs() < 1e-10);
418        assert!((result[2] - 3.0).abs() < 1e-10);
419    }
420}