Skip to main content

faer_precond/ic0/
numeric.rs

1//! Numeric IC(0) factorisation.
2
3use faer::sparse::SparseColMatRef;
4use faer_traits::math_utils::{conj, copy, from_real, mul, real, recip, sqrt, sub, zero};
5use faer_traits::{ComplexField, Index};
6
7use super::Ic0Error;
8use super::symbolic::SymbolicIc0;
9
10/// Numeric IC(0) factor of a Hermitian positive-definite CSC matrix `A`.
11///
12/// Holds the `L` value buffer together with the symbolic structure and the
13/// dense workspace used during refactorisation. After construction the
14/// factor can be refactored against any matrix with the same lower-triangle
15/// pattern — [`Ic0::refactorize`] performs zero heap allocations.
16#[derive(Debug, Clone)]
17pub struct Ic0<I, T> {
18    pub(crate) symbolic: SymbolicIc0<I>,
19    pub(crate) l_values: Vec<T>,
20    pub(crate) workspace_w: Vec<T>,
21    pub(crate) workspace_marker: Vec<usize>,
22}
23
24impl<I: Index, T: ComplexField> Ic0<I, T> {
25    /// Allocate the value buffer and dense workspace for a given symbolic factor.
26    ///
27    /// The returned factor is *not* yet populated — call [`Ic0::refactorize`]
28    /// to fill it with values from a matrix matching `symbolic`'s pattern.
29    pub fn new_with_symbolic(symbolic: SymbolicIc0<I>) -> Self {
30        let n = symbolic.dim;
31        let l_nnz = symbolic.l_nnz();
32        let l_values = (0..l_nnz).map(|_| zero::<T>()).collect();
33        let workspace_w = (0..n).map(|_| zero::<T>()).collect();
34        let workspace_marker = vec![usize::MAX; n];
35        Self {
36            symbolic,
37            l_values,
38            workspace_w,
39            workspace_marker,
40        }
41    }
42
43    /// Build a fully populated IC(0) factor from a Hermitian PD CSC matrix `A`.
44    ///
45    /// Only the lower triangle of `A` is consumed — values above the diagonal
46    /// are silently ignored. Allocates internally for the symbolic structure,
47    /// the `L` value buffer, and the dense workspace.
48    pub fn try_new(a: SparseColMatRef<'_, I, T>) -> Result<Self, Ic0Error> {
49        let symbolic = SymbolicIc0::try_new(a.symbolic())?;
50        let mut me = Self::new_with_symbolic(symbolic);
51        me.refactorize(a)?;
52        Ok(me)
53    }
54
55    /// Dimension `n` of the factored matrix.
56    #[inline]
57    pub fn dim(&self) -> usize {
58        self.symbolic.dim
59    }
60
61    /// Borrow the symbolic factor.
62    #[inline]
63    pub fn symbolic(&self) -> &SymbolicIc0<I> {
64        &self.symbolic
65    }
66
67    /// Refactorise against a new matrix with the same sparsity pattern.
68    ///
69    /// Performs zero heap allocations.
70    ///
71    /// # Errors
72    ///
73    /// - [`Ic0Error::PatternMismatch`] if `a`'s shape or column lengths
74    ///   disagree with the symbolic structure, or if the diagonal entries
75    ///   land at unexpected positions.
76    /// - [`Ic0Error::NotPositiveDefinite`] if a non-positive pivot is
77    ///   encountered (indicating the matrix is not positive definite, or that
78    ///   the IC(0) algorithm has broken down on a non-H-matrix input).
79    pub fn refactorize(&mut self, a: SparseColMatRef<'_, I, T>) -> Result<(), Ic0Error> {
80        let n = self.symbolic.dim;
81        if a.nrows() != n || a.ncols() != n {
82            return Err(Ic0Error::PatternMismatch);
83        }
84        let a_sym = a.symbolic();
85
86        // Stage 1: copy A's lower triangle into L slots.
87        for j in 0..n {
88            let a_row_idx = a_sym.row_idx_of_col_raw(j);
89            let a_col_len = a_row_idx.len();
90            let d = self.symbolic.diag_pos[j];
91            if d >= a_col_len || a_row_idx[d].zx() != j {
92                return Err(Ic0Error::PatternMismatch);
93            }
94            let l_start = self.symbolic.l_col_ptr[j].zx();
95            let l_end = self.symbolic.l_col_ptr[j + 1].zx();
96            if l_end - l_start != a_col_len - d {
97                return Err(Ic0Error::PatternMismatch);
98            }
99
100            let a_val = a.val_of_col(j);
101            for (dst, val) in self.l_values[l_start..l_end]
102                .iter_mut()
103                .zip(a_val[d..].iter())
104            {
105                *dst = copy(val);
106            }
107        }
108
109        // Stage 2: IC(0) elimination — column-by-column, left-looking.
110        let symbolic = &self.symbolic;
111        let w = self.workspace_w.as_mut_slice();
112        let marker = self.workspace_marker.as_mut_slice();
113        let l_values = self.l_values.as_mut_slice();
114
115        for j in 0..n {
116            let l_start = symbolic.l_col_ptr[j].zx();
117            let l_end = symbolic.l_col_ptr[j + 1].zx();
118
119            // Scatter L col j (currently containing A's lower triangle) into w.
120            for (raw_row, val) in symbolic.l_row_idx[l_start..l_end]
121                .iter()
122                .zip(l_values[l_start..l_end].iter())
123            {
124                let i = raw_row.zx();
125                marker[i] = j;
126                w[i] = copy(val);
127            }
128
129            // For each k < j with L[j, k] in pattern, subtract
130            // L[i, k] * conj(L[j, k]) from w[i] at every i >= j with
131            // (i, j) ∈ pattern(L col j).
132            let rcp_start = symbolic.row_col_ptr[j];
133            let rcp_end = symbolic.row_col_ptr[j + 1];
134            for (raw_k, &l_jk_pos) in symbolic.row_col_idx[rcp_start..rcp_end]
135                .iter()
136                .zip(symbolic.row_lpos[rcp_start..rcp_end].iter())
137            {
138                let k = raw_k.zx();
139                let lk_end = symbolic.l_col_ptr[k + 1].zx();
140                let conj_l_jk = conj(&l_values[l_jk_pos]);
141                // Range [l_jk_pos..lk_end] covers L col k rows >= j (including i = j,
142                // which contributes -|L[j,k]|^2 to w[j]).
143                for (raw_row, lv) in symbolic.l_row_idx[l_jk_pos..lk_end]
144                    .iter()
145                    .zip(l_values[l_jk_pos..lk_end].iter())
146                {
147                    let i = raw_row.zx();
148                    if marker[i] == j {
149                        let upd = mul(lv, &conj_l_jk);
150                        w[i] = sub(&w[i], &upd);
151                    }
152                }
153            }
154
155            // Pivot: A[j,j] - sum_k |L[j,k]|^2 must be real and positive.
156            // `partial_cmp` catches NaN as well as zero/negative.
157            let pivot_real = real(&w[j]);
158            if pivot_real.partial_cmp(&zero::<T::Real>()) != Some(core::cmp::Ordering::Greater) {
159                return Err(Ic0Error::NotPositiveDefinite { col: j });
160            }
161            let pivot_root = sqrt::<T::Real>(&pivot_real);
162            let pivot = from_real::<T>(&pivot_root);
163            let pivot_inv = recip(&pivot);
164
165            // Gather L col j: diagonal slot gets pivot, off-diagonal entries
166            // get w[i] / pivot.
167            l_values[l_start] = pivot;
168            for (raw_row, dst) in symbolic.l_row_idx[l_start + 1..l_end]
169                .iter()
170                .zip(l_values[l_start + 1..l_end].iter_mut())
171            {
172                let i = raw_row.zx();
173                *dst = mul(&w[i], &pivot_inv);
174            }
175        }
176
177        Ok(())
178    }
179
180    /// Construct a [`SparseColMatRef`] view over the `L` factor (lower
181    /// triangular, diagonal stored first in each column with the positive
182    /// real pivot value).
183    #[inline]
184    pub fn l_view(&self) -> SparseColMatRef<'_, I, T> {
185        let symbolic = unsafe {
186            faer::sparse::SymbolicSparseColMatRef::<'_, I>::new_unchecked(
187                self.symbolic.dim,
188                self.symbolic.dim,
189                &self.symbolic.l_col_ptr,
190                None,
191                &self.symbolic.l_row_idx,
192            )
193        };
194        SparseColMatRef::new(symbolic, &self.l_values)
195    }
196}