1#![cfg(feature = "backend-faer")]
2
3#[cfg(feature = "complex")]
44use crate::algebra::bridge::{BridgeScratch, copy_scalar_to_real_in};
45use crate::algebra::scalar::KrystScalar;
46#[cfg(feature = "complex")]
47use crate::algebra::scalar::S as GlobalScalar;
48use crate::error::KError;
49use crate::matrix::sparse::CsrMatrix;
50#[cfg(feature = "complex")]
51use crate::ops::kpc::KPreconditioner;
52use crate::preconditioner::LocalPreconditioner;
53use crate::preconditioner::stats::{ParIluHistory, ParIluIterSample};
54use crate::preconditioner::{PcSide, legacy::Preconditioner, pivot::*, tri_solve::TriangularSolve};
55use crate::utils::conditioning::{ConditioningOptions, apply_dense_transforms};
56use crate::utils::metrics::{Counters, SolveTimer};
57use crate::utils::monitor::{Event, Monitor};
58use crate::utils::permutation::{Permutation, amd_from_adj, permutation_from_order, rcm_from_adj};
59use faer::Mat;
60use std::collections::{BTreeMap, HashMap};
61use std::sync::Mutex;
62
63#[cfg(feature = "rayon")]
64use rayon::prelude::*;
65
66#[cfg(feature = "logging")]
67use log::{debug, info, trace, warn};
68
69type S = f64;
71type Real = f64;
72
73pub const COMPLEX_SUPPORT: &str = "native_complex";
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub enum IluType {
79 ILU0 = 0,
81 ILUK = 1,
83 ILUT = 2,
85 MILU0 = 3,
87 BlockJacobi = 10,
89 GmresIluk = 20,
91 GmresIlut = 21,
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub enum ReorderingType {
98 None = 0,
100 RCM = 1,
102 AMD = 2,
104 Natural = 3,
106}
107
108#[derive(Clone, Copy, Debug, PartialEq, Eq)]
110pub enum TriSolveType {
111 Exact = 0,
113 Jacobi = 1,
115 GaussSeidel = 2,
117}
118
119#[derive(Clone, Debug)]
141pub struct IluConfig {
142 pub ilu_type: IluType,
144 pub level_of_fill: usize,
146 pub max_fill_per_row: usize,
148 pub drop_tolerance: Real,
150 pub offdiag_drop_tolerance: Real,
152 pub schur_drop_tolerance: Real,
154 pub reordering_type: ReorderingType,
156 pub triangular_solve: TriSolveType,
158 pub lower_jacobi_iters: usize,
160 pub upper_jacobi_iters: usize,
162 pub tolerance: Real,
164 pub max_iterations: usize,
166 pub logging_level: usize,
168 pub print_level: usize,
170 pub ieee_checks: bool,
172 pub optimize_workspace: bool,
174 pub pivot_policy: PivotPolicy,
176 pub enable_parallel_factorization: bool,
181 pub enable_parallel_triangular_solve: bool,
187 pub parallel_chunk_size: usize,
192 pub enable_distributed: bool,
194 pub parilu_enabled: bool,
196 pub parilu_max_iters: usize,
198 pub parilu_min_iters: usize,
200 pub parilu_tol: Real,
202 pub parilu_omega: Real,
204 pub conditioning: ConditioningOptions,
206}
207
208#[derive(Clone, Copy, Debug, PartialEq, Eq)]
209enum ParFactorizationMode {
210 Serial,
211 Block,
212 ParIlu,
213}
214
215impl IluConfig {
216 fn par_factor_mode(&self) -> ParFactorizationMode {
217 if !self.enable_parallel_factorization {
218 ParFactorizationMode::Serial
219 } else if self.parilu_enabled {
220 ParFactorizationMode::ParIlu
221 } else {
222 ParFactorizationMode::Block
223 }
224 }
225}
226
227impl Default for IluConfig {
228 fn default() -> Self {
230 Self {
231 ilu_type: IluType::ILU0,
232 level_of_fill: 0, max_fill_per_row: 0, drop_tolerance: 1e-4, offdiag_drop_tolerance: 1e-4, schur_drop_tolerance: 1e-4, reordering_type: ReorderingType::None, triangular_solve: TriSolveType::Exact, lower_jacobi_iters: 1, upper_jacobi_iters: 1, tolerance: 1e-6, max_iterations: 1, logging_level: 0, print_level: 0, ieee_checks: true, optimize_workspace: true, pivot_policy: PivotPolicy::default(),
248 enable_parallel_factorization: false, enable_parallel_triangular_solve: false, parallel_chunk_size: 64, enable_distributed: false, parilu_enabled: false,
253 parilu_max_iters: 0,
254 parilu_min_iters: 0,
255 parilu_tol: 1e-2,
256 parilu_omega: 1.0,
257 conditioning: ConditioningOptions::default(),
258 }
259 }
260}
261
262#[cfg(feature = "logging")]
263fn print_ilu_banner(cfg: &IluConfig) {
264 if cfg.logging_level == 0 {
265 return;
266 }
267 info!("ILU Setup:");
268 info!(" kind : {:?}", cfg.ilu_type);
269 info!(" reordering : {:?}", cfg.reordering_type);
270 let tri = match cfg.triangular_solve {
271 TriSolveType::Exact => "Exact".to_string(),
272 TriSolveType::Jacobi => format!(
273 "Jacobi (L:{} U:{})",
274 cfg.lower_jacobi_iters, cfg.upper_jacobi_iters
275 ),
276 TriSolveType::GaussSeidel => "GaussSeidel".to_string(),
277 };
278 info!(" triangular solve : {tri}");
279 info!(
280 " iterative setup : tol={:.2e}, max_iter={}",
281 cfg.tolerance, cfg.max_iterations
282 );
283 info!(
284 " exec : distributed={}, par_factorization={}, par_trisolve={}",
285 cfg.enable_distributed,
286 cfg.enable_parallel_factorization,
287 cfg.enable_parallel_triangular_solve
288 );
289 info!(" pivot : {:?}", cfg.pivot_policy);
290 info!(
291 " parilu : enabled={}, max_iter={}, min_iter={}, tol={:.2e}, omega={:.2}",
292 cfg.parilu_enabled,
293 cfg.parilu_max_iters,
294 cfg.parilu_min_iters,
295 cfg.parilu_tol,
296 cfg.parilu_omega
297 );
298}
299
300pub struct IluBuilder {
302 config: IluConfig,
303}
304
305impl IluBuilder {
306 pub fn new() -> Self {
308 Self {
309 config: IluConfig::default(),
310 }
311 }
312
313 pub fn ilu_type(mut self, ilu_type: IluType) -> Self {
315 self.config.ilu_type = ilu_type;
316 self
317 }
318
319 pub fn level_of_fill(mut self, level: usize) -> Self {
321 self.config.level_of_fill = level;
322 self
323 }
324
325 pub fn max_fill_per_row(mut self, max_fill: usize) -> Self {
327 self.config.max_fill_per_row = max_fill;
328 self
329 }
330
331 pub fn drop_tolerance(mut self, tol: Real) -> Self {
333 self.config.drop_tolerance = tol;
334 self
335 }
336
337 pub fn enable_reordering(mut self, reordering: ReorderingType) -> Self {
339 self.config.reordering_type = reordering;
340 self
341 }
342
343 pub fn triangular_solve(mut self, solve_type: TriSolveType) -> Self {
345 self.config.triangular_solve = solve_type;
346 self
347 }
348
349 pub fn jacobi_iterations(mut self, lower: usize, upper: usize) -> Self {
351 self.config.lower_jacobi_iters = lower;
352 self.config.upper_jacobi_iters = upper;
353 self
354 }
355
356 pub fn enable_logging(mut self) -> Self {
358 self.config.logging_level = 1;
359 self
360 }
361
362 pub fn logging_level(mut self, level: usize) -> Self {
364 self.config.logging_level = level;
365 self
366 }
367
368 pub fn enable_printing(mut self) -> Self {
370 self.config.print_level = 1;
371 self
372 }
373
374 pub fn print_level(mut self, level: usize) -> Self {
376 self.config.print_level = level;
377 self
378 }
379
380 pub fn pivot_policy(mut self, policy: PivotPolicy) -> Self {
382 self.config.pivot_policy = policy;
383 self
384 }
385
386 pub fn pivot_floor(mut self, tau: Real) -> Self {
391 self.config.pivot_policy.tau = tau.max(0.0);
392 self
393 }
394
395 pub fn enable_parallel_factorization(mut self) -> Self {
397 self.config.enable_parallel_factorization = true;
398 self
399 }
400
401 pub fn enable_parallel_triangular_solve(mut self) -> Self {
403 self.config.enable_parallel_triangular_solve = true;
404 self
405 }
406
407 pub fn parallel_chunk_size(mut self, chunk_size: usize) -> Self {
409 self.config.parallel_chunk_size = chunk_size;
410 self
411 }
412
413 pub fn enable_parallel(mut self) -> Self {
415 self.config.enable_parallel_factorization = true;
416 self.config.enable_parallel_triangular_solve = true;
417 self
418 }
419
420 pub fn enable_parilu(mut self, max_iters: usize, tol: Real, omega: Real) -> Self {
422 self.config.parilu_enabled = true;
423 self.config.parilu_max_iters = max_iters;
424 self.config.parilu_tol = tol;
425 self.config.parilu_omega = omega;
426 self
427 }
428
429 pub fn parilu_min_iters(mut self, min_iters: usize) -> Self {
431 self.config.parilu_min_iters = min_iters;
432 self
433 }
434
435 pub fn enable_distributed(mut self) -> Self {
437 self.config.enable_distributed = true;
438 self
439 }
440
441 pub fn build(self) -> Result<Ilu, KError> {
443 Ilu::new_with_config(self.config)
444 }
445}
446
447impl Default for IluBuilder {
448 fn default() -> Self {
449 Self::new()
450 }
451}
452
453pub struct Ilu {
462 config: IluConfig,
464 l: CsrMatrix<f64>,
466 u: CsrMatrix<f64>,
468 inv_diag_u: Vec<f64>,
470 #[allow(dead_code)]
472 row_perm: Vec<usize>,
473 row_perm_inv: Vec<usize>,
474 #[allow(dead_code)]
475 col_perm: Vec<usize>,
476 col_perm_inv: Vec<usize>,
477 workspace: IluWorkspace,
479 #[cfg(feature = "rayon")]
480 levels_l: Levels,
482 #[cfg(feature = "rayon")]
483 levels_u: Levels,
485 setup_complexity: f64,
487 nnz_l: usize,
489 nnz_u: usize,
490 num_zero_pivots: usize,
491 pivot_stats: PivotStats,
493 max_diag_a: f64,
495 row_inf_a: Vec<f64>,
497 row_gersh_a: Vec<f64>,
499 running_max_u: f64,
501 setup_time: f64,
503 solve_ctrs: Counters,
504 history: Option<ParIluHistory>,
506 monitor: Option<Box<dyn Monitor>>,
508 complex_setup_used_native: bool,
510 complex_setup_fallback_reason: Option<String>,
512}
513
514#[derive(Debug)]
516pub struct IluWorkspace {
517 solve_buf: Mutex<Vec<f64>>,
519 temp2: Mutex<Vec<f64>>,
521 levels: Mutex<Vec<usize>>,
523 pattern_work: Mutex<Vec<bool>>,
525 size: usize,
527}
528
529impl IluWorkspace {
530 pub fn new(size: usize) -> Self {
532 Self {
533 solve_buf: Mutex::new(vec![0.0; size]),
534 temp2: Mutex::new(vec![0.0; size]),
535 levels: Mutex::new(vec![0; size]),
536 pattern_work: Mutex::new(vec![false; size]),
537 size,
538 }
539 }
540
541 pub fn ensure_size(&mut self, new_size: usize) {
543 if new_size > self.size {
544 self.solve_buf.lock().unwrap().resize(new_size, 0.0);
545 self.temp2.lock().unwrap().resize(new_size, 0.0);
546 self.levels.lock().unwrap().resize(new_size, 0);
547 self.pattern_work.lock().unwrap().resize(new_size, false);
548 self.size = new_size;
549 }
550 }
551
552 pub fn clear(&self) {
554 for x in self.solve_buf.lock().unwrap().iter_mut() {
555 *x = 0.0;
556 }
557 for x in self.temp2.lock().unwrap().iter_mut() {
558 *x = 0.0;
559 }
560 for x in self.levels.lock().unwrap().iter_mut() {
561 *x = 0;
562 }
563 for x in self.pattern_work.lock().unwrap().iter_mut() {
564 *x = false;
565 }
566 }
567
568 #[inline]
570 pub fn borrow_solve_buf(&self, n: usize) -> std::sync::MutexGuard<'_, Vec<f64>> {
571 debug_assert!(
572 self.size >= n,
573 "workspace not sized; call ensure_size in setup()"
574 );
575 self.solve_buf.lock().unwrap()
576 }
577}
578
579#[cfg(feature = "rayon")]
580struct BlockFactorResult {
581 l: CsrMatrix<f64>,
582 u: CsrMatrix<f64>,
583 pivot_stats: PivotStats,
584 running_max: f64,
585 zero_pivots: usize,
586}
587
588#[cfg(feature = "rayon")]
589#[derive(Clone, Debug, Default)]
590struct Levels {
591 buckets: Vec<Vec<usize>>,
593 max_level: u32,
595}
596
597#[cfg(feature = "rayon")]
598fn build_levels_lower(l: &CsrMatrix<f64>) -> Levels {
599 let n = l.nrows();
600 let mut lev = vec![0u32; n];
601 let mut maxl = 0u32;
602 for i in 0..n {
603 let (cols, _vals) = l.row(i);
604 let mut li = 0u32;
605 for &j in cols {
606 if j >= i {
607 continue;
608 }
609 li = li.max(lev[j] + 1);
610 }
611 lev[i] = li;
612 maxl = maxl.max(li);
613 }
614 let mut buckets = vec![Vec::new(); (maxl as usize) + 1];
615 for (i, &l) in lev.iter().enumerate() {
616 buckets[l as usize].push(i);
617 }
618 Levels {
619 buckets,
620 max_level: maxl,
621 }
622}
623
624#[cfg(feature = "rayon")]
625fn build_levels_upper(u: &CsrMatrix<f64>) -> Levels {
626 let n = u.nrows();
627 let mut lev = vec![0u32; n];
628 let mut maxl = 0u32;
629 for i in (0..n).rev() {
630 let (cols, _vals) = u.row(i);
631 let mut li = 0u32;
632 for &j in cols {
633 if j <= i {
634 continue;
635 }
636 li = li.max(lev[j] + 1);
637 }
638 lev[i] = li;
639 maxl = maxl.max(li);
640 }
641 let mut buckets = vec![Vec::new(); (maxl as usize) + 1];
642 for (i, &l) in lev.iter().enumerate() {
643 buckets[l as usize].push(i);
644 }
645 Levels {
646 buckets,
647 max_level: maxl,
648 }
649}
650
651impl Ilu {
652 pub fn new() -> Self {
654 Self::new_with_config(IluConfig::default()).unwrap()
655 }
656
657 pub fn new_with_config(config: IluConfig) -> Result<Self, KError> {
659 Self::validate_config(&config)?;
660
661 #[cfg(feature = "logging")]
662 if config.logging_level > 0 {
663 info!(
664 "ILU Setup: Creating {:?} factorization with HYPRE-inspired configuration",
665 config.ilu_type
666 );
667 debug!(
668 "ILU Config: fill_level={}, drop_tol={:.2e}, reordering={:?}",
669 config.level_of_fill, config.drop_tolerance, config.reordering_type
670 );
671 }
672
673 Ok(Self {
674 config,
675 l: CsrMatrix::from_csr(0, 0, vec![0], Vec::new(), Vec::new()),
676 u: CsrMatrix::from_csr(0, 0, vec![0], Vec::new(), Vec::new()),
677 inv_diag_u: Vec::new(),
678 row_perm: Vec::new(),
679 row_perm_inv: Vec::new(),
680 col_perm: Vec::new(),
681 col_perm_inv: Vec::new(),
682 workspace: IluWorkspace::new(0),
683 #[cfg(feature = "rayon")]
684 levels_l: Levels::default(),
685 #[cfg(feature = "rayon")]
686 levels_u: Levels::default(),
687 setup_complexity: 0.0,
688 nnz_l: 0,
689 nnz_u: 0,
690 num_zero_pivots: 0,
691 pivot_stats: PivotStats::default(),
692 max_diag_a: Real::default(),
693 row_inf_a: Vec::new(),
694 row_gersh_a: Vec::new(),
695 running_max_u: Real::default(),
696 setup_time: 0.0,
697 solve_ctrs: Counters::new(),
698 history: None,
699 monitor: None,
700 complex_setup_used_native: true,
701 complex_setup_fallback_reason: None,
702 })
703 }
704
705 fn validate_config(config: &IluConfig) -> Result<(), KError> {
707 if config.drop_tolerance < 0.0 {
708 return Err(KError::InvalidInput(
709 "drop_tolerance must be >= 0".to_string(),
710 ));
711 }
712
713 if config.enable_parallel_triangular_solve && config.parallel_chunk_size == 0 {
714 return Err(KError::InvalidInput(
715 "parallel_chunk_size must be > 0 when parallel triangular solve is enabled"
716 .to_string(),
717 ));
718 }
719
720 if config.tolerance <= 0.0 {
721 return Err(KError::InvalidInput("tolerance must be > 0".to_string()));
722 }
723
724 if config.parilu_omega <= 0.0 {
725 return Err(KError::InvalidInput(
726 "parilu_omega must be > 0 (relaxation factor)".to_string(),
727 ));
728 }
729
730 if config.parilu_min_iters > config.parilu_max_iters {
731 return Err(KError::InvalidInput(
732 "parilu_min_iters cannot exceed parilu_max_iters".to_string(),
733 ));
734 }
735
736 Ok(())
737 }
738
739 fn allow_parallel_factorization(&self, n: usize) -> bool {
740 if !self.config.enable_parallel_factorization {
741 return false;
742 }
743 #[cfg(feature = "rayon")]
744 {
745 if crate::algebra::parallel_cfg::force_serial() {
746 return false;
747 }
748 if crate::parallel::threads::current_rayon_threads() <= 1 {
749 return false;
750 }
751 let chunk_size = self.config.parallel_chunk_size.max(1);
752 if chunk_size == 1 {
753 return true;
754 }
755 let tune = crate::algebra::parallel_cfg::parallel_tune();
756 return n >= tune.min_rows_ilu_factorization;
757 }
758 #[cfg(not(feature = "rayon"))]
759 {
760 true
761 }
762 }
763
764 fn allow_parallel_triangular_solve(&self, n: usize) -> bool {
765 if !self.config.enable_parallel_triangular_solve {
766 return false;
767 }
768 #[cfg(feature = "rayon")]
769 {
770 if crate::algebra::parallel_cfg::force_serial() {
771 return false;
772 }
773 if crate::parallel::threads::current_rayon_threads() <= 1 {
774 return false;
775 }
776 let tune = crate::algebra::parallel_cfg::parallel_tune();
777 return n >= tune.min_rows_ilu_triangular;
778 }
779 #[cfg(not(feature = "rayon"))]
780 {
781 false
782 }
783 }
784
785 fn check_ieee_values(matrix: &Mat<f64>) -> Result<(), KError> {
787 for i in 0..matrix.nrows() {
788 for j in 0..matrix.ncols() {
789 let val = matrix[(i, j)];
790 if val.is_nan() {
791 return Err(KError::InvalidInput(format!(
792 "NaN detected in matrix at position ({i}, {j})"
793 )));
794 }
795 if val.is_infinite() {
796 return Err(KError::InvalidInput(format!(
797 "Infinity detected in matrix at position ({i}, {j})"
798 )));
799 }
800 }
801 }
802 Ok(())
803 }
804
805 fn validate_matrix(matrix: &Mat<f64>) -> Result<(), KError> {
807 if matrix.nrows() == 0 || matrix.ncols() == 0 {
808 return Err(KError::InvalidInput("Matrix cannot be empty".to_string()));
809 }
810
811 if matrix.nrows() != matrix.ncols() {
812 return Err(KError::InvalidInput(
813 "ILU requires square matrices".to_string(),
814 ));
815 }
816
817 Ok(())
818 }
819
820 fn calculate_complexity(&self, original_nnz: usize) -> f64 {
822 let total_nnz = self.nnz_l + self.nnz_u;
823 if original_nnz > 0 {
824 total_nnz as f64 / original_nnz as f64
825 } else {
826 0.0
827 }
828 }
829
830 fn stabilize_pivot_value(
831 pivot: &mut f64,
832 row: usize,
833 matrix: &Mat<f64>,
834 policy: &PivotPolicy,
835 max_diag_a: f64,
836 row_inf_a: &[f64],
837 row_gersh_a: &[f64],
838 stats: &mut PivotStats,
839 running_max: &mut f64,
840 zero_pivots: &mut usize,
841 ) -> Result<(), KError> {
842 let s_i = match policy.scale {
843 PivotScale::MaxDiagA => max_diag_a,
844 PivotScale::LocalDiagA => matrix[(row, row)].abs(),
845 PivotScale::RowInfA => row_inf_a[row],
846 PivotScale::RowGershgorin => row_gersh_a[row],
847 PivotScale::RunningMaxU => *running_max,
848 }
849 .max(max_diag_a);
850
851 if let Err(e) =
852 stabilize_pivot_in_place(pivot, s_i, policy.tau, policy.sign, policy.mode, stats, row)
853 {
854 *zero_pivots += 1;
855 return Err(e);
856 }
857
858 let abs = pivot.abs();
859 if abs > *running_max {
860 *running_max = abs;
861 }
862 Ok(())
863 }
864
865 fn handle_pivot(
867 &mut self,
868 pivot: &mut f64,
869 row: usize,
870 matrix: &Mat<f64>,
871 ) -> Result<(), KError> {
872 let policy = &self.config.pivot_policy;
873
874 Self::stabilize_pivot_value(
877 pivot,
878 row,
879 matrix,
880 policy,
881 self.max_diag_a,
882 &self.row_inf_a,
883 &self.row_gersh_a,
884 &mut self.pivot_stats,
885 &mut self.running_max_u,
886 &mut self.num_zero_pivots,
887 )?;
888
889 Ok(())
890 }
891
892 fn partition_rows(n: usize, block_size: usize) -> Vec<std::ops::Range<usize>> {
893 let block_size = block_size.max(1);
894 let mut blocks = Vec::new();
895 let mut start = 0;
896 while start < n {
897 let end = (start + block_size).min(n);
898 blocks.push(start..end);
899 start = end;
900 }
901 blocks
902 }
903
904 fn extract_block_dense(matrix: &Mat<f64>, rows: std::ops::Range<usize>) -> Mat<f64> {
905 let m = rows.len();
906 let mut block = Mat::zeros(m, m);
907 for local_i in 0..m {
908 let global_i = rows.start + local_i;
909 for local_j in 0..m {
910 let global_j = rows.start + local_j;
911 block[(local_i, local_j)] = matrix[(global_i, global_j)];
912 }
913 }
914 block
915 }
916
917 #[cfg(feature = "rayon")]
918 fn factor_block(
919 matrix: &Mat<f64>,
920 rows: std::ops::Range<usize>,
921 policy: &PivotPolicy,
922 max_diag: f64,
923 row_inf: &[f64],
924 row_gersh: &[f64],
925 ) -> Result<BlockFactorResult, KError> {
926 let block = Self::extract_block_dense(matrix, rows.clone());
927 let drop_tol: f64 = 1e-15;
928 let mut l = CsrMatrix::from_dense(&block, drop_tol)?;
929 let mut u = CsrMatrix::from_dense(&block, drop_tol)?;
930 let size = block.nrows();
931
932 for i in 0..size {
933 for j in 0..size {
934 if i > j {
935 Self::sparse_set(&mut u, i, j, 0.0);
936 } else if i < j {
937 Self::sparse_set(&mut l, i, j, 0.0);
938 } else {
939 Self::sparse_set(&mut l, i, i, 1.0);
940 }
941 }
942 }
943
944 let mut stats = PivotStats::default();
945 let mut running_max: f64 = 0.0;
946 let mut zero_pivots = 0usize;
947
948 for k in 0..size {
949 let mut pivot = Self::sparse_get(&u, k, k);
950 Self::stabilize_pivot_value(
951 &mut pivot,
952 rows.start + k,
953 matrix,
954 policy,
955 max_diag,
956 row_inf,
957 row_gersh,
958 &mut stats,
959 &mut running_max,
960 &mut zero_pivots,
961 )?;
962 Self::sparse_set(&mut u, k, k, pivot);
963
964 for i in (k + 1)..size {
965 let l_ik = Self::sparse_get(&l, i, k);
966 if l_ik != 0.0 {
967 let multiplier = l_ik / pivot;
968 Self::sparse_set(&mut l, i, k, multiplier);
969 for j in (k + 1)..size {
970 let u_kj = Self::sparse_get(&u, k, j);
971 if u_kj != 0.0 {
972 let global_i = rows.start + i;
973 let global_j = rows.start + j;
974 if matrix[(global_i, global_j)] != 0.0 {
975 let u_ij = Self::sparse_get(&u, i, j);
976 let new_val = u_ij - multiplier * u_kj;
977 Self::sparse_set(&mut u, i, j, new_val);
978 }
979 }
980 }
981 }
982 }
983 }
984
985 Ok(BlockFactorResult {
986 l,
987 u,
988 pivot_stats: stats,
989 running_max,
990 zero_pivots,
991 })
992 }
993
994 fn sparse_get(matrix: &CsrMatrix<f64>, i: usize, j: usize) -> f64 {
996 let (cols, vals) = matrix.row(i);
997 match cols.binary_search(&j) {
998 Ok(pos) => vals[pos],
999 Err(_) => 0.0,
1000 }
1001 }
1002
1003 fn sparse_set(matrix: &mut CsrMatrix<f64>, i: usize, j: usize, value: f64) {
1008 let start = matrix.row_ptr()[i];
1009 let end = matrix.row_ptr()[i + 1];
1010 let mut pos_in_row = None;
1013 {
1014 let cols = &matrix.col_idx()[start..end];
1015 if let Ok(off) = cols.binary_search(&j) {
1016 pos_in_row = Some(start + off);
1017 }
1018 }
1019 if let Some(p) = pos_in_row {
1020 let values = matrix.values_mut();
1021 values[p] = value;
1022 }
1023 }
1024
1025 fn compute_ilu0(&mut self, matrix: &Mat<f64>) -> Result<(), KError> {
1027 let n = matrix.nrows();
1028
1029 let drop_tol: f64 = 1e-15;
1031 let mut l = CsrMatrix::from_dense(matrix, drop_tol)?;
1032 let mut u = CsrMatrix::from_dense(matrix, drop_tol)?;
1033
1034 for i in 0..n {
1036 for j in 0..n {
1037 if i > j {
1038 Self::sparse_set(&mut u, i, j, 0.0);
1040 } else if i < j {
1041 Self::sparse_set(&mut l, i, j, 0.0);
1043 } else {
1044 Self::sparse_set(&mut l, i, i, 1.0);
1046 }
1047 }
1048 }
1049
1050 for k in 0..n {
1052 let mut pivot = Self::sparse_get(&u, k, k);
1054 self.handle_pivot(&mut pivot, k, matrix)?;
1055 Self::sparse_set(&mut u, k, k, pivot);
1056
1057 for i in (k + 1)..n {
1058 let l_ik = Self::sparse_get(&l, i, k);
1059 if l_ik != 0.0 {
1060 let multiplier = l_ik / pivot;
1061 Self::sparse_set(&mut l, i, k, multiplier);
1062
1063 for j in (k + 1)..n {
1064 let u_kj = Self::sparse_get(&u, k, j);
1065 if u_kj != 0.0 && matrix[(i, j)] != 0.0 {
1066 let u_ij = Self::sparse_get(&u, i, j);
1067 let new_val = u_ij - multiplier * u_kj;
1068 Self::sparse_set(&mut u, i, j, new_val);
1069 }
1070 }
1071 }
1072 }
1073 }
1074
1075 self.nnz_l = l.nnz();
1077 self.nnz_u = u.nnz();
1078
1079 self.inv_diag_u = u.diagonal().into_iter().map(|v| 1.0 / v).collect();
1081
1082 self.l = l;
1083 self.u = u;
1084
1085 Ok(())
1086 }
1087
1088 #[cfg(feature = "rayon")]
1089 fn compute_ilu0_block_parallel(&mut self, matrix: &Mat<f64>) -> Result<(), KError> {
1090 let n = matrix.nrows();
1091 let chunk_size = self.config.parallel_chunk_size.max(1);
1092 let blocks = Self::partition_rows(n, chunk_size);
1093
1094 let row_inf = &self.row_inf_a;
1095 let row_gersh = &self.row_gersh_a;
1096 let pivot_policy = &self.config.pivot_policy;
1097 let max_diag = self.max_diag_a;
1098
1099 let mut block_results: Vec<_> = blocks
1100 .into_par_iter()
1101 .map(|rows| {
1102 Self::factor_block(
1103 matrix,
1104 rows.clone(),
1105 pivot_policy,
1106 max_diag,
1107 row_inf,
1108 row_gersh,
1109 )
1110 .map(|res| (rows, res))
1111 })
1112 .collect::<Result<Vec<_>, KError>>()?;
1113
1114 block_results.sort_by_key(|(range, _)| range.start);
1115
1116 let mut merged_stats = PivotStats::default();
1117 let mut running_max: f64 = 0.0;
1118 let mut zero_pivots = 0usize;
1119 for (_, result) in block_results.iter() {
1120 merged_stats.num_floors += result.pivot_stats.num_floors;
1121 merged_stats.sum_abs_shift += result.pivot_stats.sum_abs_shift;
1122 merged_stats.num_strict_fail += result.pivot_stats.num_strict_fail;
1123 merged_stats.max_abs_shift = merged_stats
1124 .max_abs_shift
1125 .max(result.pivot_stats.max_abs_shift);
1126 merged_stats.last_floor_value = merged_stats
1127 .last_floor_value
1128 .max(result.pivot_stats.last_floor_value);
1129 running_max = running_max.max(result.running_max);
1130 zero_pivots += result.zero_pivots;
1131 }
1132
1133 self.pivot_stats = merged_stats;
1134 self.running_max_u = running_max;
1135 self.num_zero_pivots = zero_pivots;
1136
1137 self.assemble_block_diagonal(n, block_results)?;
1138 Ok(())
1139 }
1140
1141 #[cfg(not(feature = "rayon"))]
1142 fn compute_ilu0_block_parallel(&mut self, matrix: &Mat<f64>) -> Result<(), KError> {
1143 #[cfg(feature = "logging")]
1144 if self.config.logging_level > 0 && self.config.enable_parallel_factorization {
1145 warn!(
1146 "ILU parallel factorization requested but 'rayon' feature disabled; falling back to serial ILU0"
1147 );
1148 }
1149 self.compute_ilu0(matrix)
1150 }
1151
1152 #[cfg(feature = "rayon")]
1153 fn assemble_block_diagonal(
1154 &mut self,
1155 n: usize,
1156 block_results: Vec<(std::ops::Range<usize>, BlockFactorResult)>,
1157 ) -> Result<(), KError> {
1158 let mut l_row_ptr = Vec::with_capacity(n + 1);
1159 let mut u_row_ptr = Vec::with_capacity(n + 1);
1160 l_row_ptr.push(0);
1161 u_row_ptr.push(0);
1162 let mut l_cols = Vec::new();
1163 let mut l_vals = Vec::new();
1164 let mut u_cols = Vec::new();
1165 let mut u_vals = Vec::new();
1166 let mut inv_diag_u = vec![0.0; n];
1167 let mut nnz_l = 0;
1168 let mut nnz_u = 0;
1169
1170 for (rows, block) in block_results {
1171 let size = rows.len();
1172 for local_i in 0..size {
1173 let global_i = rows.start + local_i;
1174 let (cols_l, vals_l) = block.l.row(local_i);
1175 for (&c, &v) in cols_l.iter().zip(vals_l.iter()) {
1176 l_cols.push(rows.start + c);
1177 l_vals.push(v);
1178 nnz_l += 1;
1179 }
1180 l_row_ptr.push(nnz_l);
1181
1182 let (cols_u, vals_u) = block.u.row(local_i);
1183 let mut diag_found = false;
1184 for (&c, &v) in cols_u.iter().zip(vals_u.iter()) {
1185 u_cols.push(rows.start + c);
1186 u_vals.push(v);
1187 if c == local_i {
1188 if v == 0.0 {
1189 return Err(KError::InvalidInput(format!(
1190 "zero diagonal detected in block row {global_i}"
1191 )));
1192 }
1193 inv_diag_u[global_i] = 1.0 / v;
1194 diag_found = true;
1195 }
1196 nnz_u += 1;
1197 }
1198 if !diag_found {
1199 return Err(KError::InvalidInput(format!(
1200 "block ILU lost diagonal entry at row {global_i}"
1201 )));
1202 }
1203 u_row_ptr.push(nnz_u);
1204 }
1205 }
1206
1207 self.l = CsrMatrix::from_csr(n, n, l_row_ptr, l_cols, l_vals);
1208 self.u = CsrMatrix::from_csr(n, n, u_row_ptr, u_cols, u_vals);
1209 self.inv_diag_u = inv_diag_u;
1210 self.nnz_l = nnz_l;
1211 self.nnz_u = nnz_u;
1212
1213 Ok(())
1214 }
1215
1216 fn compute_milu0(&mut self, matrix: &Mat<f64>) -> Result<(), KError> {
1218 let n = matrix.nrows();
1219
1220 let drop_tol: f64 = 1e-15;
1222 let mut l = CsrMatrix::from_dense(matrix, drop_tol)?;
1223 let mut u = CsrMatrix::from_dense(matrix, drop_tol)?;
1224
1225 let mut original_row_sums = vec![0.0; n];
1227 for i in 0..n {
1228 for j in 0..n {
1229 original_row_sums[i] = original_row_sums[i] + matrix[(i, j)];
1230 }
1231 }
1232
1233 for i in 0..n {
1235 for j in 0..n {
1236 if i > j {
1237 Self::sparse_set(&mut u, i, j, 0.0);
1238 } else if i < j {
1239 Self::sparse_set(&mut l, i, j, 0.0);
1240 } else {
1241 Self::sparse_set(&mut l, i, i, 1.0);
1242 }
1243 }
1244 }
1245
1246 for k in 0..n {
1248 let mut pivot = Self::sparse_get(&u, k, k);
1249 self.handle_pivot(&mut pivot, k, matrix)?;
1250 Self::sparse_set(&mut u, k, k, pivot);
1251
1252 for i in (k + 1)..n {
1253 let l_ik = Self::sparse_get(&l, i, k);
1254 if l_ik != 0.0 {
1255 let multiplier = l_ik / pivot;
1256 Self::sparse_set(&mut l, i, k, multiplier);
1257
1258 let mut dropped_sum = 0.0;
1259 for j in (k + 1)..n {
1260 let u_kj = Self::sparse_get(&u, k, j);
1261 if u_kj != 0.0 {
1262 let update = multiplier * u_kj;
1263 if matrix[(i, j)] != 0.0 {
1264 let u_ij = Self::sparse_get(&u, i, j);
1265 Self::sparse_set(&mut u, i, j, u_ij - update);
1266 } else {
1267 dropped_sum = dropped_sum + update;
1268 }
1269 }
1270 }
1271 let u_ii = Self::sparse_get(&u, i, i);
1273 Self::sparse_set(&mut u, i, i, u_ii + dropped_sum);
1274 }
1275 }
1276 }
1277
1278 self.nnz_l = l.nnz();
1279 self.nnz_u = u.nnz();
1280
1281 self.inv_diag_u = u.diagonal().into_iter().map(|v| 1.0 / v).collect();
1282
1283 self.l = l;
1284 self.u = u;
1285
1286 Ok(())
1287 }
1288
1289 fn compose_perm(new_then_old: &Permutation, old: &Permutation) -> Permutation {
1290 let n = old.len();
1291 let p: Vec<usize> = (0..n).map(|i| old.p[new_then_old.p[i]]).collect();
1292 let mut pinv = vec![0usize; n];
1293 for (new_i, &old_i) in p.iter().enumerate() {
1294 pinv[old_i] = new_i;
1295 }
1296 Permutation { p, pinv }
1297 }
1298
1299 fn maximum_transversal_permutations(matrix: &Mat<f64>) -> (Permutation, Permutation) {
1300 let n = matrix.nrows();
1301 let mut row_order: Vec<usize> = (0..n).collect();
1302 row_order.sort_unstable_by(|&a, &b| {
1303 let aa = matrix[(a, a)].abs();
1304 let bb = matrix[(b, b)].abs();
1305 aa.partial_cmp(&bb)
1306 .unwrap_or(std::cmp::Ordering::Equal)
1307 .then_with(|| a.cmp(&b))
1308 });
1309
1310 let mut row_neighbors = vec![Vec::<usize>::new(); n];
1311 for i in 0..n {
1312 for j in 0..n {
1313 if matrix[(i, j)] != 0.0 {
1314 row_neighbors[i].push(j);
1315 }
1316 }
1317 row_neighbors[i].sort_unstable_by(|&a, &b| {
1318 let aa = matrix[(i, a)].abs();
1319 let bb = matrix[(i, b)].abs();
1320 bb.partial_cmp(&aa)
1321 .unwrap_or(std::cmp::Ordering::Equal)
1322 .then_with(|| a.cmp(&b))
1323 });
1324 }
1325
1326 fn dfs_augment(
1327 r: usize,
1328 seen: &mut [bool],
1329 row_neighbors: &[Vec<usize>],
1330 col_match: &mut [Option<usize>],
1331 ) -> bool {
1332 for &c in &row_neighbors[r] {
1333 if seen[c] {
1334 continue;
1335 }
1336 seen[c] = true;
1337 let can_claim = match col_match[c] {
1338 None => true,
1339 Some(prev_r) => dfs_augment(prev_r, seen, row_neighbors, col_match),
1340 };
1341 if can_claim {
1342 col_match[c] = Some(r);
1343 return true;
1344 }
1345 }
1346 false
1347 }
1348
1349 let mut col_match = vec![None; n];
1350 for &r in &row_order {
1351 let mut seen = vec![false; n];
1352 let _ = dfs_augment(r, &mut seen, &row_neighbors, &mut col_match);
1353 }
1354
1355 let mut row_to_col = vec![None; n];
1356 for (c, &r) in col_match.iter().enumerate() {
1357 if let Some(rr) = r {
1358 row_to_col[rr] = Some(c);
1359 }
1360 }
1361
1362 let mut free_cols: Vec<usize> = (0..n).filter(|&c| col_match[c].is_none()).collect();
1363 let mut pairs = Vec::with_capacity(n);
1364 for (r, maybe_c) in row_to_col.iter().enumerate().take(n) {
1365 let c = maybe_c.unwrap_or_else(|| free_cols.pop().unwrap_or(r));
1366 pairs.push((r, c));
1367 }
1368 pairs.sort_unstable_by_key(|&(_r, c)| c);
1369
1370 let row_order: Vec<usize> = pairs.iter().map(|&(r, _)| r).collect();
1371 let col_order: Vec<usize> = pairs.iter().map(|&(_, c)| c).collect();
1372 (
1373 permutation_from_order(row_order),
1374 permutation_from_order(col_order),
1375 )
1376 }
1377
1378 fn compute_factor_permutations(&self, matrix: &Mat<f64>) -> (Permutation, Permutation) {
1380 let n = matrix.nrows();
1381 let (mut row_perm, mut col_perm) = Self::maximum_transversal_permutations(matrix);
1382 let matched = Self::permute_dense_nonsymmetric(matrix, &row_perm, &col_perm);
1383
1384 let mut adj = vec![Vec::new(); n];
1385 for i in 0..n {
1386 for j in (i + 1)..n {
1387 if matched[(i, j)] != 0.0 || matched[(j, i)] != 0.0 {
1388 adj[i].push(j);
1389 adj[j].push(i);
1390 }
1391 }
1392 }
1393
1394 let reorder = match self.config.reordering_type {
1395 ReorderingType::None | ReorderingType::Natural => None,
1396 ReorderingType::RCM => Some(rcm_from_adj(&mut adj)),
1397 ReorderingType::AMD => Some(amd_from_adj(&mut adj)),
1398 };
1399 if let Some(sym) = reorder {
1400 row_perm = Self::compose_perm(&sym, &row_perm);
1401 col_perm = Self::compose_perm(&sym, &col_perm);
1402 }
1403 (row_perm, col_perm)
1404 }
1405
1406 fn permute_dense_nonsymmetric(
1407 matrix: &Mat<f64>,
1408 row_perm: &Permutation,
1409 col_perm: &Permutation,
1410 ) -> Mat<f64> {
1411 let n = matrix.nrows();
1412 let mut result = Mat::zeros(n, n);
1413 for i in 0..n {
1414 let old_i = row_perm.p[i];
1415 for j in 0..n {
1416 let old_j = col_perm.p[j];
1417 result[(i, j)] = matrix[(old_i, old_j)];
1418 }
1419 }
1420 result
1421 }
1422
1423 fn prune_row_keep_largest(row: &mut BTreeMap<usize, f64>, max_keep: usize, min_col: usize) {
1424 if max_keep == 0 {
1425 return;
1426 }
1427
1428 let mut candidates: Vec<(usize, f64)> = row
1429 .iter()
1430 .filter_map(|(&j, &v)| (j >= min_col).then_some((j, v)))
1431 .collect();
1432 if candidates.len() <= max_keep {
1433 return;
1434 }
1435 candidates.sort_by(|a, b| {
1436 b.1.abs()
1437 .partial_cmp(&a.1.abs())
1438 .unwrap_or(std::cmp::Ordering::Equal)
1439 });
1440 candidates.truncate(max_keep);
1441 let keep: std::collections::HashSet<usize> =
1442 candidates.into_iter().map(|(j, _)| j).collect();
1443 row.retain(|&j, _| j < min_col || keep.contains(&j));
1444 }
1445
1446 fn compute_iluk(&mut self, matrix: &Mat<f64>) -> Result<(), KError> {
1451 let n = matrix.nrows();
1452 let (row_perm, col_perm) = self.compute_factor_permutations(matrix);
1453 self.row_perm = row_perm.p.clone();
1454 self.row_perm_inv = row_perm.pinv.clone();
1455 self.col_perm = col_perm.p.clone();
1456 self.col_perm_inv = col_perm.pinv.clone();
1457 let factor_matrix = Self::permute_dense_nonsymmetric(matrix, &row_perm, &col_perm);
1458 let lfill = self.config.level_of_fill;
1459
1460 let mut l_rows = vec![BTreeMap::<usize, f64>::new(); n];
1461 let mut u_rows = vec![BTreeMap::<usize, f64>::new(); n];
1462 let mut level_rows = vec![HashMap::<usize, usize>::new(); n];
1463
1464 for i in 0..n {
1465 l_rows[i].insert(i, 1.0);
1466 for j in 0..n {
1467 let aij = factor_matrix[(i, j)];
1468 if aij == 0.0 {
1469 continue;
1470 }
1471 level_rows[i].insert(j, 0);
1472 if j < i {
1473 l_rows[i].insert(j, aij);
1474 } else {
1475 u_rows[i].insert(j, aij);
1476 }
1477 }
1478 }
1479
1480 for i in 0..n {
1481 let mut w = BTreeMap::<usize, f64>::new();
1482 for (&j, &v) in &l_rows[i] {
1483 if j < i {
1484 w.insert(j, v);
1485 }
1486 }
1487 for (&j, &v) in &u_rows[i] {
1488 w.insert(j, v);
1489 }
1490 let mut w_level = level_rows[i].clone();
1491
1492 let lower_cols: Vec<usize> = w.keys().copied().filter(|&j| j < i).collect();
1493 for k in lower_cols {
1494 let Some(&lev_ik) = w_level.get(&k) else {
1495 continue;
1496 };
1497 if lev_ik > lfill {
1498 continue;
1499 }
1500
1501 let mut ukk = u_rows[k].get(&k).copied().unwrap_or(0.0);
1502 self.handle_pivot(&mut ukk, k, &factor_matrix)?;
1503 u_rows[k].insert(k, ukk);
1504 self.inv_diag_u.resize(n, 0.0);
1505 self.inv_diag_u[k] = 1.0 / ukk;
1506
1507 let lik = w.get(&k).copied().unwrap_or(0.0) / ukk;
1508 w.insert(k, lik);
1509
1510 for (&j, &ukj) in &u_rows[k] {
1511 if j <= k {
1512 continue;
1513 }
1514 let lev_kj = *level_rows[k].get(&j).unwrap_or(&usize::MAX);
1515 if lev_kj == usize::MAX {
1516 continue;
1517 }
1518 let new_level = lev_ik.saturating_add(lev_kj).saturating_add(1);
1519 if new_level > lfill {
1520 continue;
1521 }
1522 let entry = w.entry(j).or_insert(0.0);
1523 *entry -= lik * ukj;
1524 let cur = w_level.get(&j).copied().unwrap_or(usize::MAX);
1525 if new_level < cur {
1526 w_level.insert(j, new_level);
1527 }
1528 }
1529 }
1530
1531 l_rows[i].clear();
1532 l_rows[i].insert(i, 1.0);
1533 u_rows[i].clear();
1534
1535 for (j, v) in w {
1536 let lev = *w_level.get(&j).unwrap_or(&usize::MAX);
1537 if lev > lfill {
1538 continue;
1539 }
1540 if j < i {
1541 l_rows[i].insert(j, v);
1542 } else {
1543 u_rows[i].insert(j, v);
1544 }
1545 }
1546
1547 let mut pivot = u_rows[i].get(&i).copied().unwrap_or(0.0);
1548 self.handle_pivot(&mut pivot, i, &factor_matrix)?;
1549 u_rows[i].insert(i, pivot);
1550 self.inv_diag_u.resize(n, 0.0);
1551 self.inv_diag_u[i] = 1.0 / pivot;
1552 level_rows[i] = w_level;
1553 }
1554
1555 let mut l_row_ptr = Vec::with_capacity(n + 1);
1556 let mut l_cols = Vec::new();
1557 let mut l_vals = Vec::new();
1558 l_row_ptr.push(0);
1559 for (i, row) in l_rows.iter().enumerate() {
1560 for (&j, &v) in row {
1561 if j <= i {
1562 l_cols.push(j);
1563 l_vals.push(v);
1564 }
1565 }
1566 l_row_ptr.push(l_cols.len());
1567 }
1568
1569 let mut u_row_ptr = Vec::with_capacity(n + 1);
1570 let mut u_cols = Vec::new();
1571 let mut u_vals = Vec::new();
1572 u_row_ptr.push(0);
1573 for (i, row) in u_rows.iter().enumerate() {
1574 for (&j, &v) in row {
1575 if j >= i {
1576 u_cols.push(j);
1577 u_vals.push(v);
1578 }
1579 }
1580 u_row_ptr.push(u_cols.len());
1581 }
1582
1583 self.l = CsrMatrix::from_csr(n, n, l_row_ptr, l_cols, l_vals);
1584 self.u = CsrMatrix::from_csr(n, n, u_row_ptr, u_cols, u_vals);
1585 self.nnz_l = self.l.nnz();
1586 self.nnz_u = self.u.nnz();
1587
1588 Ok(())
1589 }
1590
1591 fn compute_ilut(&mut self, matrix: &Mat<f64>) -> Result<(), KError> {
1596 let n = matrix.nrows();
1597 let (row_perm, col_perm) = self.compute_factor_permutations(matrix);
1598 self.row_perm = row_perm.p.clone();
1599 self.row_perm_inv = row_perm.pinv.clone();
1600 self.col_perm = col_perm.p.clone();
1601 self.col_perm_inv = col_perm.pinv.clone();
1602 let factor_matrix = Self::permute_dense_nonsymmetric(matrix, &row_perm, &col_perm);
1603 let drop_tol: f64 = self.config.drop_tolerance;
1604 let max_fill = self.config.max_fill_per_row;
1605
1606 let mut l_rows = vec![BTreeMap::<usize, f64>::new(); n];
1607 let mut u_rows = vec![BTreeMap::<usize, f64>::new(); n];
1608
1609 self.inv_diag_u = vec![0.0; n];
1610
1611 for i in 0..n {
1612 let mut w = BTreeMap::<usize, f64>::new();
1613 for j in 0..n {
1614 let aij = factor_matrix[(i, j)];
1615 if aij.abs() >= drop_tol || j == i {
1616 w.insert(j, aij);
1617 }
1618 }
1619
1620 let lower_cols: Vec<usize> = w.keys().copied().filter(|&j| j < i).collect();
1621 for k in lower_cols {
1622 let wk = w.get(&k).copied().unwrap_or(0.0);
1623 if wk.abs() < drop_tol {
1624 w.remove(&k);
1625 continue;
1626 }
1627 let mut ukk = u_rows[k].get(&k).copied().unwrap_or(0.0);
1628 self.handle_pivot(&mut ukk, k, &factor_matrix)?;
1629 u_rows[k].insert(k, ukk);
1630 self.inv_diag_u[k] = 1.0 / ukk;
1631
1632 let lik = wk / ukk;
1633 if lik.abs() < drop_tol {
1634 w.remove(&k);
1635 continue;
1636 }
1637 w.insert(k, lik);
1638
1639 for (&j, &ukj) in &u_rows[k] {
1640 if j <= k {
1641 continue;
1642 }
1643 let new_val = w.get(&j).copied().unwrap_or(0.0) - lik * ukj;
1644 if new_val.abs() >= drop_tol {
1645 w.insert(j, new_val);
1646 } else if j != i {
1647 w.remove(&j);
1648 }
1649 }
1650 }
1651
1652 l_rows[i].clear();
1653 l_rows[i].insert(i, 1.0);
1654 u_rows[i].clear();
1655 for (j, v) in w {
1656 if j < i {
1657 if v.abs() >= drop_tol {
1658 l_rows[i].insert(j, v);
1659 }
1660 } else if j == i || v.abs() >= drop_tol {
1661 u_rows[i].insert(j, v);
1662 }
1663 }
1664 Self::prune_row_keep_largest(&mut l_rows[i], max_fill, 0);
1665 l_rows[i].retain(|&j, _| j < i || j == i);
1666 Self::prune_row_keep_largest(&mut u_rows[i], max_fill, i + 1);
1667
1668 let mut pivot = u_rows[i].get(&i).copied().unwrap_or(0.0);
1669 self.handle_pivot(&mut pivot, i, &factor_matrix)?;
1670 u_rows[i].insert(i, pivot);
1671 self.inv_diag_u[i] = 1.0 / pivot;
1672 }
1673
1674 let mut l_row_ptr = Vec::with_capacity(n + 1);
1675 let mut l_cols = Vec::new();
1676 let mut l_vals = Vec::new();
1677 l_row_ptr.push(0);
1678 for (i, row) in l_rows.iter().enumerate() {
1679 for (&j, &v) in row {
1680 if j <= i {
1681 l_cols.push(j);
1682 l_vals.push(v);
1683 }
1684 }
1685 l_row_ptr.push(l_cols.len());
1686 }
1687
1688 let mut u_row_ptr = Vec::with_capacity(n + 1);
1689 let mut u_cols = Vec::new();
1690 let mut u_vals = Vec::new();
1691 u_row_ptr.push(0);
1692 for (i, row) in u_rows.iter().enumerate() {
1693 for (&j, &v) in row {
1694 if j >= i {
1695 u_cols.push(j);
1696 u_vals.push(v);
1697 }
1698 }
1699 u_row_ptr.push(u_cols.len());
1700 }
1701
1702 self.l = CsrMatrix::from_csr(n, n, l_row_ptr, l_cols, l_vals);
1703 self.u = CsrMatrix::from_csr(n, n, u_row_ptr, u_cols, u_vals);
1704 self.nnz_l = self.l.nnz();
1705 self.nnz_u = self.u.nnz();
1706 Ok(())
1707 }
1708
1709 fn lu_row_product(
1711 &self,
1712 i: usize,
1713 j: usize,
1714 l_row_ptr: &[usize],
1715 l_col_idx: &[usize],
1716 l_vals: &[f64],
1717 u_row_ptr: &[usize],
1718 u_col_idx: &[usize],
1719 u_vals: &[f64],
1720 ) -> f64 {
1721 let limit = i.min(j);
1722 let start_l = l_row_ptr[i];
1723 let end_l = l_row_ptr[i + 1];
1724 let mut sum = 0.0;
1725
1726 for idx in start_l..end_l {
1727 let k = l_col_idx[idx];
1728 if k >= limit {
1729 break;
1730 }
1731 let lik = l_vals[idx];
1732 let start_u = u_row_ptr[k];
1733 let end_u = u_row_ptr[k + 1];
1734 if let Ok(off) = u_col_idx[start_u..end_u].binary_search(&j) {
1735 let ukj = u_vals[start_u + off];
1736 sum = f64::mul_add(lik, ukj, sum);
1737 }
1738 }
1739
1740 sum
1741 }
1742
1743 #[allow(clippy::too_many_arguments)]
1745 fn parilu_sweep_serial(
1746 &self,
1747 a: &CsrMatrix<f64>,
1748 l_row_ptr: &[usize],
1749 l_col_idx: &[usize],
1750 l_old: &[f64],
1751 u_row_ptr: &[usize],
1752 u_col_idx: &[usize],
1753 u_old: &[f64],
1754 l_new: &mut [f64],
1755 u_new: &mut [f64],
1756 omega: f64,
1757 ) -> Result<f64, KError> {
1758 let n = a.nrows();
1759 let mut res_sq = 0.0;
1760
1761 let get_u_diag = |j: usize| -> f64 {
1762 let start = u_row_ptr[j];
1763 let end = u_row_ptr[j + 1];
1764 for idx in start..end {
1765 if u_col_idx[idx] == j {
1766 return u_old[idx];
1767 }
1768 }
1769 0.0
1770 };
1771
1772 for i in 0..n {
1773 let (a_cols, a_vals) = a.row(i);
1774
1775 let l_start = l_row_ptr[i];
1777 let l_end = l_row_ptr[i + 1];
1778 for idx in l_start..l_end {
1779 let j = l_col_idx[idx];
1780 if j >= i {
1781 continue;
1782 }
1783
1784 let a_ij = match a_cols.binary_search(&j) {
1785 Ok(pos) => a_vals[pos],
1786 Err(_) => 0.0,
1787 };
1788
1789 let s_ij = self.lu_row_product(
1790 i, j, l_row_ptr, l_col_idx, l_old, u_row_ptr, u_col_idx, u_old,
1791 );
1792
1793 let r_ij = a_ij - s_ij;
1794
1795 let u_jj = get_u_diag(j);
1796 if u_jj == 0.0 {
1797 return Err(KError::ZeroPivot(j));
1798 }
1799
1800 let lij_old = l_old[idx];
1801 let lij_new = (1.0 - omega) * lij_old + omega * (r_ij / u_jj);
1802 l_new[idx] = lij_new;
1803
1804 res_sq += r_ij * r_ij;
1805 }
1806
1807 let u_start = u_row_ptr[i];
1809 let u_end = u_row_ptr[i + 1];
1810 for idx in u_start..u_end {
1811 let j = u_col_idx[idx];
1812 if j < i {
1813 continue;
1814 }
1815
1816 let a_ij = match a_cols.binary_search(&j) {
1817 Ok(pos) => a_vals[pos],
1818 Err(_) => 0.0,
1819 };
1820
1821 let s_ij = self.lu_row_product(
1822 i, j, l_row_ptr, l_col_idx, l_old, u_row_ptr, u_col_idx, u_old,
1823 );
1824
1825 let r_ij = a_ij - s_ij;
1826
1827 let uij_old = u_old[idx];
1828 let uij_new = (1.0 - omega) * uij_old + omega * r_ij;
1829 u_new[idx] = uij_new;
1830
1831 res_sq += r_ij * r_ij;
1832 }
1833 }
1834
1835 Ok(res_sq.sqrt())
1836 }
1837
1838 #[allow(clippy::too_many_arguments)]
1840 fn parilu_sweep(
1841 &self,
1842 a: &CsrMatrix<f64>,
1843 l_row_ptr: &[usize],
1844 l_col_idx: &[usize],
1845 l_old: &[f64],
1846 u_row_ptr: &[usize],
1847 u_col_idx: &[usize],
1848 u_old: &[f64],
1849 l_new: &mut [f64],
1850 u_new: &mut [f64],
1851 omega: f64,
1852 ) -> Result<f64, KError> {
1853 #[cfg(feature = "rayon")]
1854 {
1855 if self.config.enable_parallel_factorization {
1856 return self.parilu_sweep_parallel(
1857 a, l_row_ptr, l_col_idx, l_old, u_row_ptr, u_col_idx, u_old, l_new, u_new,
1858 omega,
1859 );
1860 }
1861 }
1862 self.parilu_sweep_serial(
1863 a, l_row_ptr, l_col_idx, l_old, u_row_ptr, u_col_idx, u_old, l_new, u_new, omega,
1864 )
1865 }
1866
1867 #[cfg(feature = "rayon")]
1869 #[allow(clippy::too_many_arguments)]
1870 fn parilu_sweep_parallel(
1871 &self,
1872 a: &CsrMatrix<f64>,
1873 l_row_ptr: &[usize],
1874 l_col_idx: &[usize],
1875 l_old: &[f64],
1876 u_row_ptr: &[usize],
1877 u_col_idx: &[usize],
1878 u_old: &[f64],
1879 l_new: &mut [f64],
1880 u_new: &mut [f64],
1881 omega: f64,
1882 ) -> Result<f64, KError> {
1883 let n = a.nrows();
1884
1885 let l_ptr = l_new.as_mut_ptr() as usize;
1889 let u_ptr = u_new.as_mut_ptr() as usize;
1890
1891 let res_sq: Result<f64, KError> = (0..n)
1892 .into_par_iter()
1893 .map(|i| {
1894 let (a_cols, a_vals) = a.row(i);
1895 let mut res_sq_i = 0.0;
1896
1897 let l_start = l_row_ptr[i];
1898 let l_end = l_row_ptr[i + 1];
1899 let u_start = u_row_ptr[i];
1900 let u_end = u_row_ptr[i + 1];
1901
1902 let get_u_diag = |j: usize| -> f64 {
1903 let start = u_row_ptr[j];
1904 let end = u_row_ptr[j + 1];
1905 for idx in start..end {
1906 if u_col_idx[idx] == j {
1907 return u_old[idx];
1908 }
1909 }
1910 0.0
1911 };
1912
1913 for idx in l_start..l_end {
1914 let j = l_col_idx[idx];
1915 if j >= i {
1916 continue;
1917 }
1918
1919 let a_ij = match a_cols.binary_search(&j) {
1920 Ok(pos) => a_vals[pos],
1921 Err(_) => 0.0,
1922 };
1923
1924 let s_ij = self.lu_row_product(
1925 i, j, l_row_ptr, l_col_idx, l_old, u_row_ptr, u_col_idx, u_old,
1926 );
1927 let r_ij = a_ij - s_ij;
1928
1929 let u_jj = get_u_diag(j);
1930 if u_jj == 0.0 {
1931 return Err(KError::ZeroPivot(j));
1932 }
1933
1934 let lij_old = l_old[idx];
1935 let lij_new = (1.0 - omega) * lij_old + omega * (r_ij / u_jj);
1936 unsafe { *(l_ptr as *mut f64).add(idx) = lij_new };
1937
1938 res_sq_i += r_ij * r_ij;
1939 }
1940
1941 for idx in u_start..u_end {
1942 let j = u_col_idx[idx];
1943 if j < i {
1944 continue;
1945 }
1946
1947 let a_ij = match a_cols.binary_search(&j) {
1948 Ok(pos) => a_vals[pos],
1949 Err(_) => 0.0,
1950 };
1951
1952 let s_ij = self.lu_row_product(
1953 i, j, l_row_ptr, l_col_idx, l_old, u_row_ptr, u_col_idx, u_old,
1954 );
1955 let r_ij = a_ij - s_ij;
1956
1957 let uij_old = u_old[idx];
1958 let uij_new = (1.0 - omega) * uij_old + omega * r_ij;
1959 unsafe { *(u_ptr as *mut f64).add(idx) = uij_new };
1960
1961 res_sq_i += r_ij * r_ij;
1962 }
1963
1964 Ok(res_sq_i)
1965 })
1966 .try_reduce(|| 0.0, |a, b| Ok(a + b));
1967
1968 Ok(res_sq?.sqrt())
1969 }
1970
1971 fn parilu_refine(&mut self, a: &CsrMatrix<f64>) -> Result<(), KError> {
1973 if !self.config.parilu_enabled || self.config.parilu_max_iters == 0 {
1974 self.history = None;
1975 return Ok(());
1976 }
1977
1978 let n = a.nrows();
1979 if n == 0 {
1980 self.history = None;
1981 return Ok(());
1982 }
1983
1984 if a.ncols() != n || self.l.nrows() != n || self.u.nrows() != n {
1985 return Err(KError::InvalidInput(
1986 "ParILU requires square matrices with matching dimensions".to_string(),
1987 ));
1988 }
1989
1990 let max_iters = self.config.parilu_max_iters;
1991 let min_iters = self.config.parilu_min_iters.min(max_iters);
1992 let omega = self.config.parilu_omega;
1993 let tol = self.config.parilu_tol;
1994
1995 let l_row_ptr = self.l.row_ptr().to_vec();
1997 let l_col_idx = self.l.col_idx().to_vec();
1998 let mut l_vals = self.l.values().to_vec();
1999
2000 let u_row_ptr = self.u.row_ptr().to_vec();
2001 let u_col_idx = self.u.col_idx().to_vec();
2002 let mut u_vals = self.u.values().to_vec();
2003
2004 let mut l_old = l_vals.clone();
2005 let mut u_old = u_vals.clone();
2006
2007 let mut history = ParIluHistory::with_capacity(max_iters);
2008
2009 for iter in 0..max_iters {
2010 let iter_start = std::time::Instant::now();
2011 let res = self.parilu_sweep(
2012 a,
2013 &l_row_ptr,
2014 &l_col_idx,
2015 &l_old,
2016 &u_row_ptr,
2017 &u_col_idx,
2018 &u_old,
2019 &mut l_vals,
2020 &mut u_vals,
2021 omega,
2022 )?;
2023 let iter_time = iter_start.elapsed().as_secs_f64();
2024
2025 history.push(ParIluIterSample {
2026 iter: iter as u32,
2027 residual: res,
2028 time_s: iter_time,
2029 });
2030
2031 if let Some(m) = &self.monitor {
2032 if let Some(sample) = history.as_slice().last() {
2033 m.on_event(Event::IluSetupIter { sample });
2034 }
2035 }
2036
2037 if iter + 1 >= min_iters && res < tol {
2038 break;
2039 }
2040
2041 l_old.copy_from_slice(&l_vals);
2042 u_old.copy_from_slice(&u_vals);
2043 }
2044
2045 self.l = CsrMatrix::from_csr(n, n, l_row_ptr, l_col_idx, l_vals);
2046 self.u = CsrMatrix::from_csr(n, n, u_row_ptr, u_col_idx, u_vals);
2047 self.inv_diag_u = self.u.diagonal().into_iter().map(|v| 1.0 / v).collect();
2048 self.nnz_l = self.l.nnz();
2049 self.nnz_u = self.u.nnz();
2050 self.history = Some(history);
2051
2052 #[cfg(feature = "logging")]
2053 if self.config.logging_level > 0 {
2054 if let Some(last) = self.history.as_ref().and_then(|h| h.as_slice().last()) {
2055 let converged = last.residual < tol;
2056 info!(
2057 "ParILU refinement: iterations={}, final residual {:.3e} (tol {:.3e}, converged={})",
2058 self.history
2059 .as_ref()
2060 .map(|h| h.as_slice().len())
2061 .unwrap_or(0),
2062 last.residual,
2063 tol,
2064 converged
2065 );
2066 }
2067 }
2068
2069 Ok(())
2070 }
2071
2072 fn setup_workspace(&mut self, n: usize) {
2074 debug_assert_eq!(
2075 self.l.nrows(),
2076 n,
2077 "L dimension mismatch during workspace sizing"
2078 );
2079 debug_assert_eq!(
2080 self.u.nrows(),
2081 n,
2082 "U dimension mismatch during workspace sizing"
2083 );
2084
2085 if self.config.optimize_workspace {
2086 self.workspace.ensure_size(n);
2088
2089 #[cfg(feature = "logging")]
2090 if self.config.logging_level > 1 {
2091 debug!("ILU: Workspace configured for {n} x {n} matrix");
2092 }
2093 } else {
2094 self.workspace.ensure_size(n);
2096 }
2097 }
2098
2099 fn solve_triangular_exact(&self, lower: bool, x: &mut [S]) {
2101 #[cfg(feature = "rayon")]
2102 if self.allow_parallel_triangular_solve(x.len()) {
2103 if lower {
2104 self.solve_triangular_parallel_forward(x);
2105 } else {
2106 self.solve_triangular_parallel_backward(x);
2107 }
2108 return;
2109 }
2110
2111 self.solve_triangular_exact_seq(lower, x);
2112 }
2113
2114 #[inline]
2115 fn solve_triangular_exact_seq(&self, lower: bool, x: &mut [S]) {
2116 let n = x.len();
2117 if lower {
2118 for i in 0..n {
2120 let mut sum = x[i];
2121 let (cols, vals) = self.l.row(i);
2122 for (&j, &val) in cols.iter().zip(vals.iter()) {
2123 if j < i {
2124 sum -= val * x[j];
2125 }
2126 }
2127 x[i] = sum;
2128 }
2129 } else {
2130 for i in (0..n).rev() {
2132 let mut sum = x[i];
2133 let (cols, vals) = self.u.row(i);
2134 for (&j, &val) in cols.iter().zip(vals.iter()) {
2135 if j <= i {
2136 continue;
2137 }
2138 sum -= val * x[j];
2139 }
2140 x[i] = sum * self.inv_diag_u[i];
2141 }
2142 }
2143 }
2144
2145 #[cfg(feature = "rayon")]
2146 fn solve_triangular_parallel_forward(&self, x: &mut [S]) {
2147 if x.is_empty() {
2148 return;
2149 }
2150
2151 let chunk_size = self.config.parallel_chunk_size.max(1);
2152 let levels = &self.levels_l;
2153 if levels.buckets.is_empty() {
2154 return;
2155 }
2156
2157 for rows in &levels.buckets {
2158 if rows.is_empty() {
2159 continue;
2160 }
2161
2162 let x_addr = x.as_mut_ptr() as usize;
2163 rows.par_iter().with_min_len(chunk_size).for_each(|&i| {
2164 let x_ptr = x_addr as *mut S;
2165 let mut sum = unsafe { *x_ptr.add(i) };
2166 let (cols, vals) = self.l.row(i);
2167 for (&j, &val) in cols.iter().zip(vals.iter()) {
2168 if j < i {
2169 sum -= val * unsafe { *x_ptr.add(j) };
2170 }
2171 }
2172 unsafe { *x_ptr.add(i) = sum };
2175 });
2176 }
2177 }
2178
2179 #[cfg(feature = "rayon")]
2180 fn solve_triangular_parallel_backward(&self, x: &mut [S]) {
2181 if x.is_empty() {
2182 return;
2183 }
2184
2185 let chunk_size = self.config.parallel_chunk_size.max(1);
2186 let levels = &self.levels_u;
2187 if levels.buckets.is_empty() {
2188 return;
2189 }
2190
2191 for ell in 0..=levels.max_level {
2192 let rows = &levels.buckets[ell as usize];
2193 if rows.is_empty() {
2194 continue;
2195 }
2196
2197 let x_addr = x.as_mut_ptr() as usize;
2198 rows.par_iter().with_min_len(chunk_size).for_each(|&i| {
2199 let x_ptr = x_addr as *mut S;
2200 let mut sum = unsafe { *x_ptr.add(i) };
2201 let (cols, vals) = self.u.row(i);
2202 for (&j, &val) in cols.iter().zip(vals.iter()) {
2203 if j <= i {
2204 continue;
2205 }
2206 sum -= val * unsafe { *x_ptr.add(j) };
2207 }
2208 unsafe { *x_ptr.add(i) = sum * self.inv_diag_u[i] };
2211 });
2212 }
2213 }
2214
2215 fn solve_triangular_jacobi(&self, lower: bool, b: &[S], x: &mut [S]) {
2217 let n = b.len();
2218 let num_iters = if lower {
2219 self.config.lower_jacobi_iters
2220 } else {
2221 self.config.upper_jacobi_iters
2222 };
2223
2224 x.copy_from_slice(b);
2226
2227 for _iter in 0..num_iters {
2228 if lower {
2229 for i in 0..n {
2231 let mut sum = S::zero();
2232 let (cols, vals) = self.l.row(i);
2233 for (&j, &val) in cols.iter().zip(vals.iter()) {
2234 if j < i {
2235 sum = sum + val * x[j];
2236 }
2237 }
2238 x[i] = b[i] - sum; }
2240 } else {
2241 for i in (0..n).rev() {
2243 let mut sum = S::zero();
2244 let (cols, vals) = self.u.row(i);
2245 for (&j, &val) in cols.iter().zip(vals.iter()) {
2246 if j > i {
2247 sum = sum + val * x[j];
2248 }
2249 }
2250 x[i] = (b[i] - sum) * self.inv_diag_u[i];
2251 }
2252 }
2253 }
2254 }
2255
2256 fn solve_triangular_gauss_seidel(&self, lower: bool, b: &[S], x: &mut [S]) {
2258 let n = b.len();
2259 let num_iters = if lower {
2260 self.config.lower_jacobi_iters
2261 } else {
2262 self.config.upper_jacobi_iters
2263 };
2264
2265 x.copy_from_slice(b);
2267
2268 for _iter in 0..num_iters {
2269 if lower {
2270 for i in 0..n {
2272 let mut sum = S::zero();
2273 let (cols, vals) = self.l.row(i);
2274 for (&j, &val) in cols.iter().zip(vals.iter()) {
2275 if j < i {
2276 sum = sum + val * x[j];
2277 }
2278 }
2279 x[i] = b[i] - sum; }
2281 } else {
2282 for i in (0..n).rev() {
2284 let mut sum = S::zero();
2285 let (cols, vals) = self.u.row(i);
2286 for (&j, &val) in cols.iter().zip(vals.iter()) {
2287 if j > i {
2288 sum = sum + val * x[j];
2289 }
2290 }
2291 x[i] = (b[i] - sum) * self.inv_diag_u[i];
2292 }
2293 }
2294 }
2295 }
2296
2297 pub fn get_stats(&self) -> IluStats {
2299 let (total_ns, count, _) = self.solve_ctrs.snapshot();
2300 let avg = if count == 0 {
2301 0.0
2302 } else {
2303 (total_ns as f64) / (count as f64) / 1e9
2304 };
2305 IluStats {
2306 setup_complexity: self.setup_complexity,
2307 nnz_l: self.nnz_l,
2308 nnz_u: self.nnz_u,
2309 num_zero_pivots: self.num_zero_pivots,
2310 setup_time: self.setup_time,
2311 solve_time: avg,
2312 solve_count: count as usize,
2313 }
2314 }
2315
2316 pub fn pivot_stats(&self) -> &PivotStats {
2318 &self.pivot_stats
2319 }
2320
2321 pub fn complex_setup_used_native(&self) -> bool {
2322 self.complex_setup_used_native
2323 }
2324
2325 pub fn complex_setup_fallback_reason(&self) -> Option<&str> {
2326 self.complex_setup_fallback_reason.as_deref()
2327 }
2328}
2329
2330#[cfg(not(feature = "complex"))]
2331impl Ilu {
2332 pub fn create_specialized(
2338 config: IluConfig,
2339 ) -> Result<Box<dyn Preconditioner<Mat<S>, Vec<S>>>, KError> {
2340 match config.ilu_type {
2341 IluType::ILUK => {
2342 let ilup = crate::preconditioner::ilup::Ilup::new(config.level_of_fill);
2344 Ok(Box::new(ilup))
2345 }
2346 IluType::ILUT | _ => {
2347 let ilu = Ilu::new_with_config(config)?;
2349 Ok(Box::new(ilu))
2350 }
2351 }
2352 }
2353}
2354
2355#[cfg(feature = "complex")]
2356impl Ilu {
2357 pub fn create_specialized(
2363 config: IluConfig,
2364 ) -> Result<Box<dyn Preconditioner<Mat<S>, Vec<S>>>, KError> {
2365 let ilu = Ilu::new_with_config(config)?;
2366 Ok(Box::new(ilu))
2367 }
2368}
2369
2370impl Ilu {
2371 #[cfg(test)]
2373 pub(crate) fn config(&self) -> &IluConfig {
2374 &self.config
2375 }
2376
2377 pub fn create_quick(ilu_type: IluType, fill_or_drop: Real) -> Result<Self, KError> {
2379 let mut config = IluConfig::default();
2380 config.ilu_type = ilu_type;
2381
2382 match ilu_type {
2383 IluType::ILUK => {
2384 config.level_of_fill = fill_or_drop as usize;
2385 }
2386 IluType::ILUT => {
2387 config.drop_tolerance = fill_or_drop;
2388 config.max_fill_per_row = 20; }
2390 _ => {}
2391 }
2392
2393 Self::new_with_config(config)
2394 }
2395}
2396
2397#[derive(Debug, Clone)]
2399pub struct IluStats {
2400 pub setup_complexity: f64,
2402 pub nnz_l: usize,
2404 pub nnz_u: usize,
2406 pub num_zero_pivots: usize,
2408 pub setup_time: f64,
2410 pub solve_time: f64,
2412 pub solve_count: usize,
2414}
2415
2416impl Default for Ilu {
2417 fn default() -> Self {
2418 Self::new()
2419 }
2420}
2421
2422impl Preconditioner<Mat<f64>, Vec<f64>> for Ilu {
2423 fn setup(&mut self, matrix: &Mat<f64>) -> Result<(), KError> {
2425 let setup_start = std::time::Instant::now();
2426 self.complex_setup_used_native = true;
2427 self.complex_setup_fallback_reason = None;
2428
2429 if let Some(m) = &self.monitor {
2430 m.on_event(Event::IluSetupBegin { opts_hash: 0 });
2431 }
2432
2433 Self::validate_matrix(matrix)?;
2435
2436 if self.config.ieee_checks {
2437 Self::check_ieee_values(matrix)?;
2438
2439 #[cfg(feature = "logging")]
2440 if self.config.logging_level > 0 {
2441 info!("ILU: IEEE safety checks passed");
2442 }
2443 }
2444
2445 let mut conditioned = None;
2446 let matrix = if self.config.conditioning.is_active() {
2447 let mut local = matrix.clone();
2448 apply_dense_transforms("ILU", &mut local, &self.config.conditioning)?;
2449 conditioned = Some(local);
2450 conditioned.as_ref().unwrap()
2451 } else {
2452 matrix
2453 };
2454
2455 let n = matrix.nrows();
2456 let a_csr = CsrMatrix::from_dense(matrix, 1e-15)?;
2457 let original_nnz = a_csr.nnz();
2458
2459 #[cfg(feature = "logging")]
2460 print_ilu_banner(&self.config);
2461
2462 let mut max_diag: Real = Real::default();
2464 self.row_inf_a.resize(n, Real::default());
2465 self.row_gersh_a.resize(n, Real::default());
2466 for i in 0..n {
2467 let mut row_inf: Real = Real::default();
2468 let mut row_gersh = matrix[(i, i)].abs();
2469 for j in 0..n {
2470 let val_abs = matrix[(i, j)].abs();
2471 if j != i {
2472 row_gersh = row_gersh + val_abs;
2473 }
2474 if val_abs > row_inf {
2475 row_inf = val_abs;
2476 }
2477 }
2478 self.row_inf_a[i] = row_inf;
2479 self.row_gersh_a[i] = row_gersh;
2480 max_diag = max_diag.max(matrix[(i, i)].abs());
2481 }
2482 self.max_diag_a = max_diag;
2483 self.running_max_u = Real::default();
2484 self.pivot_stats = PivotStats::default();
2485 self.history = None;
2486 self.row_perm = (0..n).collect();
2487 self.row_perm_inv = (0..n).collect();
2488 self.col_perm = (0..n).collect();
2489 self.col_perm_inv = (0..n).collect();
2490
2491 #[cfg(feature = "logging")]
2492 if self.config.logging_level > 0 {
2493 info!("ILU Setup: {n} x {n} matrix with {original_nnz} nonzeros");
2494 debug!("ILU: Using {:?} factorization type", self.config.ilu_type);
2495 }
2496
2497 let mut parilu_iters = 0u32;
2498 let mut parilu_converged = true;
2499
2500 let par_mode = if self.allow_parallel_factorization(n) {
2502 self.config.par_factor_mode()
2503 } else {
2504 ParFactorizationMode::Serial
2505 };
2506
2507 match (self.config.ilu_type, par_mode) {
2508 (IluType::ILU0, ParFactorizationMode::Serial) => {
2509 self.compute_ilu0(matrix)?;
2510 }
2511 (IluType::ILU0, ParFactorizationMode::Block)
2512 | (IluType::ILU0, ParFactorizationMode::ParIlu) => {
2513 self.compute_ilu0_block_parallel(matrix)?;
2514 }
2515 (IluType::MILU0, _) => {
2516 self.compute_milu0(matrix)?;
2517 }
2518 (IluType::ILUK, _) => {
2519 self.compute_iluk(matrix)?;
2520 }
2521 (IluType::ILUT, _) => {
2522 self.compute_ilut(matrix)?;
2523 }
2524 _ => {
2525 return Err(KError::NotImplemented(format!(
2526 "ILU type {:?} not yet implemented",
2527 self.config.ilu_type
2528 )));
2529 }
2530 }
2531
2532 if self.config.parilu_enabled && self.config.parilu_max_iters > 0 {
2533 self.parilu_refine(&a_csr)?;
2534 if let Some(hist) = &self.history {
2535 parilu_iters = hist.as_slice().len() as u32;
2536 parilu_converged = hist
2537 .as_slice()
2538 .last()
2539 .map(|s| s.residual < self.config.parilu_tol)
2540 .unwrap_or(false);
2541 }
2542 } else {
2543 self.history = None;
2544 }
2545
2546 self.setup_workspace(n);
2548
2549 self.setup_complexity = self.calculate_complexity(original_nnz);
2551 self.setup_time = setup_start.elapsed().as_secs_f64();
2552
2553 #[cfg(feature = "rayon")]
2554 if self.allow_parallel_triangular_solve(n) {
2555 self.levels_l = build_levels_lower(&self.l);
2556 self.levels_u = build_levels_upper(&self.u);
2557 }
2558
2559 #[cfg(feature = "logging")]
2560 if self.config.logging_level > 0 {
2561 info!(
2562 "ILU Setup Complete: complexity={:.2}, L_nnz={}, U_nnz={}, setup_time={:.3}s",
2563 self.setup_complexity, self.nnz_l, self.nnz_u, self.setup_time
2564 );
2565
2566 if let Some(hist) = &self.history {
2567 if let Some(last) = hist.as_slice().last() {
2568 info!(
2569 "ParILU refinement: sweeps={}, final residual {:.2e} (tol {:.2e})",
2570 hist.as_slice().len(),
2571 last.residual,
2572 self.config.parilu_tol
2573 );
2574 }
2575 }
2576
2577 debug!(
2578 "Pivot floors: {} (max shift {:.3e})",
2579 self.pivot_stats.num_floors, self.pivot_stats.max_abs_shift
2580 );
2581
2582 if self.num_zero_pivots > 0 {
2583 warn!(
2584 "ILU: {} zero pivots encountered during factorization",
2585 self.num_zero_pivots
2586 );
2587 }
2588
2589 if self.config.print_level > 0 {
2590 println!(
2591 "ILU Setup: {} -> {} nonzeros (complexity: {:.2})",
2592 original_nnz,
2593 self.nnz_l + self.nnz_u,
2594 self.setup_complexity
2595 );
2596 }
2597 }
2598 if let Some(m) = &self.monitor {
2599 m.on_event(Event::IluSetupEnd {
2600 iters: parilu_iters,
2601 converged: parilu_converged,
2602 setup_time_s: self.setup_time,
2603 });
2604 }
2605
2606 Ok(())
2607 }
2608
2609 fn apply(&self, side: PcSide, x: &Vec<f64>, y: &mut Vec<f64>) -> Result<(), KError> {
2612 self.apply_slice(side, x.as_slice(), y.as_mut_slice())
2613 }
2614}
2615
2616impl Ilu {
2617 fn apply_slice(&self, _side: PcSide, x: &[f64], y: &mut [f64]) -> Result<(), KError> {
2618 let n = self.l.nrows();
2619 if x.len() != n || y.len() != n {
2620 return Err(KError::InvalidInput(format!(
2621 "Vector length mismatch: expected {}, got x={} y={}",
2622 n,
2623 x.len(),
2624 y.len(),
2625 )));
2626 }
2627
2628 let _timer = SolveTimer::start(&self.solve_ctrs);
2629 let has_perm = !self.row_perm.is_empty()
2630 && (self.row_perm.iter().enumerate().any(|(i, &p)| i != p)
2631 || self.col_perm.iter().enumerate().any(|(i, &p)| i != p));
2632
2633 if has_perm {
2634 let mut perm_in = self.workspace.borrow_solve_buf(n);
2635 for i in 0..n {
2636 perm_in[i] = x[self.row_perm[i]];
2637 }
2638
2639 let mut tmp = self.workspace.temp2.lock().unwrap();
2640 let solve_rhs = &perm_in[..n];
2641 let solve_out = &mut tmp[..n];
2642
2643 match self.config.triangular_solve {
2644 TriSolveType::Exact => {
2645 solve_out.copy_from_slice(solve_rhs);
2646 self.solve_triangular_exact(true, solve_out);
2647 self.solve_triangular_exact(false, solve_out);
2648 }
2649 TriSolveType::Jacobi => {
2650 self.solve_triangular_jacobi(true, solve_rhs, solve_out);
2651 self.solve_triangular_jacobi(false, solve_out, &mut perm_in[..n]);
2652 solve_out.copy_from_slice(&perm_in[..n]);
2653 }
2654 TriSolveType::GaussSeidel => {
2655 self.solve_triangular_gauss_seidel(true, solve_rhs, solve_out);
2656 self.solve_triangular_gauss_seidel(false, solve_out, &mut perm_in[..n]);
2657 solve_out.copy_from_slice(&perm_in[..n]);
2658 }
2659 }
2660
2661 for old_i in 0..n {
2662 y[old_i] = solve_out[self.col_perm_inv[old_i]];
2663 }
2664 return Ok(());
2665 }
2666
2667 match self.config.triangular_solve {
2668 TriSolveType::Exact => {
2669 y.copy_from_slice(x);
2670 self.solve_triangular_exact(true, y);
2671 self.solve_triangular_exact(false, y);
2672 }
2673 TriSolveType::Jacobi => {
2674 let mut buf = self.workspace.borrow_solve_buf(n);
2675 self.solve_triangular_jacobi(true, x, &mut buf[..n]);
2676 self.solve_triangular_jacobi(false, &buf[..n], y);
2677 }
2678 TriSolveType::GaussSeidel => {
2679 let mut buf = self.workspace.borrow_solve_buf(n);
2680 self.solve_triangular_gauss_seidel(true, x, &mut buf[..n]);
2681 self.solve_triangular_gauss_seidel(false, &buf[..n], y);
2682 }
2683 }
2684
2685 #[cfg(feature = "logging")]
2686 if self.config.logging_level > 2 {
2687 let _solve_time = _timer.elapsed().as_secs_f64();
2688 trace!(
2689 "ILU Apply: solve_time={:.6}s, workspace_size={}",
2690 _solve_time, self.workspace.size
2691 );
2692 }
2693
2694 Ok(())
2695 }
2696}
2697
2698impl TriangularSolve<f64> for Ilu {
2699 fn solve_lower_in_place(&self, x: &mut [f64]) -> Result<(), KError> {
2700 self.solve_triangular_exact(true, x);
2701 Ok(())
2702 }
2703
2704 fn solve_upper_in_place(&self, x: &mut [f64]) -> Result<(), KError> {
2705 self.solve_triangular_exact(false, x);
2706 Ok(())
2707 }
2708}
2709
2710impl Ilu {
2711 pub fn parilu_history(&self) -> Option<&[ParIluIterSample]> {
2712 self.history.as_ref().map(|h| h.as_slice())
2713 }
2714
2715 pub fn set_monitor(&mut self, m: Option<Box<dyn Monitor>>) {
2716 self.monitor = m;
2717 }
2718}
2719
2720impl LocalPreconditioner<f64> for Ilu {
2721 fn dims(&self) -> (usize, usize) {
2722 (self.l.nrows(), self.l.ncols())
2723 }
2724
2725 fn apply_local(&self, x: &[S], y: &mut [S]) -> Result<(), KError> {
2726 let (n, _) = LocalPreconditioner::<f64>::dims(self);
2727 debug_assert_eq!(x.len(), n);
2728 debug_assert_eq!(y.len(), n);
2729 self.apply_slice(PcSide::Left, x, y)
2730 }
2731}
2732
2733#[cfg(feature = "complex")]
2734impl KPreconditioner for Ilu {
2735 type Scalar = GlobalScalar;
2736
2737 #[inline]
2738 fn dims(&self) -> (usize, usize) {
2739 LocalPreconditioner::<f64>::dims(self)
2740 }
2741
2742 fn apply_s(
2743 &self,
2744 side: PcSide,
2745 x: &[GlobalScalar],
2746 y: &mut [GlobalScalar],
2747 scratch: &mut BridgeScratch,
2748 ) -> Result<(), KError> {
2749 let (rows, cols) = LocalPreconditioner::<f64>::dims(self);
2750 let n = x.len();
2751 if x.len() != y.len() || rows != n || cols != n {
2752 return Err(KError::InvalidInput(format!(
2753 "Ilu::apply_s dimension mismatch: expected {}x{}, got x.len()={} y.len()={}",
2754 rows,
2755 cols,
2756 x.len(),
2757 y.len()
2758 )));
2759 }
2760
2761 scratch.with_pair(n, |xr, yr| {
2762 copy_scalar_to_real_in(x, xr);
2763 self.apply_slice(side, xr, yr)?;
2764
2765 for (dst, &re) in y.iter_mut().zip(yr.iter()) {
2766 *dst = GlobalScalar::from_parts(re, 0.0);
2767 }
2768
2769 let mut yi = self.workspace.temp2.lock().unwrap();
2770 for (dst, &src) in xr.iter_mut().zip(x.iter()) {
2771 *dst = src.imag();
2772 }
2773 self.apply_slice(side, xr, &mut yi[..n])?;
2774 for (dst, &im) in y.iter_mut().zip(yi[..n].iter()) {
2775 *dst = GlobalScalar::from_parts(dst.real(), im);
2776 }
2777 Ok(())
2778 })
2779 }
2780
2781 fn apply_mut_s(
2782 &mut self,
2783 side: PcSide,
2784 x: &[GlobalScalar],
2785 y: &mut [GlobalScalar],
2786 scratch: &mut BridgeScratch,
2787 ) -> Result<(), KError> {
2788 KPreconditioner::apply_s(self, side, x, y, scratch)
2789 }
2790
2791 fn on_restart_s(
2792 &mut self,
2793 _outer_iter: usize,
2794 _residual_norm: <GlobalScalar as KrystScalar>::Real,
2795 ) -> Result<(), KError> {
2796 Ok(())
2797 }
2798}
2799
2800pub type Ilu0 = Ilu;
2802
2803#[cfg(test)]
2804mod tests {
2805 use super::{Ilu, IluBuilder, IluConfig, IluType, TriSolveType};
2806 use crate::algebra::parallel::par_sum_abs2_local;
2807 use crate::algebra::prelude::*;
2808 use crate::error::KError;
2809 use crate::matrix::sparse::CsrMatrix;
2810 use crate::preconditioner::PcSide;
2811 use crate::preconditioner::legacy::Preconditioner;
2812 #[cfg(not(feature = "complex"))]
2813 use rand::{RngExt, SeedableRng, rngs::StdRng};
2814
2815 #[cfg(feature = "rayon")]
2816 use rayon::prelude::*;
2817 #[cfg(feature = "rayon")]
2818 use std::sync::Arc;
2819
2820 fn make_spd_3x3() -> faer::Mat<S> {
2821 faer::Mat::from_fn(3, 3, |i, j| match (i, j) {
2825 (0, 0) => S::from_real(4.0),
2826 (0, 1) | (1, 0) => S::from_real(1.0),
2827 (1, 1) => S::from_real(3.0),
2828 (1, 2) | (2, 1) => S::from_real(1.0),
2829 (2, 2) => S::from_real(2.0),
2830 _ => S::zero(),
2831 })
2832 }
2833
2834 fn mat_vec_mul(a: &faer::Mat<S>, x: &[S]) -> Vec<S> {
2835 let mut out = vec![S::zero(); a.nrows()];
2836 for i in 0..a.nrows() {
2837 let mut acc = S::zero();
2838 for j in 0..a.ncols() {
2839 acc = acc + a[(i, j)] * x[j];
2840 }
2841 out[i] = acc;
2842 }
2843 out
2844 }
2845
2846 #[cfg(not(feature = "complex"))]
2847 fn mat_vec_mul_inplace(a: &faer::Mat<S>, x: &[S], y: &mut [S]) {
2848 for i in 0..a.nrows() {
2849 let mut acc = S::zero();
2850 for j in 0..a.ncols() {
2851 acc = acc + a[(i, j)] * x[j];
2852 }
2853 y[i] = acc;
2854 }
2855 }
2856
2857 #[cfg(not(feature = "complex"))]
2858 fn random_spd(n: usize, seed: u64) -> faer::Mat<S> {
2859 let mut rng = StdRng::seed_from_u64(seed);
2860 let mut a = faer::Mat::zeros(n, n);
2861 for i in 0..n {
2862 for j in 0..=i {
2863 let v = rng.random_range(-1.0..1.0);
2864 a[(i, j)] = S::from_real(v);
2865 a[(j, i)] = S::from_real(v);
2866 }
2867 }
2868 for i in 0..n {
2869 a[(i, i)] = a[(i, i)] + S::from_real(n as f64 + 1.0);
2870 }
2871 a
2872 }
2873
2874 #[cfg(not(feature = "complex"))]
2875 fn cg_unpreconditioned(a: &faer::Mat<S>, b: &[S], x0: &[S], max_iter: usize) -> (usize, f64) {
2876 let n = b.len();
2877 let mut x = x0.to_vec();
2878 let mut r = vec![S::zero(); n];
2879 let mut p = vec![S::zero(); n];
2880 let mut ap = vec![S::zero(); n];
2881
2882 mat_vec_mul_inplace(a, &x, &mut r);
2883 for i in 0..n {
2884 r[i] = b[i] - r[i];
2885 }
2886 p.copy_from_slice(&r);
2887 let mut rr = dot(&r, &r);
2888
2889 let mut iters = 0;
2890 while iters < max_iter && rr > 1e-20 {
2891 mat_vec_mul_inplace(a, &p, &mut ap);
2892 let denom = dot(&p, &ap);
2893 if denom.abs() < 1e-30 {
2894 break;
2895 }
2896 let alpha = rr / denom;
2897 for i in 0..n {
2898 x[i] = x[i] + alpha * p[i];
2899 r[i] = r[i] - alpha * ap[i];
2900 }
2901 let rr_new = dot(&r, &r);
2902 if rr_new.sqrt() < 1e-10 {
2903 rr = rr_new;
2904 iters += 1;
2905 break;
2906 }
2907 let beta = rr_new / rr;
2908 for i in 0..n {
2909 p[i] = r[i] + beta * p[i];
2910 }
2911 rr = rr_new;
2912 iters += 1;
2913 }
2914
2915 (iters, rr.sqrt())
2916 }
2917
2918 #[cfg(not(feature = "complex"))]
2919 fn cg_left_preconditioned(
2920 a: &faer::Mat<S>,
2921 pc: &Ilu,
2922 b: &[S],
2923 x0: &[S],
2924 max_iter: usize,
2925 ) -> (usize, f64) {
2926 let n = b.len();
2927 let mut x = x0.to_vec();
2928 let mut r = vec![S::zero(); n];
2929 let mut z = vec![S::zero(); n];
2930 let mut p = vec![S::zero(); n];
2931 let mut ap = vec![S::zero(); n];
2932
2933 mat_vec_mul_inplace(a, &x, &mut r);
2934 for i in 0..n {
2935 r[i] = b[i] - r[i];
2936 }
2937 pc.apply(PcSide::Left, &r, &mut z).expect("pc apply");
2938 p.copy_from_slice(&z);
2939 let mut rz = dot(&r, &z);
2940
2941 let mut iters = 0;
2942 while iters < max_iter && rz.abs() > 1e-20 {
2943 mat_vec_mul_inplace(a, &p, &mut ap);
2944 let denom = dot(&p, &ap);
2945 if denom.abs() < 1e-30 {
2946 break;
2947 }
2948 let alpha = rz / denom;
2949 for i in 0..n {
2950 x[i] = x[i] + alpha * p[i];
2951 r[i] = r[i] - alpha * ap[i];
2952 }
2953 let r_norm = dot(&r, &r).sqrt();
2954 if r_norm < 1e-10 {
2955 rz = r_norm;
2956 iters += 1;
2957 break;
2958 }
2959 pc.apply(PcSide::Left, &r, &mut z).expect("pc apply");
2960 let rz_new = dot(&r, &z);
2961 let beta = rz_new / rz;
2962 for i in 0..n {
2963 p[i] = z[i] + beta * p[i];
2964 }
2965 rz = rz_new;
2966 iters += 1;
2967 }
2968
2969 (iters, dot(&r, &r).sqrt())
2970 }
2971
2972 #[cfg(not(feature = "complex"))]
2973 fn dot(x: &[S], y: &[S]) -> f64 {
2974 x.iter().zip(y.iter()).map(|(&a, &b)| a * b).sum()
2975 }
2976
2977 #[cfg(feature = "rayon")]
2978 fn make_tridiag_matrix(n: usize) -> faer::Mat<f64> {
2979 faer::Mat::from_fn(n, n, |i, j| {
2980 if i == j {
2981 4.0
2982 } else if (i as isize - j as isize).abs() == 1 {
2983 -1.0
2984 } else {
2985 0.0
2986 }
2987 })
2988 }
2989
2990 #[test]
2991 fn test_ilu_default_creation() {
2992 let ilu = Ilu::new();
2993 assert_eq!(ilu.config.ilu_type, IluType::ILU0);
2994 }
2995
2996 #[test]
2997 fn test_ilu_builder() {
2998 let ilu = IluBuilder::new()
2999 .ilu_type(IluType::ILUT)
3000 .drop_tolerance(1e-6)
3001 .enable_logging()
3002 .build()
3003 .unwrap();
3004
3005 assert_eq!(ilu.config.ilu_type, IluType::ILUT);
3006 assert_eq!(ilu.config.drop_tolerance, 1e-6);
3007 assert_eq!(ilu.config.logging_level, 1);
3008 }
3009
3010 #[test]
3011 fn test_ilu_config_validation() {
3012 let mut config = IluConfig::default();
3013 config.drop_tolerance = -1.0;
3014
3015 let result = Ilu::new_with_config(config);
3016 assert!(result.is_err());
3017 }
3018
3019 #[test]
3020 fn test_ilu0_simple_matrix() {
3021 let matrix = faer::Mat::from_fn(3, 3, |i, j| {
3022 if i == j {
3023 4.0
3024 } else if (i as i32 - j as i32).abs() == 1 {
3025 -1.0
3026 } else {
3027 0.0
3028 }
3029 });
3030
3031 let mut ilu = Ilu::new();
3032 use crate::preconditioner::legacy::Preconditioner;
3033 let result = ilu.setup(&matrix);
3034 assert!(result.is_ok());
3035
3036 let stats = ilu.get_stats();
3037 assert!(stats.setup_complexity > 0.0);
3038 assert_eq!(stats.num_zero_pivots, 0);
3039 }
3040
3041 #[test]
3042 fn test_enhanced_pivot_handling() {
3043 let matrix = faer::Mat::from_fn(3, 3, |i, j| {
3044 if i == j && i == 1 {
3045 1e-15 } else if i == j {
3047 1.0
3048 } else {
3049 0.0
3050 }
3051 });
3052
3053 let config = IluConfig::default();
3055 let mut ilu = Ilu::new_with_config(config).unwrap();
3056 use crate::preconditioner::legacy::Preconditioner;
3057 let result = ilu.setup(&matrix);
3058 assert!(result.is_ok());
3059 assert!(ilu.pivot_stats().num_floors > 0);
3060 }
3061
3062 #[cfg(not(feature = "complex"))]
3063 #[test]
3064 fn test_ilu_variants() {
3065 let _matrix = faer::Mat::from_fn(3, 3, |i, j| {
3066 if i == j {
3067 4.0
3068 } else if (i as i32 - j as i32).abs() == 1 {
3069 -1.0
3070 } else {
3071 0.0
3072 }
3073 });
3074
3075 let ilu_k = Ilu::create_quick(IluType::ILUK, 1.0).unwrap();
3077 assert_eq!(ilu_k.config.ilu_type, IluType::ILUK);
3078 assert_eq!(ilu_k.config.level_of_fill, 1);
3079
3080 let ilu_t = Ilu::create_quick(IluType::ILUT, 1e-6).unwrap();
3082 assert_eq!(ilu_t.config.ilu_type, IluType::ILUT);
3083 assert_eq!(ilu_t.config.drop_tolerance, 1e-6);
3084 }
3085
3086 #[test]
3087 fn test_triangular_solve_options() {
3088 let matrix = faer::Mat::from_fn(2, 2, |i, j| if i == j { 2.0 } else { 0.0 });
3089
3090 let mut config = IluConfig::default();
3092 config.triangular_solve = TriSolveType::GaussSeidel;
3093 config.lower_jacobi_iters = 2;
3094 config.upper_jacobi_iters = 2;
3095
3096 let mut ilu = Ilu::new_with_config(config).unwrap();
3097 use crate::preconditioner::legacy::Preconditioner;
3098 let result = ilu.setup(&matrix);
3099 assert!(result.is_ok());
3100 }
3101
3102 #[test]
3103 fn test_matching_permutation_strengthens_diagonal() {
3104 let matrix = faer::Mat::from_fn(3, 3, |i, j| match (i, j) {
3105 (0, 1) => 2.0,
3106 (1, 0) => 3.0,
3107 (2, 2) => 4.0,
3108 _ => 0.0,
3109 });
3110 let (row_perm, col_perm) = Ilu::maximum_transversal_permutations(&matrix);
3111 let permuted = Ilu::permute_dense_nonsymmetric(&matrix, &row_perm, &col_perm);
3112 for i in 0..3 {
3113 assert_ne!(
3114 permuted[(i, i)],
3115 0.0,
3116 "diagonal entry at {i} should be nonzero"
3117 );
3118 }
3119 }
3120
3121 #[test]
3122 fn test_apply_respects_distinct_row_col_permutations() {
3123 let mut ilu = Ilu::new();
3124 ilu.l = CsrMatrix::from_csr(2, 2, vec![0, 1, 2], vec![0, 1], vec![1.0, 1.0]);
3125 ilu.u = CsrMatrix::from_csr(2, 2, vec![0, 1, 2], vec![0, 1], vec![1.0, 1.0]);
3126 ilu.inv_diag_u = vec![1.0, 1.0];
3127 ilu.row_perm = vec![1, 0];
3128 ilu.row_perm_inv = vec![1, 0];
3129 ilu.col_perm = vec![0, 1];
3130 ilu.col_perm_inv = vec![0, 1];
3131 ilu.setup_workspace(2);
3132
3133 let x = vec![2.0, 5.0];
3134 let mut y = vec![0.0; 2];
3135 use crate::preconditioner::legacy::Preconditioner;
3136 ilu.apply(PcSide::Left, &x, &mut y)
3137 .expect("apply with nonsymmetric mapping should succeed");
3138 assert_eq!(y, vec![5.0, 2.0]);
3139 }
3140
3141 #[cfg(not(feature = "complex"))]
3142 #[test]
3143 fn test_specialized_factory() {
3144 let config = IluConfig {
3145 ilu_type: IluType::ILUK,
3146 level_of_fill: 2,
3147 ..Default::default()
3148 };
3149
3150 let ilu_box = Ilu::create_specialized(config);
3151 assert!(ilu_box.is_ok());
3152 }
3153
3154 #[test]
3155 fn test_parallel_configuration() {
3156 let ilu = IluBuilder::new()
3157 .enable_parallel()
3158 .parallel_chunk_size(128)
3159 .build()
3160 .unwrap();
3161
3162 assert!(ilu.config.enable_parallel_factorization);
3163 assert!(ilu.config.enable_parallel_triangular_solve);
3164 assert_eq!(ilu.config.parallel_chunk_size, 128);
3165 }
3166
3167 #[test]
3168 fn test_workspace_optimization() {
3169 let matrix = faer::Mat::from_fn(3, 3, |i, j| {
3170 if i == j {
3171 4.0
3172 } else if (i as i32 - j as i32).abs() == 1 {
3173 -1.0
3174 } else {
3175 0.0
3176 }
3177 });
3178
3179 let mut ilu = IluBuilder::new().ilu_type(IluType::ILU0).build().unwrap();
3180
3181 use crate::preconditioner::legacy::Preconditioner;
3182 let result = ilu.setup(&matrix);
3183 assert!(result.is_ok());
3184
3185 assert!(ilu.workspace.size > 0);
3187
3188 let x = vec![1.0, 2.0, 3.0];
3190 let mut y = vec![0.0; 3];
3191 use crate::preconditioner::PcSide;
3192 let apply_result = ilu.apply(PcSide::Left, &x, &mut y);
3193 assert!(apply_result.is_ok());
3194 }
3195
3196 #[cfg(feature = "rayon")]
3197 #[test]
3198 fn test_parallel_factorization() {
3199 let matrix = faer::Mat::from_fn(10, 10, |i, j| {
3200 if i == j {
3201 4.0
3202 } else if (i as i32 - j as i32).abs() == 1 {
3203 -1.0
3204 } else {
3205 0.0
3206 }
3207 });
3208
3209 let mut ilu_serial = IluBuilder::new().ilu_type(IluType::ILU0).build().unwrap();
3210
3211 let mut ilu_parallel = IluBuilder::new()
3212 .ilu_type(IluType::ILU0)
3213 .enable_parallel_factorization()
3214 .parallel_chunk_size(2) .build()
3216 .unwrap();
3217
3218 use crate::preconditioner::legacy::Preconditioner;
3219
3220 let serial_result = ilu_serial.setup(&matrix);
3221 assert!(serial_result.is_ok());
3222
3223 let parallel_result = ilu_parallel.setup(&matrix);
3224 assert!(parallel_result.is_ok());
3225
3226 let serial_stats = ilu_serial.get_stats();
3227 let parallel_stats = ilu_parallel.get_stats();
3228
3229 assert!(
3230 parallel_stats.nnz_l <= serial_stats.nnz_l,
3231 "block ILU should not increase L nz count: serial={} parallel={}",
3232 serial_stats.nnz_l,
3233 parallel_stats.nnz_l
3234 );
3235 assert!(
3236 parallel_stats.nnz_u <= serial_stats.nnz_u,
3237 "block ILU should not increase U nz count: serial={} parallel={}",
3238 serial_stats.nnz_u,
3239 parallel_stats.nnz_u
3240 );
3241 }
3242
3243 #[cfg(feature = "rayon")]
3244 #[test]
3245 fn block_ilu_single_block_matches_serial() {
3246 let n = 6;
3247 let matrix = make_tridiag_matrix(n);
3248
3249 let mut ilu_serial = IluBuilder::new().ilu_type(IluType::ILU0).build().unwrap();
3250 ilu_serial.setup(&matrix).unwrap();
3251
3252 let mut config = IluConfig::default();
3253 config.enable_parallel_factorization = true;
3254 config.parallel_chunk_size = n;
3255 let mut ilu_block = Ilu::new_with_config(config).unwrap();
3256 ilu_block.setup(&matrix).unwrap();
3257
3258 let serial_stats = ilu_serial.get_stats();
3259 let block_stats = ilu_block.get_stats();
3260 assert_eq!(
3261 block_stats.nnz_l, serial_stats.nnz_l,
3262 "single-block parallel should match serial L pattern"
3263 );
3264 assert_eq!(
3265 block_stats.nnz_u, serial_stats.nnz_u,
3266 "single-block parallel should match serial U pattern"
3267 );
3268
3269 let rhs: Vec<f64> = (0..n).map(|i| (i + 1) as f64).collect();
3270 let mut y_serial = vec![0.0; n];
3271 let mut y_block = vec![0.0; n];
3272 ilu_serial.apply(PcSide::Left, &rhs, &mut y_serial).unwrap();
3273 ilu_block.apply(PcSide::Left, &rhs, &mut y_block).unwrap();
3274
3275 for (&a, &b) in y_serial.iter().zip(y_block.iter()) {
3276 assert!((a - b).abs() < 1e-12, "serial {} vs block {}", a, b);
3277 }
3278 }
3279
3280 #[cfg(feature = "rayon")]
3281 #[test]
3282 fn block_ilu_chunk_size_one_is_diag() {
3283 let n = 5;
3284 let matrix = make_tridiag_matrix(n);
3285
3286 let mut config = IluConfig::default();
3287 config.enable_parallel_factorization = true;
3288 config.parallel_chunk_size = 1;
3289 let mut ilu_block = Ilu::new_with_config(config).unwrap();
3290 ilu_block.setup(&matrix).unwrap();
3291
3292 let stats = ilu_block.get_stats();
3293 assert_eq!(stats.nnz_l, n);
3294 assert_eq!(stats.nnz_u, n);
3295
3296 let rhs = vec![1.0; n];
3297 let mut sol = vec![0.0; n];
3298 ilu_block.apply(PcSide::Left, &rhs, &mut sol).unwrap();
3299 for &val in sol.iter() {
3300 assert!(!val.is_nan());
3301 }
3302 }
3303
3304 #[cfg(feature = "rayon")]
3305 #[test]
3306 fn triangular_parallel_matches_sequential() {
3307 let matrix = make_tridiag_matrix(5);
3308
3309 let mut cfg_seq = IluConfig::default();
3310 cfg_seq.triangular_solve = TriSolveType::Exact;
3311 cfg_seq.enable_parallel_triangular_solve = false;
3312
3313 let mut cfg_par = IluConfig::default();
3314 cfg_par.triangular_solve = TriSolveType::Exact;
3315 cfg_par.enable_parallel_triangular_solve = true;
3316 cfg_par.parallel_chunk_size = 1;
3317
3318 let mut ilu_seq = Ilu::new_with_config(cfg_seq).unwrap();
3319 ilu_seq.setup(&matrix).unwrap();
3320
3321 let mut ilu_par = Ilu::new_with_config(cfg_par).unwrap();
3322 ilu_par.setup(&matrix).unwrap();
3323
3324 let n = matrix.nrows();
3325 let x: Vec<f64> = (0..n).map(|i| (i + 1) as f64).collect();
3326
3327 let mut y_seq = vec![0.0; n];
3328 let mut y_par = vec![0.0; n];
3329
3330 ilu_seq.apply(PcSide::Left, &x, &mut y_seq).unwrap();
3331 ilu_par.apply(PcSide::Left, &x, &mut y_par).unwrap();
3332
3333 for (&seq_val, &par_val) in y_seq.iter().zip(y_par.iter()) {
3334 assert!(
3335 (seq_val - par_val).abs() < 1e-12,
3336 "seq={seq_val}, par={par_val} for n={n}"
3337 );
3338 }
3339 }
3340
3341 #[cfg(feature = "rayon")]
3342 #[test]
3343 fn triangular_parallel_matches_sequential_large() {
3344 let n = 64;
3345 let matrix = make_tridiag_matrix(n);
3346
3347 let mut cfg_seq = IluConfig::default();
3348 cfg_seq.triangular_solve = TriSolveType::Exact;
3349 cfg_seq.enable_parallel_triangular_solve = false;
3350
3351 let mut cfg_par = IluConfig::default();
3352 cfg_par.triangular_solve = TriSolveType::Exact;
3353 cfg_par.enable_parallel_triangular_solve = true;
3354 cfg_par.parallel_chunk_size = 16;
3355
3356 let mut ilu_seq = Ilu::new_with_config(cfg_seq).unwrap();
3357 ilu_seq.setup(&matrix).unwrap();
3358
3359 let mut ilu_par = Ilu::new_with_config(cfg_par).unwrap();
3360 ilu_par.setup(&matrix).unwrap();
3361
3362 let x: Vec<f64> = (0..n).map(|i| (i + 1) as f64).collect();
3363
3364 let mut y_seq = vec![0.0; n];
3365 let mut y_par = vec![0.0; n];
3366
3367 ilu_seq.apply(PcSide::Left, &x, &mut y_seq).unwrap();
3368 ilu_par.apply(PcSide::Left, &x, &mut y_par).unwrap();
3369
3370 for (&seq_val, &par_val) in y_seq.iter().zip(y_par.iter()) {
3371 assert!(
3372 (seq_val - par_val).abs() < 1e-10,
3373 "seq={seq_val}, par={par_val} for n={n}"
3374 );
3375 }
3376 }
3377
3378 #[test]
3379 fn test_distributed_configuration() {
3380 let ilu = IluBuilder::new().enable_distributed().build().unwrap();
3381
3382 assert!(ilu.config.enable_distributed);
3383 }
3384
3385 #[test]
3386 #[cfg(not(feature = "complex"))]
3387 fn ilu0_real_factorization_solves_spd() {
3388 let matrix = make_spd_3x3();
3389 let x_true = vec![S::from_real(1.0), S::from_real(2.0), S::from_real(-1.0)];
3390 let b = mat_vec_mul(&matrix, &x_true);
3391
3392 let mut ilu = Ilu::new();
3393 ilu.setup(&matrix).expect("ILU(0) setup");
3394
3395 let mut x = vec![S::zero(); b.len()];
3396 ilu.apply(PcSide::Left, &b, &mut x).expect("ILU(0) apply");
3397
3398 let r: Vec<S> = mat_vec_mul(&matrix, &x)
3399 .into_iter()
3400 .zip(b.iter())
3401 .map(|(ax, &bi)| ax - bi)
3402 .collect();
3403 let res_norm = par_sum_abs2_local(&r).sqrt();
3404 assert!(
3405 res_norm.real() < 1e-10,
3406 "residual too large: {:?}",
3407 res_norm
3408 );
3409 }
3410
3411 #[cfg(not(feature = "complex"))]
3412 #[test]
3413 fn ilu0_improves_cg_for_random_spd() {
3414 let n = 20;
3415 let a = random_spd(n, 12345);
3416 let b = vec![S::from_real(1.0); n];
3417 let x0 = vec![S::zero(); n];
3418
3419 let (iters_unpre, res_unpre) = cg_unpreconditioned(&a, &b, &x0, 200);
3420
3421 let mut ilu = Ilu::new();
3422 ilu.setup(&a).expect("ilu0 setup");
3423 let (iters_pc, res_pc) = cg_left_preconditioned(&a, &ilu, &b, &x0, 200);
3424
3425 assert!(
3426 res_pc < res_unpre * 0.5 || iters_pc < iters_unpre,
3427 "preconditioned res={res_pc} iters={iters_pc}, baseline res={res_unpre} iters={iters_unpre}"
3428 );
3429 }
3430
3431 #[test]
3432 fn parilu_refines_residual() {
3433 let matrix = faer::Mat::<f64>::from_fn(3, 3, |i, j| match (i, j) {
3434 (0, 0) => 4.0,
3435 (0, 1) | (1, 0) => 1.0,
3436 (1, 1) => 3.0,
3437 (1, 2) | (2, 1) => 1.0,
3438 (2, 2) => 2.0,
3439 _ => 0.0,
3440 });
3441 let rhs = vec![1.0f64; 3];
3442
3443 let mut baseline = Ilu::new();
3444 baseline.setup(&matrix).expect("baseline ILU");
3445 let mut y_base = vec![0.0; 3];
3446 baseline
3447 .apply(PcSide::Left, &rhs, &mut y_base)
3448 .expect("apply baseline");
3449 let res_base = {
3450 let mut sum = 0.0;
3451 for i in 0..matrix.nrows() {
3452 let mut ax = 0.0;
3453 for j in 0..matrix.ncols() {
3454 ax += matrix[(i, j)] * y_base[j];
3455 }
3456 let r = ax - rhs[i];
3457 sum += r * r;
3458 }
3459 sum.sqrt()
3460 };
3461
3462 let mut cfg = IluConfig::default();
3463 cfg.parilu_enabled = true;
3464 cfg.parilu_max_iters = 5;
3465 cfg.parilu_min_iters = 1;
3466 cfg.parilu_tol = 1e-8;
3467 let mut parilu = Ilu::new_with_config(cfg).unwrap();
3468 parilu.setup(&matrix).expect("parilu ILU");
3469 let mut y_parilu = vec![0.0; 3];
3470 parilu
3471 .apply(PcSide::Left, &rhs, &mut y_parilu)
3472 .expect("apply parilu");
3473 let res_parilu = {
3474 let mut sum = 0.0;
3475 for i in 0..matrix.nrows() {
3476 let mut ax = 0.0;
3477 for j in 0..matrix.ncols() {
3478 ax += matrix[(i, j)] * y_parilu[j];
3479 }
3480 let r = ax - rhs[i];
3481 sum += r * r;
3482 }
3483 sum.sqrt()
3484 };
3485
3486 assert!(
3487 res_parilu <= res_base + 1e-10,
3488 "ParILU should not worsen residual (baseline {res_base}, parilu {res_parilu})"
3489 );
3490
3491 let hist = parilu.parilu_history().unwrap();
3492 assert!(!hist.is_empty());
3493 }
3494
3495 #[test]
3496 fn ilu_rejects_nan_inf_when_ieee_checks_enabled() {
3497 let mut a = faer::Mat::zeros(3, 3);
3498 a[(0, 0)] = 1.0;
3499 a[(1, 1)] = f64::NAN;
3500 a[(2, 2)] = f64::INFINITY;
3501
3502 let mut cfg = IluConfig::default();
3503 cfg.ieee_checks = true;
3504 let mut ilu = Ilu::new_with_config(cfg).unwrap();
3505 let err = ilu.setup(&a).unwrap_err();
3506 match err {
3507 KError::InvalidInput(msg) => {
3508 assert!(
3509 msg.contains("NaN") || msg.contains("Infinity"),
3510 "unexpected message: {msg}"
3511 )
3512 }
3513 other => panic!("expected InvalidInput, got {other:?}"),
3514 }
3515 }
3516
3517 #[cfg(feature = "rayon")]
3518 #[test]
3519 fn parilu_parallel_matches_serial_small() {
3520 let n = 8;
3521 let matrix = make_tridiag_matrix(n);
3522
3523 let mut cfg_serial = IluConfig::default();
3524 cfg_serial.parilu_enabled = true;
3525 cfg_serial.parilu_max_iters = 3;
3526 cfg_serial.parilu_tol = 0.0;
3527
3528 let mut cfg_par = cfg_serial.clone();
3529 cfg_par.enable_parallel_factorization = true;
3530
3531 let mut ilu_serial = Ilu::new_with_config(cfg_serial).unwrap();
3532 ilu_serial.setup(&matrix).unwrap();
3533 let mut ilu_par = Ilu::new_with_config(cfg_par).unwrap();
3534 ilu_par.setup(&matrix).unwrap();
3535
3536 let h_serial = ilu_serial.parilu_history().unwrap();
3537 let h_par = ilu_par.parilu_history().unwrap();
3538 assert_eq!(h_serial.len(), h_par.len());
3539 let last_serial = h_serial.last().unwrap().residual;
3540 let last_par = h_par.last().unwrap().residual;
3541 assert!(
3542 (last_serial - last_par).abs() < 1e-6,
3543 "serial {last_serial} vs parallel {last_par}"
3544 );
3545 }
3546
3547 #[cfg(feature = "rayon")]
3548 #[test]
3549 fn parallel_triangular_stress() {
3550 let n = 1000;
3551 let matrix = make_tridiag_matrix(n);
3552
3553 let mut ilu = IluBuilder::new()
3554 .ilu_type(IluType::ILU0)
3555 .enable_parallel_triangular_solve()
3556 .build()
3557 .unwrap();
3558 ilu.setup(&matrix).unwrap();
3559
3560 let rhs = Arc::new(vec![1.0; n]);
3561 let ilu = Arc::new(ilu);
3562
3563 (0..100).into_par_iter().for_each(|_| {
3564 let mut y = vec![0.0; n];
3565 ilu.apply(PcSide::Left, &*rhs, &mut y).unwrap();
3566 assert!(y.iter().all(|v| v.is_finite()));
3567 });
3568 }
3569}
3570
3571#[cfg(all(test, feature = "complex"))]
3572mod tests_complex_bridge {
3573 use super::Ilu;
3574 use crate::algebra::bridge::BridgeScratch;
3575 use crate::algebra::scalar::KrystScalar;
3576 use crate::algebra::scalar::S as GlobalScalar;
3577 use crate::ops::kpc::KPreconditioner;
3578 use crate::preconditioner::PcSide;
3579 use crate::preconditioner::legacy::Preconditioner as LegacyPc;
3580 use faer::Mat;
3581
3582 #[test]
3583 fn apply_s_matches_real_path_for_ilu() {
3584 let matrix = Mat::from_fn(2, 2, |i, j| match (i, j) {
3585 (0, 0) => 4.0,
3586 (0, 1) | (1, 0) => 1.0,
3587 (1, 1) => 3.0,
3588 _ => 0.0,
3589 });
3590
3591 let mut ilu = Ilu::new();
3592 LegacyPc::setup(&mut ilu, &matrix).expect("ilu setup");
3593
3594 let rhs_real = vec![1.0f64, 2.0];
3595 let mut out_real = vec![0.0; rhs_real.len()];
3596 LegacyPc::apply(&ilu, PcSide::Left, &rhs_real, &mut out_real).expect("ilu real apply");
3597
3598 let rhs_s: Vec<GlobalScalar> = rhs_real
3599 .iter()
3600 .copied()
3601 .map(GlobalScalar::from_real)
3602 .collect();
3603 let mut out_s = vec![GlobalScalar::zero(); rhs_s.len()];
3604 let mut scratch = BridgeScratch::default();
3605 ilu.apply_s(PcSide::Left, &rhs_s, &mut out_s, &mut scratch)
3606 .expect("ilu apply_s");
3607
3608 for (ys, &yr) in out_s.iter().zip(out_real.iter()) {
3609 assert!((ys.real() - yr).abs() < 1e-12);
3610 }
3611 }
3612}
3613
3614#[cfg(test)]
3616pub mod benchmarks {
3617 use super::*;
3618 use std::time::Instant;
3619
3620 #[derive(Debug, Default)]
3622 pub struct AllocationStats {
3623 pub total_allocations: usize,
3624 pub total_bytes: usize,
3625 pub peak_memory: usize,
3626 pub solve_allocations: usize,
3627 }
3628
3629 pub fn benchmark_ilu_factorization(
3631 matrix_size: usize,
3632 nnz_per_row: usize,
3633 ) -> (f64, AllocationStats) {
3634 let matrix = create_sparse_test_matrix(matrix_size, nnz_per_row);
3636
3637 let start = Instant::now();
3638 let mut ilu = IluBuilder::new()
3639 .ilu_type(IluType::ILU0)
3640 .enable_parallel_factorization()
3641 .build()
3642 .unwrap();
3643
3644 let setup_result = ilu.setup(&matrix);
3646 let factorization_time = start.elapsed().as_secs_f64();
3647
3648 assert!(setup_result.is_ok());
3649
3650 let stats = AllocationStats {
3651 total_allocations: 1, total_bytes: matrix_size * matrix_size * 8, peak_memory: matrix_size * matrix_size * 8,
3654 solve_allocations: 0,
3655 };
3656
3657 (factorization_time, stats)
3658 }
3659
3660 pub fn benchmark_ilu_solve_phase(
3662 matrix_size: usize,
3663 num_solves: usize,
3664 ) -> (f64, AllocationStats) {
3665 let matrix = create_sparse_test_matrix(matrix_size, 3);
3666
3667 let mut ilu = IluBuilder::new().ilu_type(IluType::ILU0).build().unwrap();
3668
3669 ilu.setup(&matrix).unwrap();
3670
3671 let rhs = vec![1.0; matrix_size];
3672 let mut solution = vec![0.0; matrix_size];
3673
3674 ilu.apply(PcSide::Left, &rhs, &mut solution).unwrap();
3676
3677 let start = Instant::now();
3678 for _ in 0..num_solves {
3679 ilu.apply(PcSide::Left, &rhs, &mut solution).unwrap();
3681 }
3682 let solve_time = start.elapsed().as_secs_f64();
3683
3684 let stats = AllocationStats {
3685 total_allocations: 0, total_bytes: 0,
3687 peak_memory: matrix_size * 16, solve_allocations: 0, };
3690
3691 (solve_time, stats)
3692 }
3693
3694 fn create_sparse_test_matrix(size: usize, nnz_per_row: usize) -> faer::Mat<f64> {
3696 let mut matrix = faer::Mat::zeros(size, size);
3697
3698 for i in 0..size {
3699 matrix[(i, i)] = 4.0;
3701
3702 let mut count = 1; for offset in 1..=(nnz_per_row / 2) {
3705 if i >= offset && count < nnz_per_row {
3706 matrix[(i, i - offset)] = -1.0;
3707 count += 1;
3708 }
3709 if i + offset < size && count < nnz_per_row {
3710 matrix[(i, i + offset)] = -1.0;
3711 count += 1;
3712 }
3713 }
3714 }
3715
3716 matrix
3717 }
3718
3719 pub fn benchmark_storage_comparison(matrix_size: usize) -> (f64, f64, usize, usize) {
3721 let matrix = create_sparse_test_matrix(matrix_size, 5);
3722
3723 let start = Instant::now();
3725 let mut ilu_dense = IluBuilder::new().ilu_type(IluType::ILU0).build().unwrap();
3726 ilu_dense.setup(&matrix).unwrap();
3727 let dense_time = start.elapsed().as_secs_f64();
3728
3729 let dense_memory = matrix_size * matrix_size * 8 * 2; let sparse_memory = ilu_dense.nnz_l * 8 + ilu_dense.nnz_u * 8; let sparse_time = dense_time; (dense_time, sparse_time, dense_memory, sparse_memory)
3736 }
3737
3738 #[test]
3739 fn test_benchmark_small_matrix() {
3740 let (factorization_time, stats) = benchmark_ilu_factorization(100, 5);
3741 println!("Factorization time: {:.6}s", factorization_time);
3742 println!("Memory stats: {:?}", stats);
3743 assert!(factorization_time > 0.0);
3744 }
3745
3746 #[test]
3747 fn test_benchmark_solve_phase() {
3748 let (solve_time, stats) = benchmark_ilu_solve_phase(50, 100);
3749 println!("Solve time for 100 solves: {:.6}s", solve_time);
3750 println!("Solve allocation stats: {:?}", stats);
3751 assert!(solve_time > 0.0);
3752 assert_eq!(stats.solve_allocations, 0); }
3754
3755 #[test]
3756 fn test_storage_comparison() {
3757 let (dense_time, sparse_time, dense_mem, sparse_mem) = benchmark_storage_comparison(50);
3758 println!("Dense: {:.6}s, {}KB", dense_time, dense_mem / 1024);
3759 println!("Sparse: {:.6}s, {}KB", sparse_time, sparse_mem / 1024);
3760 assert!(sparse_mem < dense_mem); }
3762}