1use core::fmt::Debug;
23
24use dyn_stack::{MemStack, StackReq};
25use faer::{
26 MatMut, MatRef, Par,
27 matrix_free::{BiLinOp, BiPrecond, LinOp, Precond},
28 prelude::ReborrowMut,
29};
30use faer_traits::ComplexField;
31use faer_traits::math_utils::{abs2, conj, copy, mul, recip, zero};
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum JacobiError {
36 NonSquareMatrix { nrows: usize, ncols: usize },
38 ZeroDiagonalEntry { index: usize },
40}
41
42#[derive(Debug, Clone)]
48pub struct JacobiPrecond<T> {
49 inv_diag: Vec<T>,
50}
51
52impl<T> JacobiPrecond<T> {
53 pub fn from_inverse_diagonal(inv_diag: Vec<T>) -> Self {
58 Self { inv_diag }
59 }
60
61 pub fn inverse_diagonal(&self) -> &[T] {
63 &self.inv_diag
64 }
65
66 pub fn dim(&self) -> usize {
68 self.inv_diag.len()
69 }
70
71 pub fn is_empty(&self) -> bool {
73 self.inv_diag.is_empty()
74 }
75}
76
77impl<T: ComplexField> JacobiPrecond<T> {
78 pub fn try_from_diagonal(diag: &[T]) -> Result<Self, JacobiError> {
84 let mut inv_diag = Vec::with_capacity(diag.len());
85
86 for (index, value) in diag.iter().enumerate() {
87 if abs2(value) == zero::<T::Real>() {
88 return Err(JacobiError::ZeroDiagonalEntry { index });
89 }
90 inv_diag.push(recip(value));
91 }
92
93 Ok(Self { inv_diag })
94 }
95
96 pub fn try_from_matrix_diagonal(mat: MatRef<'_, T>) -> Result<Self, JacobiError> {
103 if mat.nrows() != mat.ncols() {
104 return Err(JacobiError::NonSquareMatrix {
105 nrows: mat.nrows(),
106 ncols: mat.ncols(),
107 });
108 }
109
110 let mut diag = Vec::with_capacity(mat.nrows());
111 for i in 0..mat.nrows() {
112 diag.push(copy(mat.get(i, i)));
113 }
114
115 Self::try_from_diagonal(&diag)
116 }
117
118 #[inline]
119 fn check_dims(&self, out_nrows: usize, rhs_nrows: usize, rhs_ncols: usize, out_ncols: usize) {
120 assert_eq!(
121 rhs_nrows,
122 self.dim(),
123 "rhs row count must match preconditioner dimension"
124 );
125 assert_eq!(
126 out_nrows,
127 self.dim(),
128 "out row count must match preconditioner dimension"
129 );
130 assert_eq!(
131 out_ncols, rhs_ncols,
132 "out and rhs must have the same number of columns"
133 );
134 }
135
136 #[inline]
137 fn apply_scale_to_out(&self, mut out: MatMut<'_, T>, rhs: MatRef<'_, T>, conjugate_diag: bool) {
138 self.check_dims(out.nrows(), rhs.nrows(), rhs.ncols(), out.ncols());
139
140 for j in 0..rhs.ncols() {
141 for i in 0..rhs.nrows() {
142 let scale = if conjugate_diag {
143 conj(&self.inv_diag[i])
144 } else {
145 copy(&self.inv_diag[i])
146 };
147 *out.rb_mut().get_mut(i, j) = mul(&scale, rhs.get(i, j));
148 }
149 }
150 }
151
152 #[inline]
153 fn apply_scale_in_place(&self, mut rhs: MatMut<'_, T>, conjugate_diag: bool) {
154 assert_eq!(
155 rhs.nrows(),
156 self.dim(),
157 "rhs row count must match preconditioner dimension"
158 );
159
160 for j in 0..rhs.ncols() {
161 for i in 0..rhs.nrows() {
162 let scale = if conjugate_diag {
163 conj(&self.inv_diag[i])
164 } else {
165 copy(&self.inv_diag[i])
166 };
167 let elem = rhs.rb_mut().get_mut(i, j);
168 *elem = mul(&scale, elem);
169 }
170 }
171 }
172}
173
174impl<T> LinOp<T> for JacobiPrecond<T>
175where
176 T: ComplexField + Debug + Sync,
177{
178 fn apply_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
179 StackReq::EMPTY
180 }
181
182 fn nrows(&self) -> usize {
183 self.dim()
184 }
185
186 fn ncols(&self) -> usize {
187 self.dim()
188 }
189
190 fn apply(&self, out: MatMut<'_, T>, rhs: MatRef<'_, T>, _par: Par, _stack: &mut MemStack) {
191 self.apply_scale_to_out(out, rhs, false);
192 }
193
194 fn conj_apply(&self, out: MatMut<'_, T>, rhs: MatRef<'_, T>, _par: Par, _stack: &mut MemStack) {
195 self.apply_scale_to_out(out, rhs, true);
196 }
197}
198
199impl<T> Precond<T> for JacobiPrecond<T>
200where
201 T: ComplexField + Debug + Sync,
202{
203 fn apply_in_place_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
204 StackReq::EMPTY
205 }
206
207 fn apply_in_place(&self, rhs: MatMut<'_, T>, _par: Par, _stack: &mut MemStack) {
208 self.apply_scale_in_place(rhs, false);
209 }
210
211 fn conj_apply_in_place(&self, rhs: MatMut<'_, T>, _par: Par, _stack: &mut MemStack) {
212 self.apply_scale_in_place(rhs, true);
213 }
214}
215
216impl<T> BiLinOp<T> for JacobiPrecond<T>
217where
218 T: ComplexField + Debug + Sync,
219{
220 fn transpose_apply_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
221 StackReq::EMPTY
222 }
223
224 fn transpose_apply(
225 &self,
226 out: MatMut<'_, T>,
227 rhs: MatRef<'_, T>,
228 _par: Par,
229 _stack: &mut MemStack,
230 ) {
231 self.apply_scale_to_out(out, rhs, false);
233 }
234
235 fn adjoint_apply(
236 &self,
237 out: MatMut<'_, T>,
238 rhs: MatRef<'_, T>,
239 _par: Par,
240 _stack: &mut MemStack,
241 ) {
242 self.apply_scale_to_out(out, rhs, true);
244 }
245}
246
247impl<T> BiPrecond<T> for JacobiPrecond<T>
248where
249 T: ComplexField + Debug + Sync,
250{
251 fn transpose_apply_in_place_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
252 StackReq::EMPTY
253 }
254
255 fn transpose_apply_in_place(&self, rhs: MatMut<'_, T>, _par: Par, _stack: &mut MemStack) {
256 self.apply_scale_in_place(rhs, false);
257 }
258
259 fn adjoint_apply_in_place(&self, rhs: MatMut<'_, T>, _par: Par, _stack: &mut MemStack) {
260 self.apply_scale_in_place(rhs, true);
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use core::mem::MaybeUninit;
267
268 use super::*;
269 use faer::{
270 Mat, MatRef, mat,
271 matrix_free::{BiLinOp, BiPrecond, LinOp, Precond},
272 };
273
274 fn with_stack(req: StackReq, f: impl FnOnce(&mut MemStack)) {
275 let nbytes = req.unaligned_bytes_required().max(1);
276 let mut buf = vec![MaybeUninit::<u8>::uninit(); nbytes].into_boxed_slice();
277 f(MemStack::new(&mut buf));
278 }
279
280 fn assert_close(lhs: MatRef<'_, f64>, rhs: MatRef<'_, f64>, tol: f64) {
281 assert_eq!(lhs.nrows(), rhs.nrows());
282 assert_eq!(lhs.ncols(), rhs.ncols());
283
284 for j in 0..lhs.ncols() {
285 for i in 0..lhs.nrows() {
286 let diff = (*lhs.get(i, j) - *rhs.get(i, j)).abs();
287 assert!(
288 diff <= tol,
289 "mismatch at ({i}, {j}): lhs={}, rhs={}, diff={diff}",
290 *lhs.get(i, j),
291 *rhs.get(i, j),
292 );
293 }
294 }
295 }
296
297 #[test]
298 fn builds_from_diagonal() {
299 let pc = JacobiPrecond::try_from_diagonal(&[2.0, 4.0, 8.0]).unwrap();
300 assert_eq!(pc.dim(), 3);
301 assert_eq!(pc.inverse_diagonal(), &[0.5, 0.25, 0.125]);
302 }
303
304 #[test]
305 fn rejects_zero_diagonal() {
306 let err = JacobiPrecond::try_from_diagonal(&[2.0, 0.0, 8.0]).unwrap_err();
307 assert_eq!(err, JacobiError::ZeroDiagonalEntry { index: 1 });
308 }
309
310 #[test]
311 fn builds_from_matrix_diagonal() {
312 let a = mat![[2.0, 9.0, 0.0], [1.0, 4.0, 5.0], [0.0, 7.0, 8.0f64],];
313
314 let pc = JacobiPrecond::try_from_matrix_diagonal(a.as_ref()).unwrap();
315 assert_eq!(pc.inverse_diagonal(), &[0.5, 0.25, 0.125]);
316 }
317
318 #[test]
319 fn apply_matches_expected_multiple_rhs() {
320 let pc = JacobiPrecond::try_from_diagonal(&[2.0, 4.0]).unwrap();
321 let rhs = mat![[2.0, 8.0], [4.0, 12.0f64],];
322 let mut out = Mat::<f64>::zeros(2, 2);
323
324 let req = pc.apply_scratch(rhs.ncols(), Par::Seq);
325 with_stack(req, |stack| {
326 pc.apply(out.as_mut(), rhs.as_ref(), Par::Seq, stack);
327 });
328
329 let expected = mat![[1.0, 4.0], [1.0, 3.0f64],];
330 assert_close(out.as_ref(), expected.as_ref(), 1e-12);
331 }
332
333 #[test]
334 fn apply_in_place_matches_apply() {
335 let pc = JacobiPrecond::try_from_diagonal(&[2.0, 4.0]).unwrap();
336 let rhs = mat![[2.0, 8.0], [4.0, 12.0f64],];
337
338 let mut out = Mat::<f64>::zeros(2, 2);
339 with_stack(pc.apply_scratch(rhs.ncols(), Par::Seq), |stack| {
340 pc.apply(out.as_mut(), rhs.as_ref(), Par::Seq, stack);
341 });
342
343 let mut inplace = rhs.to_owned();
344 with_stack(
345 pc.apply_in_place_scratch(inplace.ncols(), Par::Seq),
346 |stack| {
347 pc.apply_in_place(inplace.as_mut(), Par::Seq, stack);
348 },
349 );
350
351 assert_close(out.as_ref(), inplace.as_ref(), 1e-12);
352 }
353
354 #[test]
355 fn transpose_and_adjoint_are_usable() {
356 let pc = JacobiPrecond::try_from_diagonal(&[2.0, 4.0]).unwrap();
357 let rhs = mat![[2.0], [4.0f64],];
358
359 let mut out_t = Mat::<f64>::zeros(2, 1);
360 with_stack(pc.transpose_apply_scratch(rhs.ncols(), Par::Seq), |stack| {
361 pc.transpose_apply(out_t.as_mut(), rhs.as_ref(), Par::Seq, stack);
362 });
363
364 let mut out_h = Mat::<f64>::zeros(2, 1);
365 with_stack(pc.transpose_apply_scratch(rhs.ncols(), Par::Seq), |stack| {
366 pc.adjoint_apply(out_h.as_mut(), rhs.as_ref(), Par::Seq, stack);
367 });
368
369 let expected = mat![[1.0], [1.0f64],];
370 assert_close(out_t.as_ref(), expected.as_ref(), 1e-12);
371 assert_close(out_h.as_ref(), expected.as_ref(), 1e-12);
372 }
373
374 #[test]
375 fn transpose_and_adjoint_in_place_are_usable() {
376 let pc = JacobiPrecond::try_from_diagonal(&[2.0, 4.0]).unwrap();
377
378 let mut rhs_t = mat![[2.0], [4.0f64],];
379 with_stack(
380 pc.transpose_apply_in_place_scratch(rhs_t.ncols(), Par::Seq),
381 |stack| {
382 pc.transpose_apply_in_place(rhs_t.as_mut(), Par::Seq, stack);
383 },
384 );
385
386 let mut rhs_h = mat![[2.0], [4.0f64],];
387 with_stack(
388 pc.transpose_apply_in_place_scratch(rhs_h.ncols(), Par::Seq),
389 |stack| {
390 pc.adjoint_apply_in_place(rhs_h.as_mut(), Par::Seq, stack);
391 },
392 );
393
394 let expected = mat![[1.0], [1.0f64],];
395 assert_close(rhs_t.as_ref(), expected.as_ref(), 1e-12);
396 assert_close(rhs_h.as_ref(), expected.as_ref(), 1e-12);
397 }
398}