1use core::fmt::Debug;
16
17use dyn_stack::{MemBuffer, MemStack, StackReq};
18use faer::{
19 Conj, MatMut, MatRef, Par,
20 linalg::lu::partial_pivoting::{factor as plu_factor, solve as plu_solve},
21 matrix_free::{BiLinOp, BiPrecond, LinOp, Precond},
22 perm::PermRef,
23 prelude::ReborrowMut,
24};
25use faer_traits::ComplexField;
26use faer_traits::math_utils::{abs2, copy, zero};
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum BlockJacobiError {
31 NonSquareMatrix { nrows: usize, ncols: usize },
33 EmptyBlocks,
35 BlockOffsetsMustStartAtZero { first: usize },
37 BlockOffsetsMustEndAtDim { last: usize, dim: usize },
39 BlockOffsetsNotStrictlyIncreasing {
41 index: usize,
42 prev: usize,
43 curr: usize,
44 },
45 SingularBlock { block_index: usize },
47}
48
49impl core::fmt::Display for BlockJacobiError {
50 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
51 match self {
52 Self::NonSquareMatrix { nrows, ncols } => {
53 write!(f, "matrix must be square but is {nrows}x{ncols}")
54 }
55 Self::EmptyBlocks => f.write_str("at least one block is required"),
56 Self::BlockOffsetsMustStartAtZero { first } => {
57 write!(f, "block offsets must start at 0 but start at {first}")
58 }
59 Self::BlockOffsetsMustEndAtDim { last, dim } => {
60 write!(
61 f,
62 "block offsets must end at the matrix dimension {dim} but end at {last}"
63 )
64 }
65 Self::BlockOffsetsNotStrictlyIncreasing { index, prev, curr } => {
66 write!(
67 f,
68 "block offsets must be strictly increasing: offset[{index}]={curr} <= offset[{}]={prev}",
69 index - 1
70 )
71 }
72 Self::SingularBlock { block_index } => {
73 write!(f, "diagonal block {block_index} is singular")
74 }
75 }
76 }
77}
78
79impl core::error::Error for BlockJacobiError {}
80
81#[derive(Debug, Clone)]
95pub struct BlockJacobiPrecond<T> {
96 n: usize,
97 block_offsets: Vec<usize>,
98 factor_offsets: Vec<usize>,
99 factors: Vec<T>,
100 perm_fwd: Vec<usize>,
101 perm_inv: Vec<usize>,
102 max_block_size: usize,
103}
104
105impl<T> BlockJacobiPrecond<T> {
106 #[inline]
108 pub fn dim(&self) -> usize {
109 self.n
110 }
111
112 #[inline]
114 pub fn is_empty(&self) -> bool {
115 self.n == 0
116 }
117
118 #[inline]
120 pub fn block_count(&self) -> usize {
121 self.block_offsets.len().saturating_sub(1)
122 }
123
124 #[inline]
126 pub fn block_offsets(&self) -> &[usize] {
127 &self.block_offsets
128 }
129
130 #[inline]
132 pub fn max_block_size(&self) -> usize {
133 self.max_block_size
134 }
135}
136
137impl<T: ComplexField> BlockJacobiPrecond<T> {
138 pub fn try_new(a: MatRef<'_, T>, block_offsets: &[usize]) -> Result<Self, BlockJacobiError> {
150 if a.nrows() != a.ncols() {
151 return Err(BlockJacobiError::NonSquareMatrix {
152 nrows: a.nrows(),
153 ncols: a.ncols(),
154 });
155 }
156 let n = a.nrows();
157
158 if block_offsets.len() < 2 {
159 return Err(BlockJacobiError::EmptyBlocks);
160 }
161 if block_offsets[0] != 0 {
162 return Err(BlockJacobiError::BlockOffsetsMustStartAtZero {
163 first: block_offsets[0],
164 });
165 }
166 if *block_offsets.last().unwrap() != n {
167 return Err(BlockJacobiError::BlockOffsetsMustEndAtDim {
168 last: *block_offsets.last().unwrap(),
169 dim: n,
170 });
171 }
172 for window in block_offsets.windows(2).enumerate() {
173 let (i, w) = window;
174 if w[1] <= w[0] {
175 return Err(BlockJacobiError::BlockOffsetsNotStrictlyIncreasing {
176 index: i + 1,
177 prev: w[0],
178 curr: w[1],
179 });
180 }
181 }
182
183 let nblocks = block_offsets.len() - 1;
184
185 let mut factor_offsets = Vec::with_capacity(nblocks + 1);
187 factor_offsets.push(0);
188 let mut total_vals = 0usize;
189 let mut max_block_size = 0usize;
190 for i in 0..nblocks {
191 let size = block_offsets[i + 1] - block_offsets[i];
192 max_block_size = max_block_size.max(size);
193 total_vals += size * size;
194 factor_offsets.push(total_vals);
195 }
196
197 let mut factors: Vec<T> = (0..total_vals).map(|_| zero::<T>()).collect();
200 let mut perm_fwd: Vec<usize> = vec![0; n];
201 let mut perm_inv: Vec<usize> = vec![0; n];
202
203 let factor_scratch = plu_factor::lu_in_place_scratch::<usize, T>(
205 max_block_size,
206 max_block_size,
207 Par::Seq,
208 Default::default(),
209 );
210 let mut factor_buf = MemBuffer::new(factor_scratch);
211
212 for k in 0..nblocks {
213 let start = block_offsets[k];
214 let size = block_offsets[k + 1] - start;
215
216 let factor_range = factor_offsets[k]..factor_offsets[k + 1];
218 let block_slice = &mut factors[factor_range];
219 let mut block = MatMut::<T>::from_column_major_slice_mut(block_slice, size, size);
220 for j in 0..size {
221 for i in 0..size {
222 *block.rb_mut().get_mut(i, j) = copy(a.get(start + i, start + j));
223 }
224 }
225
226 let perm_slice_fwd = &mut perm_fwd[start..start + size];
228 let perm_slice_inv = &mut perm_inv[start..start + size];
229 let _ = plu_factor::lu_in_place::<usize, T>(
230 block.rb_mut(),
231 perm_slice_fwd,
232 perm_slice_inv,
233 Par::Seq,
234 MemStack::new(&mut factor_buf),
235 Default::default(),
236 );
237
238 let factor_view = MatRef::<T>::from_column_major_slice(
240 &factors[factor_offsets[k]..factor_offsets[k + 1]],
241 size,
242 size,
243 );
244 for i in 0..size {
245 if abs2(factor_view.get(i, i)) == zero::<T::Real>() {
246 return Err(BlockJacobiError::SingularBlock { block_index: k });
247 }
248 }
249 }
250
251 Ok(Self {
252 n,
253 block_offsets: block_offsets.to_vec(),
254 factor_offsets,
255 factors,
256 perm_fwd,
257 perm_inv,
258 max_block_size,
259 })
260 }
261
262 #[inline]
264 fn solve_scratch(&self, rhs_ncols: usize, par: Par) -> StackReq {
265 plu_solve::solve_in_place_scratch::<usize, T>(self.max_block_size, rhs_ncols, par)
266 }
267
268 #[inline]
269 fn apply_blocks(
270 &self,
271 mut rhs: MatMut<'_, T>,
272 conj: Conj,
273 transpose: bool,
274 par: Par,
275 stack: &mut MemStack,
276 ) {
277 assert_eq!(
278 rhs.nrows(),
279 self.n,
280 "rhs row count must match preconditioner dimension"
281 );
282
283 let nblocks = self.block_count();
284 for k in 0..nblocks {
285 let start = self.block_offsets[k];
286 let size = self.block_offsets[k + 1] - start;
287
288 let factor_slice = &self.factors[self.factor_offsets[k]..self.factor_offsets[k + 1]];
289 let lu = MatRef::<T>::from_column_major_slice(factor_slice, size, size);
290
291 let perm = unsafe {
292 PermRef::<'_, usize>::new_unchecked(
293 &self.perm_fwd[start..start + size],
294 &self.perm_inv[start..start + size],
295 size,
296 )
297 };
298
299 let rhs_block = rhs.rb_mut().subrows_mut(start, size);
300 if transpose {
301 plu_solve::solve_transpose_in_place_with_conj(
302 lu, lu, perm, conj, rhs_block, par, stack,
303 );
304 } else {
305 plu_solve::solve_in_place_with_conj(lu, lu, perm, conj, rhs_block, par, stack);
306 }
307 }
308 }
309}
310
311impl<T> LinOp<T> for BlockJacobiPrecond<T>
312where
313 T: ComplexField + Debug + Sync,
314{
315 fn apply_scratch(&self, rhs_ncols: usize, par: Par) -> StackReq {
316 self.solve_scratch(rhs_ncols, par)
317 }
318
319 fn nrows(&self) -> usize {
320 self.n
321 }
322
323 fn ncols(&self) -> usize {
324 self.n
325 }
326
327 fn apply(&self, mut out: MatMut<'_, T>, rhs: MatRef<'_, T>, par: Par, stack: &mut MemStack) {
328 assert_eq!(
329 out.nrows(),
330 self.n,
331 "out row count must match preconditioner dimension"
332 );
333 assert_eq!(
334 rhs.nrows(),
335 self.n,
336 "rhs row count must match preconditioner dimension"
337 );
338 assert_eq!(
339 out.ncols(),
340 rhs.ncols(),
341 "out and rhs must have the same number of columns"
342 );
343 out.copy_from(rhs);
344 self.apply_blocks(out, Conj::No, false, par, stack);
345 }
346
347 fn conj_apply(
348 &self,
349 mut out: MatMut<'_, T>,
350 rhs: MatRef<'_, T>,
351 par: Par,
352 stack: &mut MemStack,
353 ) {
354 assert_eq!(
355 out.nrows(),
356 self.n,
357 "out row count must match preconditioner dimension"
358 );
359 assert_eq!(
360 rhs.nrows(),
361 self.n,
362 "rhs row count must match preconditioner dimension"
363 );
364 assert_eq!(
365 out.ncols(),
366 rhs.ncols(),
367 "out and rhs must have the same number of columns"
368 );
369 out.copy_from(rhs);
370 self.apply_blocks(out, Conj::Yes, false, par, stack);
371 }
372}
373
374impl<T> Precond<T> for BlockJacobiPrecond<T>
375where
376 T: ComplexField + Debug + Sync,
377{
378 fn apply_in_place_scratch(&self, rhs_ncols: usize, par: Par) -> StackReq {
379 self.solve_scratch(rhs_ncols, par)
380 }
381
382 fn apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, stack: &mut MemStack) {
383 self.apply_blocks(rhs, Conj::No, false, par, stack);
384 }
385
386 fn conj_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, stack: &mut MemStack) {
387 self.apply_blocks(rhs, Conj::Yes, false, par, stack);
388 }
389}
390
391impl<T> BiLinOp<T> for BlockJacobiPrecond<T>
392where
393 T: ComplexField + Debug + Sync,
394{
395 fn transpose_apply_scratch(&self, rhs_ncols: usize, par: Par) -> StackReq {
396 self.solve_scratch(rhs_ncols, par)
397 }
398
399 fn transpose_apply(
400 &self,
401 mut out: MatMut<'_, T>,
402 rhs: MatRef<'_, T>,
403 par: Par,
404 stack: &mut MemStack,
405 ) {
406 assert_eq!(
407 out.nrows(),
408 self.n,
409 "out row count must match preconditioner dimension"
410 );
411 assert_eq!(
412 rhs.nrows(),
413 self.n,
414 "rhs row count must match preconditioner dimension"
415 );
416 assert_eq!(
417 out.ncols(),
418 rhs.ncols(),
419 "out and rhs must have the same number of columns"
420 );
421 out.copy_from(rhs);
422 self.apply_blocks(out, Conj::No, true, par, stack);
423 }
424
425 fn adjoint_apply(
426 &self,
427 mut out: MatMut<'_, T>,
428 rhs: MatRef<'_, T>,
429 par: Par,
430 stack: &mut MemStack,
431 ) {
432 assert_eq!(
433 out.nrows(),
434 self.n,
435 "out row count must match preconditioner dimension"
436 );
437 assert_eq!(
438 rhs.nrows(),
439 self.n,
440 "rhs row count must match preconditioner dimension"
441 );
442 assert_eq!(
443 out.ncols(),
444 rhs.ncols(),
445 "out and rhs must have the same number of columns"
446 );
447 out.copy_from(rhs);
448 self.apply_blocks(out, Conj::Yes, true, par, stack);
449 }
450}
451
452impl<T> BiPrecond<T> for BlockJacobiPrecond<T>
453where
454 T: ComplexField + Debug + Sync,
455{
456 fn transpose_apply_in_place_scratch(&self, rhs_ncols: usize, par: Par) -> StackReq {
457 self.solve_scratch(rhs_ncols, par)
458 }
459
460 fn transpose_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, stack: &mut MemStack) {
461 self.apply_blocks(rhs, Conj::No, true, par, stack);
462 }
463
464 fn adjoint_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, stack: &mut MemStack) {
465 self.apply_blocks(rhs, Conj::Yes, true, par, stack);
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use core::mem::MaybeUninit;
472
473 use super::*;
474 use faer::{
475 Mat, MatRef, mat,
476 matrix_free::{BiLinOp, LinOp, Precond},
477 };
478
479 fn with_stack(req: StackReq, f: impl FnOnce(&mut MemStack)) {
480 let nbytes = req.unaligned_bytes_required().max(1);
481 let mut buf = vec![MaybeUninit::<u8>::uninit(); nbytes].into_boxed_slice();
482 f(MemStack::new(&mut buf));
483 }
484
485 fn assert_close(lhs: MatRef<'_, f64>, rhs: MatRef<'_, f64>, tol: f64) {
486 assert_eq!(lhs.nrows(), rhs.nrows());
487 assert_eq!(lhs.ncols(), rhs.ncols());
488 for j in 0..lhs.ncols() {
489 for i in 0..lhs.nrows() {
490 let diff = (*lhs.get(i, j) - *rhs.get(i, j)).abs();
491 assert!(
492 diff <= tol,
493 "mismatch at ({i}, {j}): lhs={}, rhs={}, diff={diff}",
494 *lhs.get(i, j),
495 *rhs.get(i, j),
496 );
497 }
498 }
499 }
500
501 fn test_matrix() -> Mat<f64> {
504 mat![
505 [4.0, 1.0, 7.0, 9.0, 0.0],
506 [2.0, 3.0, 0.0, 0.0, 8.0],
507 [9.0, 5.0, 6.0, 1.0, 2.0],
508 [1.0, 1.0, 3.0, 5.0, 1.0],
509 [3.0, 0.0, 2.0, 1.0, 4.0],
510 ]
511 }
512
513 #[test]
514 fn builds_from_matrix() {
515 let a = test_matrix();
516 let pc = BlockJacobiPrecond::try_new(a.as_ref(), &[0, 2, 5]).unwrap();
517 assert_eq!(pc.dim(), 5);
518 assert_eq!(pc.block_count(), 2);
519 assert_eq!(pc.max_block_size(), 3);
520 assert_eq!(pc.block_offsets(), &[0, 2, 5]);
521 }
522
523 #[test]
524 fn rejects_non_square() {
525 let a = Mat::<f64>::from_fn(3, 4, |i, j| (i + j) as f64);
526 let err = BlockJacobiPrecond::try_new(a.as_ref(), &[0, 3]).unwrap_err();
527 assert_eq!(
528 err,
529 BlockJacobiError::NonSquareMatrix { nrows: 3, ncols: 4 }
530 );
531 }
532
533 #[test]
534 fn rejects_bad_offsets() {
535 let a = Mat::<f64>::identity(4, 4);
536 assert!(matches!(
537 BlockJacobiPrecond::try_new(a.as_ref(), &[]).unwrap_err(),
538 BlockJacobiError::EmptyBlocks
539 ));
540 assert!(matches!(
541 BlockJacobiPrecond::try_new(a.as_ref(), &[1, 4]).unwrap_err(),
542 BlockJacobiError::BlockOffsetsMustStartAtZero { first: 1 }
543 ));
544 assert!(matches!(
545 BlockJacobiPrecond::try_new(a.as_ref(), &[0, 3]).unwrap_err(),
546 BlockJacobiError::BlockOffsetsMustEndAtDim { last: 3, dim: 4 }
547 ));
548 assert!(matches!(
549 BlockJacobiPrecond::try_new(a.as_ref(), &[0, 2, 2, 4]).unwrap_err(),
550 BlockJacobiError::BlockOffsetsNotStrictlyIncreasing { .. }
551 ));
552 }
553
554 #[test]
555 fn rejects_singular_block() {
556 let mut a = Mat::<f64>::identity(4, 4);
557 *a.as_mut().get_mut(2, 2) = 0.0;
559 *a.as_mut().get_mut(2, 3) = 1.0;
560 *a.as_mut().get_mut(3, 2) = 0.0;
561 *a.as_mut().get_mut(3, 3) = 0.0;
562 let err = BlockJacobiPrecond::try_new(a.as_ref(), &[0, 2, 4]).unwrap_err();
563 assert_eq!(err, BlockJacobiError::SingularBlock { block_index: 1 });
564 }
565
566 #[test]
567 fn apply_inverts_block_diagonal_part() {
568 let a = mat![
570 [4.0, 1.0, 0.0, 0.0, 0.0],
571 [2.0, 3.0, 0.0, 0.0, 0.0],
572 [0.0, 0.0, 6.0, 1.0, 2.0],
573 [0.0, 0.0, 3.0, 5.0, 1.0],
574 [0.0, 0.0, 2.0, 1.0, 4.0],
575 ];
576 let pc = BlockJacobiPrecond::try_new(a.as_ref(), &[0, 2, 5]).unwrap();
577
578 let x = mat![
579 [1.0, -2.0],
580 [2.0, 1.0],
581 [3.0, 0.5],
582 [-1.0, 2.0],
583 [0.5, -1.0_f64],
584 ];
585 let b = &a * &x;
586
587 let mut out = Mat::<f64>::zeros(5, 2);
588 with_stack(pc.apply_scratch(b.ncols(), Par::Seq), |stack| {
589 pc.apply(out.as_mut(), b.as_ref(), Par::Seq, stack);
590 });
591 assert_close(out.as_ref(), x.as_ref(), 1e-12);
592 }
593
594 #[test]
595 fn apply_in_place_matches_apply() {
596 let a = test_matrix();
597 let pc = BlockJacobiPrecond::try_new(a.as_ref(), &[0, 2, 5]).unwrap();
598
599 let rhs = mat![
600 [1.0, 4.0],
601 [2.0, 5.0],
602 [3.0, 6.0],
603 [4.0, 7.0],
604 [5.0, 8.0_f64],
605 ];
606
607 let mut out = Mat::<f64>::zeros(5, 2);
608 with_stack(pc.apply_scratch(rhs.ncols(), Par::Seq), |stack| {
609 pc.apply(out.as_mut(), rhs.as_ref(), Par::Seq, stack);
610 });
611
612 let mut inplace = rhs.to_owned();
613 with_stack(pc.apply_in_place_scratch(rhs.ncols(), Par::Seq), |stack| {
614 pc.apply_in_place(inplace.as_mut(), Par::Seq, stack);
615 });
616
617 assert_close(out.as_ref(), inplace.as_ref(), 1e-12);
618 }
619
620 #[test]
621 fn transpose_matches_block_transpose_solve() {
622 let a = mat![
624 [4.0, 1.0, 0.0, 0.0, 0.0],
625 [2.0, 3.0, 0.0, 0.0, 0.0],
626 [0.0, 0.0, 6.0, 1.0, 2.0],
627 [0.0, 0.0, 3.0, 5.0, 1.0],
628 [0.0, 0.0, 2.0, 1.0, 4.0_f64],
629 ];
630 let pc = BlockJacobiPrecond::try_new(a.as_ref(), &[0, 2, 5]).unwrap();
631
632 let x = mat![[1.0], [2.0], [3.0], [-1.0], [0.5_f64],];
633 let b = a.transpose() * &x;
634
635 let mut out = Mat::<f64>::zeros(5, 1);
636 with_stack(pc.transpose_apply_scratch(b.ncols(), Par::Seq), |stack| {
637 pc.transpose_apply(out.as_mut(), b.as_ref(), Par::Seq, stack);
638 });
639 assert_close(out.as_ref(), x.as_ref(), 1e-12);
640 }
641
642 #[test]
643 fn adjoint_equals_transpose_for_real() {
644 let a = test_matrix();
645 let pc = BlockJacobiPrecond::try_new(a.as_ref(), &[0, 2, 5]).unwrap();
646
647 let rhs = mat![[1.0], [-2.0], [3.0], [4.0], [-1.0_f64],];
648
649 let mut out_t = Mat::<f64>::zeros(5, 1);
650 with_stack(pc.transpose_apply_scratch(rhs.ncols(), Par::Seq), |stack| {
651 pc.transpose_apply(out_t.as_mut(), rhs.as_ref(), Par::Seq, stack);
652 });
653 let mut out_h = Mat::<f64>::zeros(5, 1);
654 with_stack(pc.transpose_apply_scratch(rhs.ncols(), Par::Seq), |stack| {
655 pc.adjoint_apply(out_h.as_mut(), rhs.as_ref(), Par::Seq, stack);
656 });
657 assert_close(out_t.as_ref(), out_h.as_ref(), 1e-12);
658 }
659
660 #[test]
661 fn single_block_matches_full_lu() {
662 let a = mat![[4.0, 1.0, 2.0], [3.0, 5.0, 1.0], [1.0, 2.0, 6.0_f64],];
664 let pc = BlockJacobiPrecond::try_new(a.as_ref(), &[0, 3]).unwrap();
665
666 let x = mat![[1.0], [2.0], [3.0_f64],];
667 let b = &a * &x;
668
669 let mut out = Mat::<f64>::zeros(3, 1);
670 with_stack(pc.apply_scratch(b.ncols(), Par::Seq), |stack| {
671 pc.apply(out.as_mut(), b.as_ref(), Par::Seq, stack);
672 });
673 assert_close(out.as_ref(), x.as_ref(), 1e-12);
674 }
675}