1#![cfg_attr(not(feature = "complex"), allow(dead_code))]
20
21#[cfg(not(feature = "complex"))]
22fn main() {
23 eprintln!(
24 "This example requires the `complex` feature. \\nre-run with `cargo run --features complex --example complex_matrix_market_demo`."
25 );
26}
27
28#[cfg(feature = "complex")]
29use kryst::error::KError;
30
31#[cfg(feature = "complex")]
32mod complex_demo {
33 use std::env;
34 use std::fs::File;
35 use std::io::Write;
36 use std::path::{Path, PathBuf};
37 use std::sync::Arc;
38 use std::time::Instant;
39
40 use super::KError;
41 use kryst::algebra::bridge::BridgeScratch;
42 use kryst::algebra::prelude::*;
43 use kryst::context::ksp_context::{ReorthPolicy, Workspace};
44 use kryst::matrix::DistCsrOp;
45 use kryst::matrix::dist_csr::DistributedPlanDiagnostics;
46 use kryst::matrix::sparse::CsrMatrix as SparseCsrMatrix;
47 use kryst::ops::klinop::KLinOp;
48 use kryst::ops::kpc::KPreconditioner;
49 use kryst::parallel::{Comm, UniverseComm};
50 use kryst::preconditioner::PcSide;
51 use kryst::preconditioner::Preconditioner;
52 use kryst::preconditioner::ilu_csr::{
53 IluCsr, IluCsrConfig, IluKind, ReorderingKind, ReorderingOptions,
54 };
55 use kryst::preconditioner::jacobi::Jacobi;
56 use kryst::preconditioner::overlap_ilu::{OverlapIluPc, OverlapRestriction};
57 use kryst::solver::fgmres::{
58 FgmresSolver, FgmresStagnationPolicy, FgmresVariant, OrthogMethod, ResidualCheckPolicy,
59 };
60 use kryst::solver::gmres::{GmresOrthog, GmresSolver, GmresVariant as PlainGmresVariant};
61 use kryst::solver::{LinearSolver, MonitorAction, MonitorCallback};
62 use kryst::utils::convergence::ConvergedReason;
63 use kryst::utils::matrix_market::read_matrix_market;
64
65 #[cfg(feature = "mpi")]
66 use kryst::parallel::MpiComm;
67 #[cfg(not(feature = "mpi"))]
68 use kryst::parallel::NoComm;
69
70 pub fn run() -> Result<(), KError> {
71 #[cfg(feature = "logging")]
72 let _ = env_logger::try_init();
73
74 #[cfg(feature = "mpi")]
75 let comm = UniverseComm::Mpi(Arc::new(MpiComm::new()));
76 #[cfg(not(feature = "mpi"))]
77 let comm = UniverseComm::NoComm(NoComm);
78
79 let rank = comm.rank();
80 let size = comm.size();
81 let config = BenchmarkConfig::from_env_args()?;
82 #[cfg(feature = "mpi")]
83 let is_parallel = matches!(comm, UniverseComm::Mpi(_)) && size > 1;
84 #[cfg(not(feature = "mpi"))]
85 let is_parallel = false;
86
87 if rank == 0 {
88 println!("Complex Matrix Market demo (GMRES/FGMRES)");
89 println!("Run mode: {}", config.run_mode.label());
90 println!(
91 "Parallel backend: {}",
92 if is_parallel {
93 format!("MPI ({} ranks)", size)
94 } else {
95 "serial".into()
96 }
97 );
98 println!(
99 "Benchmark runs: warmup={}, measured={}",
100 config.warmup_runs, config.measured_runs
101 );
102 println!(
103 "Mode profile: {}",
104 match config.run_mode {
105 RunMode::Correctness => {
106 "small matrices, explicit true residuals, optional replicated operator checks"
107 }
108 RunMode::Scalability => {
109 "distributed operator + local-block PC only, global norms only, lightweight reporting"
110 }
111 RunMode::Robustness => {
112 "robustness stress: fallback-enabled stagnation handling and restart-heavy behavior"
113 }
114 }
115 );
116 println!(
117 "Stagnation fallback: {} (min_inner_before_fallback={})",
118 if config.run_mode == RunMode::Correctness || !config.allow_stagnation_fallback {
119 "disabled"
120 } else {
121 "enabled"
122 },
123 config.min_inner_before_fallback
124 );
125 println!("KSP default: {}", config.ksp.label());
126 println!("GMRES/FGMRES haptol: {:.3e}", config.fgmres_haptol);
127 println!(
128 "Complex ILU reordering: kind={:?}, symmetric={}",
129 config.ilu_reordering.kind, config.ilu_reordering.symmetric
130 );
131 if config.run_mode == RunMode::Correctness {
132 println!(
133 "Replicated check marker: {}",
134 if config.mark_replicated_check {
135 "enabled (metadata-only)"
136 } else {
137 "disabled"
138 }
139 );
140 println!(
141 "Overlap PC preset: {}",
142 if should_include_overlap_pcs(&config, size) {
143 "enabled (MPI correctness only; GlobalOK/XOK verdict columns shown)"
144 } else if config.include_overlap_pcs {
145 "requested but inactive (requires MPI correctness with >1 rank)"
146 } else {
147 "disabled"
148 }
149 );
150 }
151 if config.residual_history {
152 println!(
153 "Residual history: enabled{}",
154 config
155 .residual_history_file
156 .as_ref()
157 .map(|p| format!(", output={}", p.display()))
158 .unwrap_or_default()
159 );
160 }
161 println!("===============================================================");
162 println!();
163 }
164
165 let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/mtx");
166 let all_cases = [
167 MatrixCase {
168 file_name: "qc324.mtx",
169 description: "qc324 (complex, general)",
170 classification: "reference complex general case",
171 },
172 MatrixCase {
173 file_name: "dwg961a.mtx",
174 description: "dwg961a (complex, general)",
175 classification: "stress/singular-or-bad-diagonal case",
176 },
177 ];
178 let cases: Vec<MatrixCase> = match config.run_mode {
179 RunMode::Correctness => all_cases.to_vec(),
180 RunMode::Scalability => all_cases.to_vec(),
181 RunMode::Robustness => all_cases.to_vec(),
182 };
183
184 if matches!(config.run_mode, RunMode::Correctness | RunMode::Scalability) {
185 let (nx, ny) = config.poisson_grid;
186 let generated_cases = [
187 (
188 config.poisson_shift_real,
189 config.poisson_shift_imag,
190 "easy",
191 "generated complex shifted Poisson case",
192 ),
193 (
194 0.01,
195 0.25,
196 "hard",
197 "generated complex shifted Poisson hard-shift case",
198 ),
199 ];
200 for (alpha, beta, difficulty, classification) in generated_cases {
201 match build_shifted_poisson_case(
202 &comm,
203 nx,
204 ny,
205 alpha,
206 beta,
207 config.poisson_convection_x,
208 config.poisson_convection_y,
209 ) {
210 Ok(problem) => execute_problem(
211 &format!(
212 "generated shifted Poisson {difficulty} (-Δ + ({alpha:.3} + {beta:.3}i)I, convection=({:.3},{:.3}))",
213 config.poisson_convection_x, config.poisson_convection_y
214 ),
215 classification,
216 problem,
217 &config,
218 rank,
219 ),
220 Err(err) if rank == 0 => {
221 println!(
222 "❌ Failed to build generated shifted Poisson {difficulty} case: {err}\n"
223 );
224 }
225 Err(_) => {}
226 }
227 }
228 }
229
230 for case in cases {
231 let mat_name = case.file_name;
232 let descr = case.description;
233 let mat_path = base.join(mat_name);
234 let available = mat_path.exists();
235 if !available {
236 if rank == 0 {
237 println!("⚠ Missing file {mat_name} for {descr}, skipping.\n");
238 }
239 continue;
240 }
241
242 let problem = match load_problem_complex(
243 &mat_path,
244 &comm,
245 config.run_mode,
246 config.mark_replicated_check,
247 ) {
248 Ok(p) => p,
249 Err(err) => {
250 if rank == 0 {
251 println!("❌ Failed to load {descr}: {err}\n");
252 }
253 continue;
254 }
255 };
256
257 execute_problem(descr, case.classification, problem, &config, rank);
258 }
259
260 if rank == 0 {
261 println!("Example complete.");
262 println!(
263 "Final backend summary: {}.",
264 if is_parallel {
265 "MPI run (see per-case backend labels for replicated/distributed mode)"
266 } else {
267 "serial CSR"
268 }
269 );
270 }
271
272 Ok(())
273 }
274
275 fn execute_problem(
276 descr: &str,
277 classification: &str,
278 problem: Problem,
279 config: &BenchmarkConfig,
280 rank: usize,
281 ) {
282 let rhs_norm2_local: f64 = problem.rhs.iter().map(|v| v.abs2()).sum();
283 let rhs_norm = problem.comm.all_reduce_f64(rhs_norm2_local).sqrt();
284 let runs = RunSpec::build_default_matrix(config, &problem);
285 if rank == 0 {
286 println!(
287 "=== [{} mode] {descr} — {} ===",
288 config.run_mode.label(),
289 problem.backend_descr
290 );
291 println!("Classification: {classification}");
292 println!(
293 "Run backend: {} ({})",
294 problem.backend.run_label(),
295 problem.backend.details()
296 );
297 println!(
298 "Benchmark/export label: {}",
299 problem.backend.benchmark_export_label()
300 );
301 println!("Global DOFs: {}", problem.global_n);
302 println!("Local DOFs (rank {rank}): {}", problem.local_n);
303 println!(
304 "Local row range (rank {rank}): [{}..{})",
305 problem.global_row_start,
306 problem.global_row_start + problem.local_n
307 );
308 if problem.comm.size() > 1 && problem.local_n == problem.global_n {
309 println!("Note: replicated execution: MPI ranks are not sharing SpMV rows.");
310 }
311 if config.run_mode == RunMode::Correctness {
312 println!(
313 "MPI scalability note: use distributed mode for scaling claims; correctness mode emphasizes solver validation."
314 );
315 }
316 println!("‖b_unscaled‖₂ = {:.3e}", rhs_norm);
317 println!(
318 "{}",
319 format_dist_policy_report(config, runs.first(), &problem)
320 );
321 println!("Residual semantics: rec/reported = solver recurrence/monitor residual.");
322 if config.run_mode == RunMode::Correctness {
323 println!(
324 "Verdict threshold: rel residual and generated x_err checks pass when <= {:.3e} ({}× rtol).",
325 correctness_verdict_tolerance(config),
326 CORRECTNESS_VERDICT_RTOL_MULTIPLIER
327 );
328 println!(
329 " True(dist) = ||b_unscaled - problem.op x_unscaled||₂ on owned rows, then reduced across ranks."
330 );
331 println!(
332 " True(global) = ||b_global - problem.global_csr x_global||₂ after gathering owned vectors in rank order."
333 );
334 println!(
335 " *(rel) columns divide by the corresponding unscaled RHS norm (stable under --row-scale)."
336 );
337 }
338 println!(
339 "FGMRES side policy: requested left/symmetric are normalized to effective right preconditioning."
340 );
341 if problem.rhs_source == RhsSource::GeneratedAOnes {
342 println!(
343 "Generated A*ones reference: {}",
344 if problem.solution_reference.valid_for_x_error() {
345 "valid for x_err(rel) reporting"
346 } else {
347 "x_err(rel) suppressed; matrix has zero/missing rows or singularity indicators, so the solution may be nonunique"
348 }
349 );
350 if !problem.solution_reference.valid_for_x_error() {
351 println!(" → {}", problem.solution_reference.suppression_reason());
352 }
353 }
354 if config.run_mode == RunMode::Correctness && config.mark_replicated_check {
355 println!(
356 "Replicated check marker: ENABLED (metadata-only marker for cross-run comparison)."
357 );
358 }
359 let include_dof_col = matches!(
360 problem.backend,
361 CsrBackend::Serial | CsrBackend::Distributed
362 );
363 if config.run_mode == RunMode::Scalability {
364 println!(
365 "{:<7} {:<8} {:<6} {:<42} {:<36} {:<34} {:>9} {:>9} {:>7} {:>5} {:>5} {:>5} {:>17} {:>14} {:>12}",
366 "Op",
367 "Exec",
368 "PCdom",
369 "PC apply",
370 "Method",
371 "Effective policy",
372 "Med(s)",
373 "Min(s)",
374 "Iters",
375 "Rst",
376 "Inn",
377 "Pfb",
378 "Reds(obs/wait/model)",
379 "Rec/Reported",
380 "DOF/s"
381 );
382 println!("{}", "-".repeat(240));
383 } else if include_dof_col {
384 println!(
385 "{:<7} {:<8} {:<6} {:<42} {:<36} {:<34} {:<34} {:>9} {:>9} {:>9} {:>7} {:>5} {:>5} {:>5} {:>14} {:>14} {:>14} {:>14} {:>14} {:>14} {:>14} {:>8} {:>8} {:>8} {:>26} {:>12}",
386 "Op",
387 "Exec",
388 "PCdom",
389 "PC apply",
390 "Method",
391 "Requested policy",
392 "Effective policy",
393 "Setup(s)",
394 "Med(s)",
395 "Min(s)",
396 "Iters",
397 "Rst",
398 "Inn",
399 "Pfb",
400 "Rec/Reported",
401 "True(dist)",
402 "TrueDist(rel)",
403 "x_err(dist)",
404 "True(global)",
405 "TrueGlob(rel)",
406 "x_err(global)",
407 "DistOK",
408 "GlobalOK",
409 "XOK",
410 "Reason",
411 "DOF/s"
412 );
413 println!("{}", "-".repeat(408));
414 } else {
415 println!(
416 "{:<7} {:<8} {:<6} {:<42} {:<36} {:<34} {:<34} {:>9} {:>9} {:>9} {:>7} {:>5} {:>5} {:>5} {:>14} {:>14} {:>14} {:>14} {:>14} {:>14} {:>14} {:>8} {:>8} {:>8} {:>26}",
417 "Op",
418 "Exec",
419 "PCdom",
420 "PC apply",
421 "Method",
422 "Requested policy",
423 "Effective policy",
424 "Setup(s)",
425 "Med(s)",
426 "Min(s)",
427 "Iters",
428 "Rst",
429 "Inn",
430 "Pfb",
431 "Rec/Reported",
432 "True(dist)",
433 "TrueDist(rel)",
434 "x_err(dist)",
435 "True(global)",
436 "TrueGlob(rel)",
437 "x_err(global)",
438 "DistOK",
439 "GlobalOK",
440 "XOK",
441 "Reason"
442 );
443 println!("{}", "-".repeat(392));
444 }
445 println!(
446 "Legend: Op=operator storage (csr-cx=complex CSR), Exec=execution backend (ser=serial, mpi-row=MPI row partition, mpi-repl=MPI replicated operator), PCdom=PC domain (full=global/full serial matrix ILU, own0=MPI owned block, overlap=0 local ILU/ASM, n/a=no ILU domain), PC apply=how the preconditioner is applied."
447 );
448 }
449
450 for spec in runs {
451 match run_once(&problem, &spec, config) {
452 Ok(row) => {
453 if rank == 0 {
454 println!("{}", render_result_row(&row, config.run_mode, &problem));
455 }
456 }
457 Err(err) => {
458 if rank == 0 {
459 println!(
460 "{}",
461 render_failure_result_row(&spec, config.run_mode, &problem)
462 );
463 println!(" → {err}");
464 }
465 }
466 }
467 problem.comm.barrier();
468 }
469
470 if rank == 0 {
471 println!("{}", "=".repeat(96));
472 println!();
473 }
474 problem.comm.barrier();
475 }
476
477 #[derive(Clone, Copy, Debug)]
478 struct MatrixCase {
479 file_name: &'static str,
480 description: &'static str,
481 classification: &'static str,
482 }
483
484 struct Problem {
485 op: Arc<dyn KLinOp<Scalar = S>>,
486 dist_plan_diagnostics: DistributedPlanDiagnostics,
487 rhs: Vec<S>,
488 rhs_source: RhsSource,
489 solution_reference: SolutionReferenceDiagnostics,
490 csr_for_pc: Arc<SparseCsrMatrix<S>>,
491 global_csr: Arc<SparseCsrMatrix<S>>,
492 local_rows_nnz: usize,
493 zero_global_rows_local: usize,
494 local_n: usize,
495 global_n: usize,
496 global_row_start: usize,
497 comm: UniverseComm,
498 backend: CsrBackend,
499 backend_descr: String,
500 generated_case: bool,
501 }
502
503 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
504 enum RhsSource {
505 LoadedFromFile,
506 GeneratedAOnes,
507 }
508
509 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
510 struct SolutionReferenceDiagnostics {
511 global_rows: usize,
512 global_cols: usize,
513 zero_row_count: usize,
514 missing_diagonal_count: usize,
515 tiny_diagonal_count: usize,
516 }
517
518 impl SolutionReferenceDiagnostics {
519 fn valid_for_x_error(self) -> bool {
520 self.global_rows == self.global_cols
521 && self.zero_row_count == 0
522 && self.missing_diagonal_count == 0
523 && self.tiny_diagonal_count == 0
524 }
525
526 fn suppression_reason(self) -> String {
527 let mut reasons = Vec::new();
528 if self.global_rows != self.global_cols {
529 reasons.push(format!(
530 "matrix is rectangular ({}x{})",
531 self.global_rows, self.global_cols
532 ));
533 }
534 if self.zero_row_count > 0 {
535 reasons.push(format!("{} zero/missing rows", self.zero_row_count));
536 }
537 if self.missing_diagonal_count > 0 {
538 reasons.push(format!(
539 "{} structurally missing diagonals",
540 self.missing_diagonal_count
541 ));
542 }
543 if self.tiny_diagonal_count > 0 {
544 reasons.push(format!("{} tiny diagonals", self.tiny_diagonal_count));
545 }
546 if reasons.is_empty() {
547 "generated A*ones reference is structurally valid".to_string()
548 } else {
549 format!(
550 "N/A (generated A*ones but matrix has {}; solution may be nonunique)",
551 reasons.join(", ")
552 )
553 }
554 }
555 }
556
557 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
558 enum CsrBackend {
559 Serial,
560 Replicated,
561 Distributed,
562 }
563
564 impl CsrBackend {
565 fn run_label(self) -> &'static str {
566 match self {
567 Self::Serial => "serial CSR",
568 Self::Replicated => "replicated CSR",
569 Self::Distributed => "distributed CSR",
570 }
571 }
572
573 fn details(self) -> &'static str {
574 match self {
575 Self::Serial => "single rank operator",
576 Self::Replicated => "all ranks hold full matrix, identical solve",
577 Self::Distributed => "row-partitioned operator",
578 }
579 }
580
581 fn benchmark_export_label(self) -> &'static str {
582 match self {
583 Self::Serial => "serial",
584 Self::Replicated => "replicated",
585 Self::Distributed => "distributed",
586 }
587 }
588 }
589
590 const CORRECTNESS_VERDICT_RTOL_MULTIPLIER: f64 = 10.0;
591
592 fn correctness_verdict_tolerance(bench_cfg: &BenchmarkConfig) -> f64 {
593 bench_cfg.rtol * CORRECTNESS_VERDICT_RTOL_MULTIPLIER
594 }
595
596 struct ResultRow {
597 operator_storage: &'static str,
598 execution_backend: &'static str,
599 pc_domain: &'static str,
600 pc_apply: &'static str,
601 method: String,
602 requested_policy: String,
603 effective_policy: String,
604 setup_secs: f64,
605 median_solve_secs: f64,
606 min_solve_secs: f64,
607 iterations: usize,
608 reductions: usize,
609 overlapped_reduction_waits: usize,
610 model_predicted_reductions: Option<usize>,
611 restart_count: Option<usize>,
612 inner_iterations_last_cycle: Option<usize>,
613 pipeline_fallbacks: Option<usize>,
614 reported_residual: R,
615 explicit_true_residual: Option<R>,
616 explicit_true_residual_rel: Option<R>,
617 x_error_rel: Option<R>,
618 global_true_residual: Option<R>,
619 global_true_residual_rel: Option<R>,
620 global_x_error_rel: Option<R>,
621 dist_ok: Option<bool>,
622 global_ok: Option<bool>,
623 x_ok: Option<bool>,
624 reason: ConvergedReason,
625 dof_per_sec: Option<f64>,
626 }
627
628 struct RunSpec {
629 restart: usize,
630 ksp: KspKind,
631 variant: FgmresVariant,
632 residual_check_policy: ResidualCheckPolicy,
633 orthog: OrthogMethod,
634 reorth: ReorthPolicy,
635 pc_side: PcSide,
636 pc: PcKind,
637 }
638
639 #[derive(Clone, Debug)]
640 struct CsrForPcDiagnostics {
641 nnz_local_block: usize,
642 nnz_local_rows: usize,
643 nnz_ratio: f64,
644 zero_local_rows: usize,
645 zero_global_rows: usize,
646 structural_diag_missing: usize,
647 diag_min_abs: f64,
648 diag_max_abs: f64,
649 diag_tiny_or_missing_count: usize,
650 }
651
652 #[derive(Clone, Debug)]
653 struct RankSpread {
654 global_sum: f64,
655 rank_min: f64,
656 rank_max: f64,
657 rank0_local: f64,
658 }
659
660 #[derive(Clone, Debug)]
661 struct CsrForPcDiagnosticsGlobal {
662 nnz_local_block: RankSpread,
663 nnz_local_rows: RankSpread,
664 nnz_ratio: RankSpread,
665 zero_local_rows_rank_count: f64,
666 zero_global_rows: RankSpread,
667 structural_diag_missing: RankSpread,
668 diag_min_abs: RankSpread,
669 diag_max_abs: RankSpread,
670 diag_tiny_or_missing_count: RankSpread,
671 pivot_perturbation_count: Option<RankSpread>,
672 }
673
674 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
675 enum PcKind {
676 None,
677 JacobiWeak,
678 Ilu0Local,
679 IlutLocal,
680 MpiBlockJacobiIlu0Local,
681 AsmIlu0Overlap { overlap: usize },
682 RasIlu0Overlap { overlap: usize },
683 RasIlukOverlap { k: usize, overlap: usize },
684 LocalIluk { k: usize },
685 ReplicatedFullIlu0,
686 ReplicatedFullIluk { k: usize },
687 }
688
689 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
690 enum PcDispatchBranch {
691 None,
692 JacobiWeak,
693 Ilu0Local,
694 ReplicatedFullIlu,
695 IlutLocal,
696 OverlapIlu,
697 }
698
699 #[derive(Clone, Debug)]
700 struct BenchmarkConfig {
701 run_mode: RunMode,
702 warmup_runs: usize,
703 measured_runs: usize,
704 rtol: f64,
705 atol: f64,
706 maxits: usize,
707 restarts: Vec<usize>,
708 ksp: KspKind,
709 pcs: Vec<PcKind>,
710 include_restart_200: bool,
711 include_ilut_real_projection_fallback: bool,
712 include_overlap_pcs: bool,
713 variants: Vec<FgmresVariant>,
714 orthogs: Vec<OrthogMethod>,
715 reorths: Vec<ReorthPolicy>,
716 dist_policy: DistPolicyMode,
717 allow_stagnation_fallback: bool,
718 min_inner_before_fallback: usize,
719 mark_replicated_check: bool,
720 residual_history: bool,
721 residual_history_file: Option<PathBuf>,
722 residual_history_force: bool,
723 fgmres_haptol: f64,
724 row_scale: bool,
725 row_scale_tiny: f64,
726 ilu_reordering: ReorderingOptions,
727 poisson_grid: (usize, usize),
728 poisson_shift_real: f64,
729 poisson_shift_imag: f64,
730 poisson_convection_x: f64,
731 poisson_convection_y: f64,
732 }
733
734 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
735 enum RunMode {
736 Correctness,
737 Scalability,
738 Robustness,
739 }
740
741 impl RunMode {
742 fn default_dist_policy(self) -> DistPolicyMode {
743 match self {
744 Self::Correctness => DistPolicyMode::Off,
745 Self::Scalability => DistPolicyMode::Auto,
746 Self::Robustness => DistPolicyMode::Off,
748 }
749 }
750
751 fn label(self) -> &'static str {
752 match self {
753 Self::Correctness => "correctness",
754 Self::Scalability => "scalability",
755 Self::Robustness => "robustness",
756 }
757 }
758
759 fn parse(token: &str) -> Result<Self, KError> {
760 match token.trim().to_ascii_lowercase().as_str() {
761 "correctness" => Ok(Self::Correctness),
762 "scalability" => Ok(Self::Scalability),
763 "robustness" => Ok(Self::Robustness),
764 other => Err(KError::InvalidInput(format!(
765 "invalid run mode '{other}', expected correctness|scalability|robustness"
766 ))),
767 }
768 }
769 }
770
771 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
772 enum DistPolicyMode {
773 Off,
774 Auto,
775 }
776
777 impl DistPolicyMode {
778 fn label(self) -> &'static str {
779 match self {
780 Self::Off => "off",
781 Self::Auto => "auto",
782 }
783 }
784
785 fn parse(token: &str) -> Result<Self, KError> {
786 match token.trim().to_ascii_lowercase().as_str() {
787 "off" => Ok(Self::Off),
788 "auto" => Ok(Self::Auto),
789 other => Err(KError::InvalidInput(format!(
790 "invalid DistCSR policy '{other}', expected off|auto"
791 ))),
792 }
793 }
794 }
795
796 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
797 enum KspKind {
798 Gmres,
799 Fgmres,
800 }
801
802 impl KspKind {
803 fn label(self) -> &'static str {
804 match self {
805 Self::Gmres => "GMRES",
806 Self::Fgmres => "FGMRES",
807 }
808 }
809
810 fn parse(token: &str) -> Result<Self, KError> {
811 match token.trim().to_ascii_lowercase().as_str() {
812 "gmres" => Ok(Self::Gmres),
813 "fgmres" => Ok(Self::Fgmres),
814 other => Err(KError::InvalidInput(format!(
815 "invalid KSP '{other}', expected gmres|fgmres"
816 ))),
817 }
818 }
819
820 fn for_pc(self, pc: PcKind) -> Self {
821 if pc.requires_flexible_right_preconditioning() {
822 Self::Fgmres
823 } else {
824 self
825 }
826 }
827 }
828
829 impl Default for BenchmarkConfig {
830 fn default() -> Self {
831 Self {
832 run_mode: RunMode::Correctness,
833 warmup_runs: 1,
834 measured_runs: 5,
835 rtol: 1e-8,
836 atol: 1e-12,
837 maxits: 500,
838 restarts: vec![50, 100, 150],
839 ksp: KspKind::Gmres,
840 pcs: Vec::new(),
841 include_restart_200: false,
842 include_ilut_real_projection_fallback: false,
843 include_overlap_pcs: false,
844 variants: vec![FgmresVariant::Classical],
845 orthogs: vec![OrthogMethod::ClassicalGS],
846 reorths: vec![ReorthPolicy::IfNeeded],
847 dist_policy: RunMode::Correctness.default_dist_policy(),
848 allow_stagnation_fallback: false,
849 min_inner_before_fallback: 8,
850 mark_replicated_check: false,
851 residual_history: false,
852 residual_history_file: None,
853 residual_history_force: false,
854 fgmres_haptol: 1e-30,
855 row_scale: false,
856 row_scale_tiny: 1e-15,
857 ilu_reordering: ReorderingOptions::default(),
858 poisson_grid: (16, 16),
859 poisson_shift_real: 1.0,
860 poisson_shift_imag: 0.25,
861 poisson_convection_x: 0.0,
862 poisson_convection_y: 0.0,
863 }
864 }
865 }
866
867 impl BenchmarkConfig {
868 fn from_env_args() -> Result<Self, KError> {
869 Self::from_args(env::args().skip(1))
870 }
871
872 fn from_args<I>(args: I) -> Result<Self, KError>
873 where
874 I: IntoIterator<Item = String>,
875 {
876 let mut cfg = Self::default();
877 let mut dist_policy_explicit = false;
878 let mut args = args.into_iter().peekable();
879 while let Some(arg) = args.next() {
880 match arg.as_str() {
881 "--warmup-runs" => {
882 let Some(v) = args.next() else {
883 return Err(KError::InvalidInput(
884 "missing value for --warmup-runs".into(),
885 ));
886 };
887 cfg.warmup_runs = parse_positive_usize("--warmup-runs", &v)?;
888 }
889 "--measured-runs" => {
890 let Some(v) = args.next() else {
891 return Err(KError::InvalidInput(
892 "missing value for --measured-runs".into(),
893 ));
894 };
895 cfg.measured_runs = parse_positive_usize("--measured-runs", &v)?;
896 }
897 "--help" | "-h" => {
898 if cfg!(feature = "mpi") {
899 println!(
900 "Usage: cargo mpirun -n <ranks> --example complex_matrix_market_demo --features complex,mpi,mpi_examples -- [--mode correctness|scalability|robustness] [--ksp gmres|fgmres] [--dist-policy off|auto] [--mark-replicated-check] [--warmup-runs N] [--measured-runs N] [--rtol F] [--atol F] [--maxits N] [--restarts csv] [--pcs csv] [--include-restart-200] [--include-ilut-real-projection-fallback] [--include-overlap-pcs] [--allow-stagnation-fallback] [--min-inner-before-fallback N] [--fgmres-variant csv] [--fgmres-orthog csv] [--fgmres-reorth csv] [--fgmres-haptol F] [--residual-history] [--residual-history-file <path>] [--residual-history-force] [--row-scale [tiny]] [--ilu-reordering none|rcm|amd[:nonsym]] [--poisson-grid NXxNY] [--poisson-shift-real F] [--poisson-shift-imag F] [--poisson-convection CX,CY]
901Defaults: --ksp gmres for fixed linear PCs; flexible/nonlinear PC modes select FGMRES. --dist-policy is off for correctness and robustness, auto for scalability.
902--include-overlap-pcs is active only for MPI correctness sweeps; it keeps block-jacobi-ilu0-overlap0 and appends asm-ilu0-overlap1, ras-ilu0-overlap1, ras-iluk1-overlap1 so GlobalOK/XOK verdicts are visible before performance comparisons."
903 );
904 } else {
905 println!(
906 "Usage: cargo run --example complex_matrix_market_demo --features complex -- [--mode correctness|scalability|robustness] [--ksp gmres|fgmres] [--dist-policy off|auto] [--mark-replicated-check] [--warmup-runs N] [--measured-runs N] [--rtol F] [--atol F] [--maxits N] [--restarts csv] [--pcs csv] [--include-restart-200] [--include-ilut-real-projection-fallback] [--include-overlap-pcs] [--allow-stagnation-fallback] [--min-inner-before-fallback N] [--fgmres-variant csv] [--fgmres-orthog csv] [--fgmres-reorth csv] [--fgmres-haptol F] [--residual-history] [--residual-history-file <path>] [--residual-history-force] [--row-scale [tiny]] [--ilu-reordering none|rcm|amd[:nonsym]] [--poisson-grid NXxNY] [--poisson-shift-real F] [--poisson-shift-imag F] [--poisson-convection CX,CY]
907Defaults: --ksp gmres for fixed linear PCs; flexible/nonlinear PC modes select FGMRES. --dist-policy is off for correctness and robustness, auto for scalability.
908--include-overlap-pcs is active only for MPI correctness sweeps; it keeps block-jacobi-ilu0-overlap0 and appends asm-ilu0-overlap1, ras-ilu0-overlap1, ras-iluk1-overlap1 so GlobalOK/XOK verdicts are visible before performance comparisons."
909 );
910 }
911 std::process::exit(0);
912 }
913 "--mode" => {
914 let Some(v) = args.next() else {
915 return Err(KError::InvalidInput("missing value for --mode".into()));
916 };
917 cfg.run_mode = RunMode::parse(&v)?;
918 }
919 "--ksp" => {
920 let Some(v) = args.next() else {
921 return Err(KError::InvalidInput("missing value for --ksp".into()));
922 };
923 cfg.ksp = KspKind::parse(&v)?;
924 }
925 "--dist-policy" => {
926 let Some(v) = args.next() else {
927 return Err(KError::InvalidInput(
928 "missing value for --dist-policy".into(),
929 ));
930 };
931 cfg.dist_policy = DistPolicyMode::parse(&v)?;
932 dist_policy_explicit = true;
933 }
934 "--poisson-grid" => {
935 let Some(v) = args.next() else {
936 return Err(KError::InvalidInput(
937 "missing value for --poisson-grid".into(),
938 ));
939 };
940 cfg.poisson_grid = parse_grid_dims("--poisson-grid", &v)?;
941 }
942 "--poisson-shift-real" => {
943 let Some(v) = args.next() else {
944 return Err(KError::InvalidInput(
945 "missing value for --poisson-shift-real".into(),
946 ));
947 };
948 cfg.poisson_shift_real = parse_finite_f64("--poisson-shift-real", &v)?;
949 }
950 "--poisson-shift-imag" => {
951 let Some(v) = args.next() else {
952 return Err(KError::InvalidInput(
953 "missing value for --poisson-shift-imag".into(),
954 ));
955 };
956 cfg.poisson_shift_imag = parse_finite_f64("--poisson-shift-imag", &v)?;
957 }
958 "--poisson-convection" => {
959 let Some(v) = args.next() else {
960 return Err(KError::InvalidInput(
961 "missing value for --poisson-convection".into(),
962 ));
963 };
964 (cfg.poisson_convection_x, cfg.poisson_convection_y) =
965 parse_f64_pair("--poisson-convection", &v)?;
966 }
967 "--mark-replicated-check" => {
968 cfg.mark_replicated_check = true;
969 }
970 "--correctness-replicated-check" => {
971 return Err(KError::InvalidInput(
972 "--correctness-replicated-check is metadata-only and has been renamed to --mark-replicated-check".into(),
973 ));
974 }
975 "--restarts" => {
976 let Some(v) = args.next() else {
977 return Err(KError::InvalidInput(
978 "missing value for --restarts".into(),
979 ));
980 };
981 cfg.restarts = parse_usize_csv("--restarts", &v)?;
982 }
983 "--rtol" => {
984 let Some(v) = args.next() else {
985 return Err(KError::InvalidInput("missing value for --rtol".into()));
986 };
987 cfg.rtol = parse_positive_f64("--rtol", &v)?;
988 }
989 "--atol" => {
990 let Some(v) = args.next() else {
991 return Err(KError::InvalidInput("missing value for --atol".into()));
992 };
993 cfg.atol = parse_positive_f64("--atol", &v)?;
994 }
995 "--maxits" => {
996 let Some(v) = args.next() else {
997 return Err(KError::InvalidInput("missing value for --maxits".into()));
998 };
999 cfg.maxits = parse_positive_usize("--maxits", &v)?;
1000 }
1001 "--pcs" => {
1002 let Some(v) = args.next() else {
1003 return Err(KError::InvalidInput("missing value for --pcs".into()));
1004 };
1005 cfg.pcs = parse_pc_csv("--pcs", &v)?;
1006 }
1007 "--include-restart-200" => {
1008 cfg.include_restart_200 = true;
1009 }
1010 "--include-ilut-real-projection-fallback" => {
1011 cfg.include_ilut_real_projection_fallback = true;
1012 }
1013 "--include-overlap-pcs" => {
1014 cfg.include_overlap_pcs = true;
1015 }
1016 "--ilu-reordering" | "--pc-ilu-reordering-type" => {
1017 let Some(v) = args.next() else {
1018 return Err(KError::InvalidInput(
1019 "missing value for --ilu-reordering".into(),
1020 ));
1021 };
1022 cfg.ilu_reordering = parse_ilu_reordering(&v)?;
1023 }
1024 "--allow-stagnation-fallback" => {
1025 cfg.allow_stagnation_fallback = true;
1026 }
1027 "--min-inner-before-fallback" => {
1028 let Some(v) = args.next() else {
1029 return Err(KError::InvalidInput(
1030 "missing value for --min-inner-before-fallback".into(),
1031 ));
1032 };
1033 cfg.min_inner_before_fallback =
1034 parse_positive_usize("--min-inner-before-fallback", &v)?;
1035 }
1036 "--fgmres-variant" => {
1037 let Some(v) = args.next() else {
1038 return Err(KError::InvalidInput(
1039 "missing value for --fgmres-variant".into(),
1040 ));
1041 };
1042 cfg.variants = parse_variant_csv("--fgmres-variant", &v)?;
1043 }
1044 "--fgmres-orthog" => {
1045 let Some(v) = args.next() else {
1046 return Err(KError::InvalidInput(
1047 "missing value for --fgmres-orthog".into(),
1048 ));
1049 };
1050 cfg.orthogs = parse_orthog_csv("--fgmres-orthog", &v)?;
1051 }
1052 "--residual-history" => {
1053 cfg.residual_history = true;
1054 }
1055 "--residual-history-file" => {
1056 let Some(v) = args.next() else {
1057 return Err(KError::InvalidInput(
1058 "missing value for --residual-history-file".into(),
1059 ));
1060 };
1061 cfg.residual_history_file = Some(PathBuf::from(v));
1062 }
1063 "--residual-history-force" => {
1064 cfg.residual_history_force = true;
1065 }
1066 "--row-scale" => {
1067 cfg.row_scale = true;
1068 if let Some(next) = args.peek() {
1069 if !next.starts_with("--") {
1070 let tiny = args.next().ok_or_else(|| {
1071 KError::InvalidInput(
1072 "failed to read --row-scale optional tiny".into(),
1073 )
1074 })?;
1075 cfg.row_scale_tiny = parse_positive_f64("--row-scale", &tiny)?;
1076 }
1077 }
1078 }
1079 "--fgmres-reorth" => {
1080 let Some(v) = args.next() else {
1081 return Err(KError::InvalidInput(
1082 "missing value for --fgmres-reorth".into(),
1083 ));
1084 };
1085 cfg.reorths = parse_reorth_csv("--fgmres-reorth", &v)?;
1086 }
1087 "--fgmres-haptol" => {
1088 let Some(v) = args.next() else {
1089 return Err(KError::InvalidInput(
1090 "missing value for --fgmres-haptol".into(),
1091 ));
1092 };
1093 cfg.fgmres_haptol = parse_positive_finite_f64("--fgmres-haptol", &v)?;
1094 }
1095 _ => {
1096 return Err(KError::InvalidInput(format!("unknown argument: {arg}")));
1097 }
1098 }
1099 }
1100 if cfg.measured_runs == 0 {
1101 return Err(KError::InvalidInput(
1102 "--measured-runs must be at least 1".into(),
1103 ));
1104 }
1105 if !dist_policy_explicit {
1106 cfg.dist_policy = cfg.run_mode.default_dist_policy();
1107 }
1108 if cfg.include_restart_200 && !cfg.restarts.contains(&200) {
1109 cfg.restarts.push(200);
1110 }
1111 if cfg.run_mode == RunMode::Scalability {
1112 cfg.mark_replicated_check = false;
1113 }
1114 if cfg.pcs.iter().any(|pc| pc.is_ilut()) {
1115 eprintln!(
1116 "⚠ --pcs includes ILUT for a complex run: current ILUT path is a degraded real projection and is not trusted for complex robustness benchmarking."
1117 );
1118 }
1119 if cfg.include_ilut_real_projection_fallback && !cfg.pcs.iter().any(|pc| pc.is_ilut()) {
1120 eprintln!(
1121 "⚠ Including {} in the default complex benchmark set: current ILUT path is a degraded real projection and is not trusted for complex robustness benchmarking.",
1122 ILUT_REAL_PROJECTION_FALLBACK_LABEL
1123 );
1124 }
1125 if cfg.residual_history
1126 && cfg.run_mode != RunMode::Correctness
1127 && !cfg.residual_history_force
1128 {
1129 cfg.residual_history = false;
1130 }
1131 Ok(cfg)
1132 }
1133 }
1134
1135 fn parse_grid_dims(flag: &str, value: &str) -> Result<(usize, usize), KError> {
1136 let Some((nx, ny)) = value.split_once(['x', 'X']) else {
1137 return Err(KError::InvalidInput(format!(
1138 "invalid value '{value}' for {flag}, expected NXxNY"
1139 )));
1140 };
1141 let nx = parse_positive_usize(flag, nx)?;
1142 let ny = parse_positive_usize(flag, ny)?;
1143 if nx == 0 || ny == 0 {
1144 return Err(KError::InvalidInput(format!(
1145 "invalid value '{value}' for {flag}, dimensions must be positive"
1146 )));
1147 }
1148 Ok((nx, ny))
1149 }
1150
1151 fn parse_positive_usize(flag: &str, value: &str) -> Result<usize, KError> {
1152 value.parse::<usize>().map_err(|_| {
1153 KError::InvalidInput(format!(
1154 "invalid value '{value}' for {flag}, expected non-negative integer"
1155 ))
1156 })
1157 }
1158
1159 fn parse_positive_f64(flag: &str, value: &str) -> Result<f64, KError> {
1160 let val = value.parse::<f64>().map_err(|_| {
1161 KError::InvalidInput(format!(
1162 "invalid value '{value}' for {flag}, expected non-negative float"
1163 ))
1164 })?;
1165 if val < 0.0 {
1166 return Err(KError::InvalidInput(format!(
1167 "invalid value '{value}' for {flag}, expected non-negative float"
1168 )));
1169 }
1170 Ok(val)
1171 }
1172
1173 fn parse_finite_f64(flag: &str, value: &str) -> Result<f64, KError> {
1174 let val = value.parse::<f64>().map_err(|_| {
1175 KError::InvalidInput(format!(
1176 "invalid value '{value}' for {flag}, expected finite float"
1177 ))
1178 })?;
1179 if !val.is_finite() {
1180 return Err(KError::InvalidInput(format!(
1181 "invalid value '{value}' for {flag}, expected finite float"
1182 )));
1183 }
1184 Ok(val)
1185 }
1186
1187 fn parse_f64_pair(flag: &str, value: &str) -> Result<(f64, f64), KError> {
1188 let Some((x, y)) = value.split_once(',') else {
1189 return Err(KError::InvalidInput(format!(
1190 "invalid value '{value}' for {flag}, expected X,Y"
1191 )));
1192 };
1193 let x = parse_finite_f64(flag, x.trim())?;
1194 let y = parse_finite_f64(flag, y.trim())?;
1195 Ok((x, y))
1196 }
1197
1198 fn parse_positive_finite_f64(flag: &str, value: &str) -> Result<f64, KError> {
1199 let val = value.parse::<f64>().map_err(|_| {
1200 KError::InvalidInput(format!(
1201 "invalid value '{value}' for {flag}, expected positive finite float"
1202 ))
1203 })?;
1204 if !val.is_finite() || val <= 0.0 {
1205 return Err(KError::InvalidInput(format!(
1206 "invalid value '{value}' for {flag}, expected positive finite float"
1207 )));
1208 }
1209 Ok(val)
1210 }
1211
1212 fn parse_usize_csv(flag: &str, value: &str) -> Result<Vec<usize>, KError> {
1213 let vals = value
1214 .split(',')
1215 .map(str::trim)
1216 .filter(|s| !s.is_empty())
1217 .map(|v| parse_positive_usize(flag, v))
1218 .collect::<Result<Vec<_>, _>>()?;
1219 if vals.is_empty() {
1220 return Err(KError::InvalidInput(format!(
1221 "{flag} expects at least one integer value"
1222 )));
1223 }
1224 Ok(vals)
1225 }
1226
1227 fn parse_variant(token: &str) -> Result<FgmresVariant, KError> {
1228 match token.trim().to_ascii_lowercase().as_str() {
1229 "classical" => Ok(FgmresVariant::Classical),
1230 "pipelined" => Ok(FgmresVariant::Pipelined),
1231 other => Err(KError::InvalidInput(format!(
1232 "invalid fgmres variant '{other}', expected classical|pipelined"
1233 ))),
1234 }
1235 }
1236
1237 fn parse_orthog(token: &str) -> Result<OrthogMethod, KError> {
1238 match token.trim().to_ascii_lowercase().as_str() {
1239 "classical" | "cgs" | "cgs_refined" | "cgs-refined" | "refined" => {
1240 Ok(OrthogMethod::ClassicalGS)
1241 }
1242 "mgs" | "modified" => Ok(OrthogMethod::ModifiedGS),
1243 other => Err(KError::InvalidInput(format!(
1244 "invalid orthog '{other}', expected classical|cgs|cgs_refined|mgs|modified"
1245 ))),
1246 }
1247 }
1248
1249 fn parse_reorth(token: &str) -> Result<ReorthPolicy, KError> {
1250 match token.trim().to_ascii_lowercase().as_str() {
1251 "never" => Ok(ReorthPolicy::Never),
1252 "ifneeded" | "if-needed" => Ok(ReorthPolicy::IfNeeded),
1253 "always" => Ok(ReorthPolicy::Always),
1254 other => Err(KError::InvalidInput(format!(
1255 "invalid reorth '{other}', expected never|ifneeded|always"
1256 ))),
1257 }
1258 }
1259
1260 fn parse_variant_csv(flag: &str, value: &str) -> Result<Vec<FgmresVariant>, KError> {
1261 parse_csv(flag, value, parse_variant)
1262 }
1263
1264 fn parse_orthog_csv(flag: &str, value: &str) -> Result<Vec<OrthogMethod>, KError> {
1265 parse_csv(flag, value, parse_orthog)
1266 }
1267
1268 fn parse_reorth_csv(flag: &str, value: &str) -> Result<Vec<ReorthPolicy>, KError> {
1269 parse_csv(flag, value, parse_reorth)
1270 }
1271
1272 fn parse_ilu_reordering(value: &str) -> Result<ReorderingOptions, KError> {
1273 let normalized = value.trim().to_ascii_lowercase();
1274 let (base, nonsym) = if let Some(base) = normalized.strip_suffix(":nonsym") {
1275 (base, true)
1276 } else if let Some(base) = normalized.strip_suffix("_nonsym") {
1277 (base, true)
1278 } else {
1279 (normalized.as_str(), false)
1280 };
1281 let kind = match base {
1282 "none" | "natural" => ReorderingKind::None,
1283 "rcm" => ReorderingKind::Rcm,
1284 "amd" => ReorderingKind::Amd,
1285 other => {
1286 return Err(KError::InvalidInput(format!(
1287 "invalid ILU reordering '{other}', expected none|rcm|amd with optional :nonsym suffix"
1288 )));
1289 }
1290 };
1291 Ok(ReorderingOptions {
1292 kind,
1293 symmetric: !nonsym,
1294 deterministic: true,
1295 })
1296 }
1297
1298 fn parse_pc(token: &str) -> Result<PcKind, KError> {
1299 let token_norm = token.trim().to_ascii_lowercase();
1300 if let Some(k_str) = token_norm.strip_prefix("local-iluk:") {
1301 return Ok(PcKind::LocalIluk {
1302 k: parse_positive_usize("local-iluk:k", k_str)?,
1303 });
1304 }
1305 if let Some(k_str) = token_norm.strip_prefix("block-jacobi-iluk:") {
1306 return Ok(PcKind::LocalIluk {
1307 k: parse_positive_usize("block-jacobi-iluk:k", k_str)?,
1308 });
1309 }
1310 if let Some(k_str) = token_norm.strip_prefix("block-jacobi-iluk") {
1311 let k_str = k_str.strip_suffix("-overlap0").unwrap_or(k_str);
1312 return Ok(PcKind::LocalIluk {
1313 k: parse_positive_usize("block-jacobi-iluk:k", k_str)?,
1314 });
1315 }
1316 if let Some(k_str) = token_norm.strip_prefix("replicated-iluk:") {
1317 return Ok(PcKind::ReplicatedFullIluk {
1318 k: parse_positive_usize("replicated-iluk:k", k_str)?,
1319 });
1320 }
1321 match token_norm.as_str() {
1322 "asm-ilu0-overlap1" => Ok(PcKind::AsmIlu0Overlap { overlap: 1 }),
1323 "asm-ilu0-overlap2" => Ok(PcKind::AsmIlu0Overlap { overlap: 2 }),
1324 "ras-ilu0-overlap1" => Ok(PcKind::RasIlu0Overlap { overlap: 1 }),
1325 "ras-iluk1-overlap1" => Ok(PcKind::RasIlukOverlap { k: 1, overlap: 1 }),
1326 "none" | "off" => Ok(PcKind::None),
1327 "jacobi" | "jacobi-weak" | "weak-jacobi" => Ok(PcKind::JacobiWeak),
1328 "ilu0" | "ilu0-local" | "local-ilu0" => Ok(PcKind::Ilu0Local),
1329 "ilut" | "ilut-local" | "local-ilut" => Ok(PcKind::IlutLocal),
1330 "replicated-ilu0" | "replicated-full-ilu0" => Ok(PcKind::ReplicatedFullIlu0),
1331 "mpi-block-jacobi-ilu0"
1332 | "block-jacobi-ilu0"
1333 | "block-jacobi-ilu0-overlap0"
1334 | "mpi-block-ilu0" => Ok(PcKind::MpiBlockJacobiIlu0Local),
1335 other => Err(KError::InvalidInput(format!(
1336 "invalid pc '{other}', expected none|jacobi|local-ilu0|local-ilut|local-iluk:<k>|block-jacobi-ilu0-overlap0|block-jacobi-iluk1|block-jacobi-iluk1-overlap0|block-jacobi-iluk:<k>|block-jacobi-iluk<k>-overlap0|replicated-ilu0|replicated-full-ilu0|replicated-iluk:<k>|mpi-block-jacobi-ilu0|asm-ilu0-overlap1|asm-ilu0-overlap2|ras-ilu0-overlap1|ras-iluk1-overlap1"
1337 ))),
1338 }
1339 }
1340
1341 fn parse_pc_csv(flag: &str, value: &str) -> Result<Vec<PcKind>, KError> {
1342 parse_csv(flag, value, parse_pc)
1343 }
1344
1345 const MPI_CORRECTNESS_OVERLAP_PCS: [PcKind; 3] = [
1346 PcKind::AsmIlu0Overlap { overlap: 1 },
1347 PcKind::RasIlu0Overlap { overlap: 1 },
1348 PcKind::RasIlukOverlap { k: 1, overlap: 1 },
1349 ];
1350
1351 fn should_include_overlap_pcs(cfg: &BenchmarkConfig, comm_size: usize) -> bool {
1352 cfg.include_overlap_pcs && cfg.run_mode == RunMode::Correctness && comm_size > 1
1353 }
1354
1355 fn push_pc_unique(pcs: &mut Vec<PcKind>, pc: PcKind) {
1356 if !pcs.contains(&pc) {
1357 pcs.push(pc);
1358 }
1359 }
1360
1361 fn append_mpi_correctness_overlap_pcs(pcs: &mut Vec<PcKind>) {
1362 push_pc_unique(pcs, PcKind::MpiBlockJacobiIlu0Local);
1363 for pc in MPI_CORRECTNESS_OVERLAP_PCS {
1364 push_pc_unique(pcs, pc);
1365 }
1366 }
1367
1368 fn parse_csv<T, F>(flag: &str, value: &str, mut parser: F) -> Result<Vec<T>, KError>
1369 where
1370 F: FnMut(&str) -> Result<T, KError>,
1371 {
1372 let vals = value
1373 .split(',')
1374 .map(str::trim)
1375 .filter(|s| !s.is_empty())
1376 .map(&mut parser)
1377 .collect::<Result<Vec<_>, _>>()?;
1378 if vals.is_empty() {
1379 return Err(KError::InvalidInput(format!(
1380 "{flag} expects at least one value"
1381 )));
1382 }
1383 Ok(vals)
1384 }
1385
1386 impl RunSpec {
1387 fn build_default_matrix(cfg: &BenchmarkConfig, problem: &Problem) -> Vec<Self> {
1388 let mut pcs = if cfg.pcs.is_empty() && problem.generated_case {
1389 vec![
1390 PcKind::None,
1391 PcKind::JacobiWeak,
1392 if problem.comm.size() > 1 {
1393 PcKind::MpiBlockJacobiIlu0Local
1394 } else {
1395 PcKind::Ilu0Local
1396 },
1397 PcKind::LocalIluk { k: 1 },
1398 ]
1399 } else if cfg.pcs.is_empty() {
1400 match cfg.run_mode {
1401 RunMode::Correctness => {
1402 if problem.comm.size() > 1 {
1403 vec![
1404 PcKind::ReplicatedFullIlu0,
1405 PcKind::None,
1406 PcKind::JacobiWeak,
1407 PcKind::MpiBlockJacobiIlu0Local,
1408 ]
1409 } else {
1410 vec![PcKind::Ilu0Local, PcKind::JacobiWeak, PcKind::None]
1411 }
1412 }
1413 RunMode::Scalability => {
1414 if problem.comm.size() > 1 {
1415 vec![PcKind::MpiBlockJacobiIlu0Local]
1416 } else {
1417 vec![PcKind::Ilu0Local]
1418 }
1419 }
1420 RunMode::Robustness => {
1421 if problem.comm.size() > 1 {
1422 vec![PcKind::MpiBlockJacobiIlu0Local, PcKind::JacobiWeak]
1423 } else {
1424 vec![PcKind::Ilu0Local, PcKind::JacobiWeak]
1425 }
1426 }
1427 }
1428 } else {
1429 cfg.pcs.clone()
1430 };
1431 if cfg.pcs.is_empty() && cfg.include_ilut_real_projection_fallback {
1432 pcs.push(PcKind::IlutLocal);
1433 }
1434 if should_include_overlap_pcs(cfg, problem.comm.size()) {
1435 append_mpi_correctness_overlap_pcs(&mut pcs);
1436 }
1437 let mut runs = Vec::new();
1438 for &restart in &cfg.restarts {
1439 for &variant in &cfg.variants {
1440 for &orthog in &cfg.orthogs {
1441 for &reorth in &cfg.reorths {
1442 for &pc in &pcs {
1443 if pc == PcKind::MpiBlockJacobiIlu0Local && problem.comm.size() <= 1
1444 {
1445 continue;
1446 }
1447 runs.push(Self {
1448 restart,
1449 ksp: cfg.ksp.for_pc(pc),
1450 variant,
1451 residual_check_policy: match cfg.run_mode {
1452 RunMode::Correctness => ResidualCheckPolicy::OnConvergence,
1453 RunMode::Scalability => ResidualCheckPolicy::RestartOnly,
1454 RunMode::Robustness => ResidualCheckPolicy::RestartOnly,
1455 },
1456 orthog,
1457 reorth,
1458 pc_side: PcSide::Right,
1459 pc,
1460 });
1461 }
1462 }
1463 }
1464 }
1465 }
1466 runs
1467 }
1468
1469 fn method_label(&self) -> String {
1470 let effective_side = normalized_fgmres_side(self.pc_side);
1471 let side_desc = if effective_side == self.pc_side {
1472 format!("{}", pc_side_label(self.pc_side))
1473 } else {
1474 format!(
1475 "{}→{} (normalized)",
1476 pc_side_label(self.pc_side),
1477 pc_side_label(effective_side)
1478 )
1479 };
1480 format!(
1481 "{}+{} [m={}, v={}, reschk={}, orth={}, reorth={}, pc=requested {}, effective {}]",
1482 self.ksp.label(),
1483 self.pc.label(),
1484 self.restart,
1485 variant_label(self.variant),
1486 residual_check_policy_label(self.residual_check_policy),
1487 orthog_label(self.orthog),
1488 reorth_label(self.reorth),
1489 pc_side_label(self.pc_side),
1490 side_desc,
1491 )
1492 }
1493
1494 fn requested_policy_label(&self) -> String {
1495 format!(
1496 "ksp={}, variant={}, restart={}, residual-check={}",
1497 self.ksp.label(),
1498 variant_label(self.variant),
1499 self.restart,
1500 residual_check_policy_label(self.residual_check_policy)
1501 )
1502 }
1503 }
1504
1505 fn operator_storage_label(_problem: &Problem) -> &'static str {
1506 "csr-cx"
1507 }
1508
1509 fn execution_backend_label(problem: &Problem) -> &'static str {
1510 match problem.backend {
1511 CsrBackend::Serial => "ser",
1512 CsrBackend::Replicated => "mpi-repl",
1513 CsrBackend::Distributed => "mpi-row",
1514 }
1515 }
1516
1517 fn pc_domain_label(pc: PcKind, problem: &Problem) -> &'static str {
1518 pc_domain_label_for_size(pc, problem.comm.size())
1519 }
1520
1521 fn pc_domain_label_for_size(pc: PcKind, comm_size: usize) -> &'static str {
1522 match pc {
1523 PcKind::ReplicatedFullIlu0 | PcKind::ReplicatedFullIluk { .. } => "full",
1524 PcKind::Ilu0Local | PcKind::IlutLocal | PcKind::LocalIluk { .. } => {
1525 if comm_size <= 1 {
1526 "full"
1527 } else {
1528 "own0"
1529 }
1530 }
1531 PcKind::MpiBlockJacobiIlu0Local => "own0",
1532 PcKind::AsmIlu0Overlap { overlap }
1533 | PcKind::RasIlu0Overlap { overlap }
1534 | PcKind::RasIlukOverlap { overlap, .. } => match overlap {
1535 1 => "own+ghost1",
1536 2 => "own+ghost2",
1537 _ => "own+ghostN",
1538 },
1539 PcKind::None | PcKind::JacobiWeak => "n/a",
1540 }
1541 }
1542
1543 fn pc_apply_label(pc: PcKind, problem: &Problem) -> &'static str {
1544 pc_apply_label_for_size(pc, problem.comm.size())
1545 }
1546
1547 fn pc_apply_label_for_size(pc: PcKind, comm_size: usize) -> &'static str {
1548 match pc {
1549 PcKind::ReplicatedFullIlu0 | PcKind::ReplicatedFullIluk { .. } => {
1550 if comm_size > 1 {
1551 "replicated/full/allgather, not scalable"
1552 } else {
1553 "replicated/full"
1554 }
1555 }
1556 PcKind::Ilu0Local
1557 | PcKind::IlutLocal
1558 | PcKind::MpiBlockJacobiIlu0Local
1559 | PcKind::LocalIluk { .. } => "owned-block/overlap0",
1560 PcKind::AsmIlu0Overlap { .. } => "asm/overlap-gather/owned-output",
1561 PcKind::RasIlu0Overlap { .. } | PcKind::RasIlukOverlap { .. } => {
1562 "ras/overlap-gather/restricted-owned"
1563 }
1564 PcKind::JacobiWeak => "jacobi/local",
1565 PcKind::None => "none",
1566 }
1567 }
1568
1569 const ILUT_REAL_PROJECTION_FALLBACK_LABEL: &str = "ILUT(real-projection fallback)";
1570
1571 impl PcKind {
1572 fn label(&self) -> String {
1573 match self {
1574 Self::None => "none (unpreconditioned reference)".to_string(),
1575 Self::JacobiWeak => "jacobi (weak baseline)".to_string(),
1576 Self::Ilu0Local => "block-jacobi-ilu0-overlap0".to_string(),
1577 Self::IlutLocal => {
1578 "local ILUT(real-projection fallback) [degraded/provisional complex path: real-projection fallback; not trusted for complex robustness benchmarking]".to_string()
1579 }
1580 Self::MpiBlockJacobiIlu0Local => {
1581 "block-jacobi-ilu0-overlap0 [mpi spelling alias]".to_string()
1582 }
1583 Self::AsmIlu0Overlap { overlap } => format!("asm-ilu0-overlap{overlap}"),
1584 Self::RasIlu0Overlap { overlap } => format!("ras-ilu0-overlap{overlap}"),
1585 Self::RasIlukOverlap { k, overlap } => format!("ras-iluk{k}-overlap{overlap}"),
1586 Self::LocalIluk { k } => format!("block-jacobi-iluk{k}-overlap0"),
1587 Self::ReplicatedFullIlu0 => {
1588 "replicated full ILU(0) [correctness only, not scalable]".to_string()
1589 }
1590 Self::ReplicatedFullIluk { .. } => {
1591 "replicated full ILU(k) [correctness only, not scalable]".to_string()
1592 }
1593 }
1594 }
1595
1596 fn dispatch_branch(self) -> PcDispatchBranch {
1597 match self {
1598 Self::None => PcDispatchBranch::None,
1599 Self::JacobiWeak => PcDispatchBranch::JacobiWeak,
1600 Self::Ilu0Local | Self::MpiBlockJacobiIlu0Local | Self::LocalIluk { .. } => {
1601 PcDispatchBranch::Ilu0Local
1602 }
1603 Self::ReplicatedFullIlu0 | Self::ReplicatedFullIluk { .. } => {
1604 PcDispatchBranch::ReplicatedFullIlu
1605 }
1606 Self::AsmIlu0Overlap { .. }
1607 | Self::RasIlu0Overlap { .. }
1608 | Self::RasIlukOverlap { .. } => PcDispatchBranch::OverlapIlu,
1609 Self::IlutLocal => PcDispatchBranch::IlutLocal,
1610 }
1611 }
1612
1613 fn explicit_alias_of(self) -> Option<Self> {
1614 match self {
1615 Self::MpiBlockJacobiIlu0Local => Some(Self::Ilu0Local),
1616 Self::None
1617 | Self::JacobiWeak
1618 | Self::Ilu0Local
1619 | Self::IlutLocal
1620 | Self::LocalIluk { .. }
1621 | Self::AsmIlu0Overlap { .. }
1622 | Self::RasIlu0Overlap { .. }
1623 | Self::RasIlukOverlap { .. }
1624 | Self::ReplicatedFullIlu0
1625 | Self::ReplicatedFullIluk { .. } => None,
1626 }
1627 }
1628
1629 fn semantic_experiment_key(self, mpi_mode: bool) -> String {
1630 match self {
1631 Self::MpiBlockJacobiIlu0Local | Self::Ilu0Local if mpi_mode => {
1632 "block-jacobi-ilu0-overlap0".to_string()
1633 }
1634 Self::MpiBlockJacobiIlu0Local => "mpi-block-jacobi-ilu0-local".to_string(),
1635 Self::Ilu0Local => "local-ilu0".to_string(),
1636 Self::None => "none".to_string(),
1637 Self::JacobiWeak => "jacobi-weak".to_string(),
1638 Self::IlutLocal => "local-ilut-real-projection-fallback".to_string(),
1639 Self::LocalIluk { k } => format!("local-iluk:{k}"),
1640 Self::AsmIlu0Overlap { overlap } => format!("asm-ilu0-overlap{overlap}"),
1641 Self::RasIlu0Overlap { overlap } => format!("ras-ilu0-overlap{overlap}"),
1642 Self::RasIlukOverlap { k, overlap } => format!("ras-iluk{k}-overlap{overlap}"),
1643 Self::ReplicatedFullIlu0 => "replicated-ilu0".to_string(),
1644 Self::ReplicatedFullIluk { k } => format!("replicated-iluk:{k}"),
1645 }
1646 }
1647 fn is_ilut(self) -> bool {
1648 matches!(self, Self::IlutLocal)
1649 }
1650
1651 fn requires_flexible_right_preconditioning(self) -> bool {
1652 false
1655 }
1656 }
1657
1658 fn variant_label(variant: FgmresVariant) -> &'static str {
1659 match variant {
1660 FgmresVariant::Classical => "classical",
1661 FgmresVariant::Pipelined => "pipelined",
1662 }
1663 }
1664
1665 fn plain_gmres_variant_label(variant: PlainGmresVariant) -> &'static str {
1666 match variant {
1667 PlainGmresVariant::Classical => "classical",
1668 PlainGmresVariant::Pipelined => "pipelined",
1669 PlainGmresVariant::SStep { .. } => "s-step",
1670 }
1671 }
1672
1673 fn dist_policy_reason(diag: &DistributedPlanDiagnostics, comm_size: usize) -> &'static str {
1674 let halo_volume = (diag.halo_recv_volume + diag.halo_send_volume) as f64;
1675 let overlap_enabled =
1676 diag.overlap_mode == kryst::matrix::dist_csr::HaloOverlapMode::Interior;
1677 let communication_heavy = comm_size > 1
1678 && (overlap_enabled
1679 || diag.expected_communication_fraction >= 0.55
1680 || halo_volume >= 4096.0);
1681
1682 if communication_heavy {
1683 "high halo/communication pressure detected"
1684 } else {
1685 "low halo pressure and compute-dominant local work"
1686 }
1687 }
1688
1689 fn format_dist_policy_report(
1690 cfg: &BenchmarkConfig,
1691 sample_spec: Option<&RunSpec>,
1692 problem: &Problem,
1693 ) -> String {
1694 match cfg.dist_policy {
1695 DistPolicyMode::Off => format!("DistCSR policy: {}", cfg.dist_policy.label()),
1696 DistPolicyMode::Auto => match sample_spec.map(|spec| spec.ksp).unwrap_or(cfg.ksp) {
1697 KspKind::Gmres => {
1698 let solver = sample_spec
1699 .map(|spec| configured_gmres_solver(spec, cfg))
1700 .unwrap_or_else(|| GmresSolver::new(1, cfg.rtol, cfg.maxits));
1701 format!(
1702 "DistCSR policy: auto, KSP=GMRES, selected restart={}, variant={}, reason={}",
1703 solver.restart,
1704 plain_gmres_variant_label(solver.variant),
1705 dist_policy_reason(&problem.dist_plan_diagnostics, problem.comm.size())
1706 )
1707 }
1708 KspKind::Fgmres => {
1709 let mut solver = sample_spec
1710 .map(|spec| configured_fgmres_solver(spec, cfg))
1711 .unwrap_or_else(|| FgmresSolver::new(cfg.rtol, cfg.maxits, 1));
1712 apply_fgmres_dist_plan_policy(&mut solver, problem);
1713 format!(
1714 "DistCSR policy: auto, KSP=FGMRES, selected restart={}, variant={}, reason={}",
1715 solver.restart,
1716 variant_label(solver.variant),
1717 dist_policy_reason(&problem.dist_plan_diagnostics, problem.comm.size())
1718 )
1719 }
1720 },
1721 }
1722 }
1723
1724 fn orthog_label(orthog: OrthogMethod) -> &'static str {
1725 match orthog {
1726 OrthogMethod::ClassicalGS => "classical-gs",
1727 OrthogMethod::ModifiedGS => "modified-gs",
1728 }
1729 }
1730
1731 fn reorth_label(reorth: ReorthPolicy) -> &'static str {
1732 match reorth {
1733 ReorthPolicy::Never => "never",
1734 ReorthPolicy::IfNeeded => "if-needed",
1735 ReorthPolicy::Always => "always",
1736 }
1737 }
1738
1739 fn residual_check_policy_label(policy: ResidualCheckPolicy) -> &'static str {
1740 match policy {
1741 ResidualCheckPolicy::RestartOnly => "restart-only",
1742 ResidualCheckPolicy::OnConvergence => "on-convergence",
1743 ResidualCheckPolicy::EveryIteration => "every-iteration",
1744 ResidualCheckPolicy::Debug => "debug",
1745 }
1746 }
1747
1748 fn pc_side_label(pc_side: PcSide) -> &'static str {
1749 match pc_side {
1750 PcSide::Right => "right",
1751 PcSide::Left => "left",
1752 PcSide::Symmetric => "symmetric",
1753 }
1754 }
1755
1756 fn normalized_fgmres_side(requested_side: PcSide) -> PcSide {
1757 match requested_side {
1758 PcSide::Right => PcSide::Right,
1759 PcSide::Left | PcSide::Symmetric => PcSide::Right,
1760 }
1761 }
1762
1763 #[derive(Clone, Debug)]
1764 struct ResidualHistoryEntry {
1765 iter: usize,
1766 recurrence_residual: R,
1767 true_residual: Option<R>,
1768 checkpoint: bool,
1769 }
1770
1771 #[derive(Default)]
1772 struct RunResidualHistory {
1773 entries: Vec<ResidualHistoryEntry>,
1774 }
1775
1776 #[derive(Clone, Debug)]
1777 struct GlobalReferenceCheck {
1778 true_residual: R,
1779 true_residual_rel: R,
1780 x_error_rel: Option<R>,
1781 }
1782
1783 struct RowScaledOp {
1784 base: Arc<dyn KLinOp<Scalar = S>>,
1785 d: Vec<R>,
1786 }
1787
1788 impl KLinOp for RowScaledOp {
1789 type Scalar = S;
1790
1791 fn dims(&self) -> (usize, usize) {
1792 self.base.dims()
1793 }
1794
1795 fn matvec_s(&self, x: &[S], y: &mut [S], scratch: &mut BridgeScratch) {
1796 self.base.matvec_s(x, y, scratch);
1797 for (yi, di) in y.iter_mut().zip(self.d.iter().copied()) {
1798 *yi *= S::from_real(di);
1799 }
1800 }
1801
1802 fn supports_t_matvec_s(&self) -> bool {
1803 self.base.supports_t_matvec_s()
1804 }
1805
1806 fn t_matvec_s(&self, x: &[S], y: &mut [S], scratch: &mut BridgeScratch) {
1807 self.base.t_matvec_s(x, y, scratch);
1808 }
1809 }
1810
1811 fn compute_row_scaling(matrix: &SparseCsrMatrix<S>, tiny: f64) -> Vec<R> {
1812 let row_ptr = matrix.row_ptr();
1813 let vals = matrix.values();
1814 (0..matrix.nrows())
1815 .map(|r| {
1816 let mut row_inf = 0.0f64;
1817 for nz in row_ptr[r]..row_ptr[r + 1] {
1818 row_inf = row_inf.max(vals[nz].abs());
1819 }
1820 if row_inf > tiny { 1.0 / row_inf } else { 1.0 }
1821 })
1822 .collect()
1823 }
1824
1825 fn scale_csr_rows(matrix: &SparseCsrMatrix<S>, row_scale: &[R]) -> SparseCsrMatrix<S> {
1826 debug_assert_eq!(matrix.nrows(), row_scale.len());
1827 let row_ptr = matrix.row_ptr().to_vec();
1828 let col_idx = matrix.col_idx().to_vec();
1829 let mut values = matrix.values().to_vec();
1830 for r in 0..matrix.nrows() {
1831 let d = S::from_real(row_scale[r]);
1832 for nz in row_ptr[r]..row_ptr[r + 1] {
1833 values[nz] *= d;
1834 }
1835 }
1836 SparseCsrMatrix::from_csr(matrix.nrows(), matrix.ncols(), row_ptr, col_idx, values)
1837 }
1838
1839 fn validate_replicated_full_apply_layout(
1840 x_local: &[S],
1841 y_local: &[S],
1842 global_n: usize,
1843 global_row_start: usize,
1844 local_n: usize,
1845 ) -> Result<(), KError> {
1846 if x_local.len() != local_n {
1847 return Err(KError::InvalidInput(format!(
1848 "replicated ILU apply expected x_local length {local_n}, got {}",
1849 x_local.len()
1850 )));
1851 }
1852 if y_local.len() != local_n {
1853 return Err(KError::InvalidInput(format!(
1854 "replicated ILU apply expected y_local length {local_n}, got {}",
1855 y_local.len()
1856 )));
1857 }
1858 if global_row_start > global_n || local_n > global_n - global_row_start {
1859 return Err(KError::InvalidInput(format!(
1860 "replicated ILU apply invalid owned segment: global_n={global_n}, \
1861 global_row_start={global_row_start}, local_n={local_n}"
1862 )));
1863 }
1864 Ok(())
1865 }
1866
1867 fn allgather_owned_segment_in_rank_order(
1868 comm: &UniverseComm,
1869 x_local: &[S],
1870 global_n: usize,
1871 global_row_start: usize,
1872 local_n: usize,
1873 x_global: &mut [S],
1874 ) -> Result<(), KError> {
1875 validate_replicated_full_apply_layout(
1876 x_local,
1877 &vec![S::zero(); local_n],
1878 global_n,
1879 global_row_start,
1880 local_n,
1881 )?;
1882 if x_global.len() != global_n {
1883 return Err(KError::InvalidInput(format!(
1884 "replicated ILU apply expected x_global scratch length {global_n}, got {}",
1885 x_global.len()
1886 )));
1887 }
1888
1889 x_global.fill(S::zero());
1890
1891 #[cfg(feature = "mpi")]
1892 {
1893 use mpi::traits::*;
1894
1895 if let UniverseComm::Mpi(comm_impl) = comm {
1896 let rank = comm_impl.rank;
1897 let size = comm_impl.size;
1898 let local_len = i32::try_from(local_n).map_err(|_| {
1899 KError::InvalidInput(format!(
1900 "replicated ILU apply local_n={local_n} exceeds MPI i32 length limit"
1901 ))
1902 })?;
1903 let mut lengths = vec![0i32; size];
1904 comm_impl
1905 .world
1906 .all_gather_into(&local_len, &mut lengths[..]);
1907
1908 let mut offset = 0usize;
1909 for (r, &len_i32) in lengths.iter().enumerate() {
1910 if len_i32 < 0 {
1911 return Err(KError::InvalidInput(format!(
1912 "replicated ILU apply received negative segment length from rank {r}"
1913 )));
1914 }
1915 let len = len_i32 as usize;
1916 if r == rank {
1917 if len != x_local.len() || len != local_n {
1918 return Err(KError::InvalidInput(format!(
1919 "replicated ILU apply local length mismatch: gathered={len}, \
1920 x_local={}, local_n={local_n}",
1921 x_local.len()
1922 )));
1923 }
1924 if offset != global_row_start {
1925 return Err(KError::InvalidInput(format!(
1926 "replicated ILU apply offset mismatch: gathered prefix={offset}, \
1927 global_row_start={global_row_start}"
1928 )));
1929 }
1930 }
1931 offset = offset.checked_add(len).ok_or_else(|| {
1932 KError::InvalidInput(
1933 "replicated ILU apply segment lengths overflowed usize".into(),
1934 )
1935 })?;
1936 }
1937 if offset != global_n {
1938 return Err(KError::InvalidInput(format!(
1939 "replicated ILU apply expected global_n={global_n}, gathered owned lengths sum={offset}"
1940 )));
1941 }
1942
1943 let max_len = lengths.iter().copied().max().unwrap_or(0) as usize;
1944 let mut send = vec![0.0f64; 2 * max_len];
1945 for (i, value) in x_local.iter().copied().enumerate() {
1946 send[2 * i] = value.real();
1947 send[2 * i + 1] = value.imag();
1948 }
1949 let mut gathered = vec![0.0f64; 2 * max_len * size];
1950 if max_len > 0 {
1951 comm_impl
1952 .world
1953 .all_gather_into(&send[..], &mut gathered[..]);
1954 }
1955
1956 let mut offset = 0usize;
1957 for (r, &len_i32) in lengths.iter().enumerate() {
1958 let len = len_i32 as usize;
1959 let start = 2 * r * max_len;
1960 for i in 0..len {
1961 x_global[offset + i] =
1962 S::from_parts(gathered[start + 2 * i], gathered[start + 2 * i + 1]);
1963 }
1964 offset += len;
1965 }
1966 return Ok(());
1967 }
1968 }
1969
1970 if comm.size() != 1 || global_row_start != 0 || local_n != global_n {
1971 return Err(KError::InvalidInput(
1972 "replicated ILU apply requires MPI allgather for distributed owned segments".into(),
1973 ));
1974 }
1975 x_global.copy_from_slice(x_local);
1976 Ok(())
1977 }
1978
1979 fn gather_global_reference_vector(
1980 comm: &UniverseComm,
1981 local: &[S],
1982 global_n: usize,
1983 global_row_start: usize,
1984 local_n: usize,
1985 global: &mut [S],
1986 ) -> Result<(), KError> {
1987 if local.len() != local_n {
1988 return Err(KError::InvalidInput(format!(
1989 "global reference gather expected local length {local_n}, got {}",
1990 local.len()
1991 )));
1992 }
1993 if global.len() != global_n {
1994 return Err(KError::InvalidInput(format!(
1995 "global reference gather expected global scratch length {global_n}, got {}",
1996 global.len()
1997 )));
1998 }
1999 if global_row_start > global_n || local_n > global_n - global_row_start {
2000 return Err(KError::InvalidInput(format!(
2001 "global reference gather invalid owned segment: global_n={global_n}, \
2002 global_row_start={global_row_start}, local_n={local_n}"
2003 )));
2004 }
2005
2006 global.fill(S::zero());
2007
2008 if comm.size() == 1 {
2009 if global_row_start != 0 || local_n != global_n {
2010 return Err(KError::InvalidInput(format!(
2011 "single-rank global reference gather expected ownership [0..{global_n}), got [{global_row_start}..{})",
2012 global_row_start + local_n
2013 )));
2014 }
2015 global.copy_from_slice(local);
2016 return Ok(());
2017 }
2018
2019 #[cfg(feature = "mpi")]
2020 {
2021 use mpi::traits::*;
2022
2023 if let UniverseComm::Mpi(comm_impl) = comm {
2024 let rank = comm_impl.rank;
2025 let size = comm_impl.size;
2026 let local_len = i32::try_from(local_n).map_err(|_| {
2027 KError::InvalidInput(format!(
2028 "global reference gather local_n={local_n} exceeds MPI i32 length limit"
2029 ))
2030 })?;
2031 let mut lengths = vec![0i32; size];
2032 comm_impl
2033 .world
2034 .all_gather_into(&local_len, &mut lengths[..]);
2035
2036 let expected_part = DistCsrOp::partition_rows_balanced(global_n, comm);
2037 let mut offset = 0usize;
2038 for (r, &len_i32) in lengths.iter().enumerate() {
2039 if len_i32 < 0 {
2040 return Err(KError::InvalidInput(format!(
2041 "global reference gather received negative segment length from rank {r}"
2042 )));
2043 }
2044 let len = len_i32 as usize;
2045 let expected_len = expected_part[r + 1] - expected_part[r];
2046 if len != expected_len {
2047 return Err(KError::InvalidInput(format!(
2048 "global reference gather rank {r} length mismatch: gathered={len}, expected balanced={expected_len}"
2049 )));
2050 }
2051 if r == rank {
2052 if len != local.len() || len != local_n {
2053 return Err(KError::InvalidInput(format!(
2054 "global reference gather local length mismatch: gathered={len}, \
2055 local={}, local_n={local_n}",
2056 local.len()
2057 )));
2058 }
2059 if offset != global_row_start || expected_part[r] != global_row_start {
2060 return Err(KError::InvalidInput(format!(
2061 "global reference gather offset mismatch: gathered prefix={offset}, \
2062 expected balanced={}, global_row_start={global_row_start}",
2063 expected_part[r]
2064 )));
2065 }
2066 }
2067 offset = offset.checked_add(len).ok_or_else(|| {
2068 KError::InvalidInput(
2069 "global reference gather segment lengths overflowed usize".into(),
2070 )
2071 })?;
2072 }
2073 if offset != global_n {
2074 return Err(KError::InvalidInput(format!(
2075 "global reference gather expected global_n={global_n}, gathered owned lengths sum={offset}"
2076 )));
2077 }
2078
2079 let max_len = lengths.iter().copied().max().unwrap_or(0) as usize;
2080 let mut send = vec![0.0f64; 2 * max_len];
2081 for (i, value) in local.iter().copied().enumerate() {
2082 send[2 * i] = value.real();
2083 send[2 * i + 1] = value.imag();
2084 }
2085 let mut gathered = vec![0.0f64; 2 * max_len * size];
2086 if max_len > 0 {
2087 comm_impl
2088 .world
2089 .all_gather_into(&send[..], &mut gathered[..]);
2090 }
2091
2092 let mut offset = 0usize;
2093 for (r, &len_i32) in lengths.iter().enumerate() {
2094 let len = len_i32 as usize;
2095 let start = 2 * r * max_len;
2096 for i in 0..len {
2097 global[offset + i] =
2098 S::from_parts(gathered[start + 2 * i], gathered[start + 2 * i + 1]);
2099 }
2100 offset += len;
2101 }
2102 return Ok(());
2103 }
2104 }
2105
2106 Err(KError::InvalidInput(
2107 "global reference gather requires MPI for multi-rank owned segments".into(),
2108 ))
2109 }
2110
2111 fn global_reference_residual(
2112 problem: &Problem,
2113 x_local: &[S],
2114 b_local: &[S],
2115 ) -> Result<GlobalReferenceCheck, KError> {
2116 if problem.global_csr.ncols() != problem.global_n {
2117 return Err(KError::InvalidInput(format!(
2118 "global reference residual requires square row-owned solution layout, got matrix {}x{} with global_n={}",
2119 problem.global_csr.nrows(),
2120 problem.global_csr.ncols(),
2121 problem.global_n
2122 )));
2123 }
2124
2125 let mut x_global = vec![S::zero(); problem.global_n];
2126 let mut b_global = vec![S::zero(); problem.global_n];
2127 gather_global_reference_vector(
2128 &problem.comm,
2129 x_local,
2130 problem.global_n,
2131 problem.global_row_start,
2132 problem.local_n,
2133 &mut x_global,
2134 )?;
2135 gather_global_reference_vector(
2136 &problem.comm,
2137 b_local,
2138 problem.global_n,
2139 problem.global_row_start,
2140 problem.local_n,
2141 &mut b_global,
2142 )?;
2143
2144 let mut ax_global = vec![S::zero(); problem.global_csr.nrows()];
2145 problem.global_csr.spmv(&x_global, &mut ax_global);
2146 let true_global = b_global
2147 .iter()
2148 .zip(ax_global.iter())
2149 .map(|(bi, ai)| (*bi - *ai).abs2())
2150 .sum::<f64>()
2151 .sqrt();
2152 let b_norm = b_global.iter().map(|bi| bi.abs2()).sum::<f64>().sqrt();
2153 let true_global_rel = true_global / b_norm.max(f64::MIN_POSITIVE);
2154 let x_error_rel = if problem.rhs_source == RhsSource::GeneratedAOnes
2155 && problem.solution_reference.valid_for_x_error()
2156 {
2157 let err = x_global
2158 .iter()
2159 .map(|xi| (*xi - S::one()).abs2())
2160 .sum::<f64>()
2161 .sqrt();
2162 let one_norm = x_global.iter().map(|_| S::one().abs2()).sum::<f64>().sqrt();
2163 Some(err / one_norm.max(f64::MIN_POSITIVE))
2164 } else {
2165 None
2166 };
2167
2168 Ok(GlobalReferenceCheck {
2169 true_residual: true_global,
2170 true_residual_rel: true_global_rel,
2171 x_error_rel,
2172 })
2173 }
2174
2175 fn apply_replicated_full_ilu_owned_segment(
2176 ilu: &IluCsr,
2177 comm: &UniverseComm,
2178 side: PcSide,
2179 x_local: &[S],
2180 y_local: &mut [S],
2181 scratch: &mut BridgeScratch,
2182 global_n: usize,
2183 global_row_start: usize,
2184 local_n: usize,
2185 scratch_in: &std::sync::Mutex<Vec<S>>,
2186 scratch_out: &std::sync::Mutex<Vec<S>>,
2187 ) -> Result<(), KError> {
2188 validate_replicated_full_apply_layout(
2189 x_local,
2190 y_local,
2191 global_n,
2192 global_row_start,
2193 local_n,
2194 )?;
2195
2196 let mut in_full = scratch_in.lock().map_err(|_| {
2197 KError::InvalidInput("failed to lock replicated ILU input scratch".into())
2198 })?;
2199 let mut out_full = scratch_out.lock().map_err(|_| {
2200 KError::InvalidInput("failed to lock replicated ILU output scratch".into())
2201 })?;
2202 if out_full.len() != global_n {
2203 return Err(KError::InvalidInput(format!(
2204 "replicated ILU apply expected output scratch length {global_n}, got {}",
2205 out_full.len()
2206 )));
2207 }
2208
2209 allgather_owned_segment_in_rank_order(
2210 comm,
2211 x_local,
2212 global_n,
2213 global_row_start,
2214 local_n,
2215 &mut in_full,
2216 )?;
2217 out_full.fill(S::zero());
2218 ilu.apply_s(side, &in_full, &mut out_full, scratch)?;
2219 y_local.copy_from_slice(&out_full[global_row_start..global_row_start + local_n]);
2220 Ok(())
2221 }
2222
2223 enum PcHandle {
2224 Jacobi(Jacobi),
2225 Ilu0(IluCsr),
2226 MpiBlockJacobiIlu0(IluCsr),
2227 OverlapIlu(OverlapIluPc),
2228 ReplicatedFull {
2229 ilu: IluCsr,
2230 comm: UniverseComm,
2231 global_n: usize,
2232 global_row_start: usize,
2233 local_n: usize,
2234 scratch_in: std::sync::Mutex<Vec<S>>,
2235 scratch_out: std::sync::Mutex<Vec<S>>,
2236 },
2237 }
2238
2239 impl PcHandle {
2240 fn as_kpc_mut(&mut self) -> &mut dyn KPreconditioner<Scalar = S> {
2241 match self {
2242 Self::Jacobi(pc) => pc,
2243 Self::Ilu0(pc) => pc,
2244 Self::MpiBlockJacobiIlu0(pc) => pc,
2245 Self::OverlapIlu(pc) => pc,
2246 Self::ReplicatedFull { .. } => self,
2247 }
2248 }
2249 }
2250 impl KPreconditioner for PcHandle {
2251 type Scalar = S;
2252 fn dims(&self) -> (usize, usize) {
2253 match self {
2254 Self::Jacobi(pc) => KPreconditioner::dims(pc),
2255 Self::Ilu0(pc) => KPreconditioner::dims(pc),
2256 Self::MpiBlockJacobiIlu0(pc) => KPreconditioner::dims(pc),
2257 Self::OverlapIlu(pc) => KPreconditioner::dims(pc),
2258 Self::ReplicatedFull { global_n, .. } => (*global_n, *global_n),
2259 }
2260 }
2261 fn apply_s(
2262 &self,
2263 side: PcSide,
2264 x: &[S],
2265 y: &mut [S],
2266 scratch: &mut BridgeScratch,
2267 ) -> Result<(), KError> {
2268 match self {
2269 Self::Jacobi(pc) => pc.apply_s(side, x, y, scratch),
2270 Self::Ilu0(pc) => pc.apply_s(side, x, y, scratch),
2271 Self::MpiBlockJacobiIlu0(pc) => pc.apply_s(side, x, y, scratch),
2272 Self::OverlapIlu(pc) => pc.apply_s(side, x, y, scratch),
2273 Self::ReplicatedFull {
2274 ilu,
2275 comm,
2276 global_n,
2277 global_row_start,
2278 local_n,
2279 scratch_in,
2280 scratch_out,
2281 } => apply_replicated_full_ilu_owned_segment(
2282 ilu,
2283 comm,
2284 side,
2285 x,
2286 y,
2287 scratch,
2288 *global_n,
2289 *global_row_start,
2290 *local_n,
2291 scratch_in,
2292 scratch_out,
2293 ),
2294 }
2295 }
2296 }
2297
2298 fn setup_preconditioner_for_run_once(
2299 problem: &Problem,
2300 spec: &RunSpec,
2301 bench_cfg: &BenchmarkConfig,
2302 pc_csr: &Arc<SparseCsrMatrix<S>>,
2303 global_pc_csr: &Arc<SparseCsrMatrix<S>>,
2304 csr_pc_diag: &CsrForPcDiagnostics,
2305 ) -> Result<Option<PcHandle>, KError> {
2306 let mut pc: Option<PcHandle> = None;
2307 match spec.pc.dispatch_branch() {
2308 PcDispatchBranch::None => {}
2309 PcDispatchBranch::JacobiWeak => {
2310 let mut jacobi = Jacobi::new();
2311 jacobi.setup(pc_csr.as_ref())?;
2312 pc = Some(PcHandle::Jacobi(jacobi));
2313 }
2314 PcDispatchBranch::Ilu0Local => {
2315 validate_local_ilu_owned_block(pc_csr.as_ref())?;
2316 let mut cfg = IluCsrConfig::default();
2317 cfg.kind = match spec.pc {
2318 PcKind::Ilu0Local | PcKind::MpiBlockJacobiIlu0Local => IluKind::Ilu0,
2319 PcKind::LocalIluk { k } => IluKind::Iluk { k },
2320 PcKind::AsmIlu0Overlap { .. }
2321 | PcKind::RasIlu0Overlap { .. }
2322 | PcKind::RasIlukOverlap { .. }
2323 | PcKind::IlutLocal
2324 | PcKind::ReplicatedFullIlu0
2325 | PcKind::ReplicatedFullIluk { .. }
2326 | PcKind::None
2327 | PcKind::JacobiWeak => unreachable!(),
2328 };
2329 cfg.reordering = bench_cfg.ilu_reordering.clone();
2330 let mut ilu = IluCsr::new_with_config(cfg);
2331 ilu.setup(pc_csr.as_ref())?;
2332 if bench_cfg.run_mode == RunMode::Correctness {
2333 let pivot_perturbation_count = None::<usize>;
2334 let diag_global = reduce_csr_for_pc_diagnostics(
2335 &problem.comm,
2336 &csr_pc_diag,
2337 pivot_perturbation_count,
2338 );
2339 if problem.comm.rank() == 0 {
2340 print_csr_for_pc_diagnostics(&spec.method_label(), &diag_global);
2341 }
2342 }
2343 pc = Some(match spec.pc {
2344 PcKind::Ilu0Local | PcKind::LocalIluk { .. } => PcHandle::Ilu0(ilu),
2345 PcKind::MpiBlockJacobiIlu0Local => PcHandle::MpiBlockJacobiIlu0(ilu),
2346 PcKind::AsmIlu0Overlap { .. }
2347 | PcKind::RasIlu0Overlap { .. }
2348 | PcKind::RasIlukOverlap { .. }
2349 | PcKind::IlutLocal
2350 | PcKind::ReplicatedFullIlu0
2351 | PcKind::ReplicatedFullIluk { .. }
2352 | PcKind::None
2353 | PcKind::JacobiWeak => unreachable!(),
2354 });
2355 }
2356 PcDispatchBranch::ReplicatedFullIlu => {
2357 let mut cfg = IluCsrConfig::default();
2358 cfg.kind = match spec.pc {
2359 PcKind::ReplicatedFullIlu0 => IluKind::Ilu0,
2360 PcKind::ReplicatedFullIluk { k } => IluKind::Iluk { k },
2361 _ => unreachable!(),
2362 };
2363 cfg.reordering = bench_cfg.ilu_reordering.clone();
2364 let mut ilu = IluCsr::new_with_config(cfg);
2365 ilu.setup(global_pc_csr.as_ref())?;
2366 if bench_cfg.run_mode == RunMode::Correctness {
2367 let pivot_perturbation_count = None::<usize>;
2368 let diag_global = reduce_csr_for_pc_diagnostics(
2369 &problem.comm,
2370 &csr_pc_diag,
2371 pivot_perturbation_count,
2372 );
2373 if problem.comm.rank() == 0 {
2374 print_csr_for_pc_diagnostics(&spec.method_label(), &diag_global);
2375 }
2376 }
2377 pc = Some(PcHandle::ReplicatedFull {
2378 ilu,
2379 comm: problem.comm.clone(),
2380 global_n: problem.global_n,
2381 global_row_start: problem.global_row_start,
2382 local_n: problem.local_n,
2383 scratch_in: std::sync::Mutex::new(vec![S::zero(); problem.global_n]),
2384 scratch_out: std::sync::Mutex::new(vec![S::zero(); problem.global_n]),
2385 });
2386 }
2387 PcDispatchBranch::OverlapIlu => {
2388 let (kind, overlap, restriction) = match spec.pc {
2389 PcKind::AsmIlu0Overlap { overlap } => {
2390 (IluKind::Ilu0, overlap, OverlapRestriction::Asm)
2391 }
2392 PcKind::RasIlu0Overlap { overlap } => {
2393 (IluKind::Ilu0, overlap, OverlapRestriction::Ras)
2394 }
2395 PcKind::RasIlukOverlap { k, overlap } => {
2396 (IluKind::Iluk { k }, overlap, OverlapRestriction::Ras)
2397 }
2398 _ => unreachable!(),
2399 };
2400 let mut cfg = IluCsrConfig::default();
2401 cfg.reordering = bench_cfg.ilu_reordering.clone();
2402 let pc_overlap = OverlapIluPc::setup_from_global_csr(
2403 global_pc_csr.as_ref(),
2404 problem.comm.clone(),
2405 problem.global_row_start,
2406 problem.global_row_start + problem.local_n,
2407 overlap,
2408 kind,
2409 cfg,
2410 restriction,
2411 )?;
2412 pc = Some(PcHandle::OverlapIlu(pc_overlap));
2413 }
2414 PcDispatchBranch::IlutLocal => {
2415 validate_local_ilu_owned_block(pc_csr.as_ref())?;
2416 let mut cfg = IluCsrConfig::default();
2417 cfg.kind = IluKind::Ilut {
2418 params: Default::default(),
2419 };
2420 cfg.reordering = bench_cfg.ilu_reordering.clone();
2421 let mut ilu = IluCsr::new_with_config(cfg);
2422 ilu.setup(pc_csr.as_ref())?;
2423 if bench_cfg.run_mode == RunMode::Correctness {
2424 let pivot_perturbation_count = None::<usize>;
2425 let diag_global = reduce_csr_for_pc_diagnostics(
2426 &problem.comm,
2427 &csr_pc_diag,
2428 pivot_perturbation_count,
2429 );
2430 if problem.comm.rank() == 0 {
2431 print_csr_for_pc_diagnostics(&spec.method_label(), &diag_global);
2432 }
2433 }
2434 pc = Some(PcHandle::Ilu0(ilu));
2435 }
2436 }
2437 Ok(pc)
2438 }
2439
2440 fn run_once(
2441 problem: &Problem,
2442 spec: &RunSpec,
2443 bench_cfg: &BenchmarkConfig,
2444 ) -> Result<ResultRow, KError> {
2445 let b_unscaled = &problem.rhs;
2446 let (row_scaling, op_scaled): (Option<Vec<R>>, Arc<dyn KLinOp<Scalar = S>>) =
2447 if bench_cfg.row_scale {
2448 let d = compute_row_scaling(problem.csr_for_pc.as_ref(), bench_cfg.row_scale_tiny);
2449 (
2450 Some(d.clone()),
2451 Arc::new(RowScaledOp {
2452 base: problem.op.clone(),
2453 d,
2454 }),
2455 )
2456 } else {
2457 (None, problem.op.clone())
2458 };
2459 let pc_csr: Arc<SparseCsrMatrix<S>> = if bench_cfg.row_scale {
2460 let row_scale = row_scaling.as_ref().ok_or_else(|| {
2461 KError::InvalidInput("row scaling requested but factors were not computed".into())
2462 })?;
2463 Arc::new(scale_csr_rows(problem.csr_for_pc.as_ref(), row_scale))
2464 } else {
2465 problem.csr_for_pc.clone()
2466 };
2467 let global_pc_csr: Arc<SparseCsrMatrix<S>> = if bench_cfg.row_scale {
2468 let row_scale = row_scaling.as_ref().ok_or_else(|| {
2469 KError::InvalidInput("row scaling requested but factors were not computed".into())
2470 })?;
2471 Arc::new(scale_csr_rows(problem.global_csr.as_ref(), row_scale))
2472 } else {
2473 problem.global_csr.clone()
2474 };
2475 let b_scaled: Vec<S> = if let Some(d) = &row_scaling {
2476 b_unscaled
2477 .iter()
2478 .zip(d.iter())
2479 .map(|(bi, di)| *bi * S::from_real(*di))
2480 .collect()
2481 } else {
2482 b_unscaled.clone()
2483 };
2484 let b = &b_scaled;
2485 let effective_pc_side = normalized_fgmres_side(spec.pc_side);
2486 let csr_pc_diag = csr_for_pc_diagnostics(
2487 pc_csr.as_ref(),
2488 problem.local_rows_nnz,
2489 problem.zero_global_rows_local,
2490 );
2491 let setup_start = Instant::now();
2492 let mut pc = setup_preconditioner_for_run_once(
2493 problem,
2494 spec,
2495 bench_cfg,
2496 &pc_csr,
2497 &global_pc_csr,
2498 &csr_pc_diag,
2499 )?;
2500 let setup_secs = setup_start.elapsed().as_secs_f64();
2501 for _ in 0..bench_cfg.warmup_runs {
2502 let mut x = vec![S::zero(); problem.local_n];
2503 let mut workspace = Workspace::new(problem.local_n);
2504 let _ = solve_with_selected_ksp(
2505 spec,
2506 bench_cfg,
2507 problem,
2508 op_scaled.as_ref(),
2509 pc.as_mut().map(PcHandle::as_kpc_mut),
2510 b,
2511 &mut x,
2512 effective_pc_side,
2513 None,
2514 Some(&mut workspace),
2515 )?;
2516 }
2517
2518 let mut solve_times = Vec::with_capacity(bench_cfg.measured_runs);
2519 let mut x_last = vec![S::zero(); problem.local_n];
2520 let mut final_stats = None;
2521 let mut residual_history_last = RunResidualHistory::default();
2522 for _ in 0..bench_cfg.measured_runs {
2523 let mut x = vec![S::zero(); problem.local_n];
2524 problem.comm.barrier();
2525 let start = Instant::now();
2526 let mut workspace = Workspace::new(problem.local_n);
2527 let mut run_history = RunResidualHistory::default();
2528 let mut monitors: Vec<Box<MonitorCallback<R>>> = Vec::new();
2529 if bench_cfg.residual_history {
2530 monitors.push(Box::new(|it, res, _| {
2531 let _ = (it, res);
2532 MonitorAction::Continue
2533 }));
2534 }
2535 let history_ref = std::sync::Arc::new(std::sync::Mutex::new(Vec::<(usize, R)>::new()));
2536 if bench_cfg.residual_history {
2537 let history_ref_c = history_ref.clone();
2538 monitors.clear();
2539 monitors.push(Box::new(move |it, res, _| {
2540 if let Ok(mut h) = history_ref_c.lock() {
2541 h.push((it, res));
2542 }
2543 MonitorAction::Continue
2544 }));
2545 }
2546 let stats = solve_with_selected_ksp(
2547 spec,
2548 bench_cfg,
2549 problem,
2550 op_scaled.as_ref(),
2551 pc.as_mut().map(PcHandle::as_kpc_mut),
2552 b,
2553 &mut x,
2554 effective_pc_side,
2555 if monitors.is_empty() {
2556 None
2557 } else {
2558 Some(&monitors)
2559 },
2560 Some(&mut workspace),
2561 )?;
2562 if bench_cfg.residual_history {
2563 if let Ok(h) = history_ref.lock() {
2564 run_history.entries = h
2565 .iter()
2566 .map(|(it, res)| ResidualHistoryEntry {
2567 iter: *it,
2568 recurrence_residual: *res,
2569 true_residual: None,
2570 checkpoint: false,
2571 })
2572 .collect();
2573 }
2574 residual_history_last = run_history;
2575 }
2576 problem.comm.barrier();
2577 let solve_secs = start.elapsed().as_secs_f64();
2578 solve_times.push(solve_secs);
2579 x_last = x;
2580 final_stats = Some(stats);
2581 }
2582 let stats = final_stats
2583 .ok_or_else(|| KError::InvalidInput("no measured solve run executed".into()))?;
2584 let x_unscaled = x_last;
2587 let min_solve_secs = solve_times.iter().copied().fold(f64::INFINITY, f64::min);
2588 let median_solve_secs = median(&mut solve_times);
2589
2590 let reductions = stats.counters.num_global_reductions;
2591 let overlapped_reduction_waits = stats.counters.overlap_global_reductions;
2592 let model_predicted_reductions = stats
2593 .reduction_model
2594 .as_ref()
2595 .map(|model| model.estimate_total(stats.iterations));
2596 let (explicit_true_residual, explicit_true_residual_rel) =
2597 if bench_cfg.run_mode == RunMode::Correctness {
2598 let mut ax = vec![S::zero(); b.len()];
2599 let mut scratch = BridgeScratch::default();
2600 problem.op.matvec_s(&x_unscaled, &mut ax, &mut scratch);
2601 for (ri, bi) in ax.iter_mut().zip(b_unscaled.iter().copied()) {
2602 *ri = bi - *ri;
2603 }
2604 let r2_local = ax.iter().map(|v| v.abs2()).sum::<f64>();
2605 let true_res = problem.comm.all_reduce_f64(r2_local).sqrt();
2606 let rhs_norm2_local = b_unscaled.iter().map(|v| v.abs2()).sum::<f64>();
2607 let rhs_norm = problem.comm.all_reduce_f64(rhs_norm2_local).sqrt();
2608 let rel_true = true_res / rhs_norm.max(f64::MIN_POSITIVE);
2609 (Some(true_res), Some(rel_true))
2610 } else {
2611 (None, None)
2612 };
2613 if bench_cfg.residual_history && bench_cfg.measured_runs > 0 && problem.comm.rank() == 0 {
2614 let mut checkpoint_count = 0usize;
2615 let restart = stats.effective_restart.unwrap_or(spec.restart).max(1);
2617 for e in &mut residual_history_last.entries {
2618 if e.iter > 0 && e.iter % restart == 0 {
2619 e.checkpoint = true;
2620 if matches!(
2621 spec.residual_check_policy,
2622 ResidualCheckPolicy::RestartOnly
2623 | ResidualCheckPolicy::OnConvergence
2624 | ResidualCheckPolicy::EveryIteration
2625 | ResidualCheckPolicy::Debug
2626 ) {
2627 e.true_residual = explicit_true_residual;
2628 }
2629 checkpoint_count += 1;
2630 }
2631 }
2632 println!(
2633 "[history][rank0] {}: {} points, {} restart checkpoints",
2634 format!(
2635 "{} [row-scale={}]",
2636 spec.method_label(),
2637 if bench_cfg.row_scale { "on" } else { "off" }
2638 ),
2639 residual_history_last.entries.len(),
2640 checkpoint_count
2641 );
2642 if let Some(path) = &bench_cfg.residual_history_file {
2643 dump_residual_history(path, &residual_history_last)?;
2644 println!("[history][rank0] wrote {}", path.display());
2645 }
2646 }
2647
2648 let x_error_rel = if bench_cfg.run_mode == RunMode::Correctness
2649 && problem.rhs_source == RhsSource::GeneratedAOnes
2650 && problem.solution_reference.valid_for_x_error()
2651 {
2652 let err2_local = x_unscaled
2653 .iter()
2654 .map(|xi| (*xi - S::one()).abs2())
2655 .sum::<f64>();
2656 let one2_local = x_unscaled.iter().map(|_| S::one().abs2()).sum::<f64>();
2657 let err = problem.comm.all_reduce_f64(err2_local).sqrt();
2658 let one_norm = problem.comm.all_reduce_f64(one2_local).sqrt();
2659 Some(err / one_norm.max(f64::MIN_POSITIVE))
2660 } else {
2661 None
2662 };
2663 let global_reference_check = if bench_cfg.run_mode == RunMode::Correctness {
2664 Some(global_reference_residual(problem, &x_unscaled, b_unscaled)?)
2665 } else {
2666 None
2667 };
2668 let verdict_tol = correctness_verdict_tolerance(bench_cfg);
2669 let dist_ok = explicit_true_residual_rel.map(|rel| rel <= verdict_tol);
2670 let global_ok = global_reference_check
2671 .as_ref()
2672 .map(|check| check.true_residual_rel <= verdict_tol);
2673 let x_ok = if bench_cfg.run_mode == RunMode::Correctness
2674 && problem.rhs_source == RhsSource::GeneratedAOnes
2675 && problem.solution_reference.valid_for_x_error()
2676 {
2677 global_reference_check
2678 .as_ref()
2679 .and_then(|check| check.x_error_rel)
2680 .map(|rel| rel <= verdict_tol)
2681 } else {
2682 None
2683 };
2684 let dof_per_sec = if matches!(
2685 problem.backend,
2686 CsrBackend::Serial | CsrBackend::Distributed
2687 ) && median_solve_secs > 0.0
2688 {
2689 Some(problem.global_n as f64 / median_solve_secs)
2690 } else {
2691 None
2692 };
2693
2694 Ok(ResultRow {
2695 operator_storage: operator_storage_label(problem),
2696 execution_backend: execution_backend_label(problem),
2697 pc_domain: pc_domain_label(spec.pc, problem),
2698 pc_apply: pc_apply_label(spec.pc, problem),
2699 method: format!(
2700 "{} [row-scale={}]",
2701 spec.method_label(),
2702 if bench_cfg.row_scale { "on" } else { "off" }
2703 ),
2704 requested_policy: spec.requested_policy_label(),
2705 effective_policy: format!(
2706 "ksp={}, variant={}, restart={}, residual-check={}",
2707 spec.ksp.label(),
2708 stats
2709 .effective_variant
2710 .as_deref()
2711 .unwrap_or(variant_label(spec.variant)),
2712 stats.effective_restart.unwrap_or(spec.restart),
2713 stats
2714 .effective_residual_check_policy
2715 .as_deref()
2716 .unwrap_or(residual_check_policy_label(spec.residual_check_policy))
2717 ) + if bench_cfg.run_mode == RunMode::Correctness
2718 && bench_cfg.mark_replicated_check
2719 {
2720 " [replicated-check=enabled]"
2721 } else {
2722 ""
2723 },
2724 setup_secs,
2725 median_solve_secs,
2726 min_solve_secs,
2727 iterations: stats.iterations,
2728 reductions,
2729 overlapped_reduction_waits,
2730 model_predicted_reductions,
2731 restart_count: stats.fgmres_counters.as_ref().map(|c| c.restart_count),
2732 inner_iterations_last_cycle: stats
2733 .fgmres_counters
2734 .as_ref()
2735 .map(|c| c.inner_iterations_last_cycle),
2736 pipeline_fallbacks: stats.fgmres_counters.as_ref().map(|c| c.pipeline_fallbacks),
2737 reported_residual: stats.final_residual,
2738 explicit_true_residual,
2739 explicit_true_residual_rel,
2740 x_error_rel,
2741 global_true_residual: global_reference_check
2742 .as_ref()
2743 .map(|check| check.true_residual),
2744 global_true_residual_rel: global_reference_check
2745 .as_ref()
2746 .map(|check| check.true_residual_rel),
2747 global_x_error_rel: global_reference_check.and_then(|check| check.x_error_rel),
2748 dist_ok,
2749 global_ok,
2750 x_ok,
2751 reason: stats.reason,
2752 dof_per_sec,
2753 })
2754 }
2755
2756 fn validate_local_ilu_owned_block(matrix: &SparseCsrMatrix<S>) -> Result<(), KError> {
2757 if matrix.nrows() != matrix.ncols() {
2758 return Err(KError::InvalidInput(
2759 "local ILU requires square owned block".into(),
2760 ));
2761 }
2762 Ok(())
2763 }
2764
2765 fn csr_for_pc_diagnostics(
2766 matrix: &SparseCsrMatrix<S>,
2767 nnz_local_rows: usize,
2768 zero_global_rows: usize,
2769 ) -> CsrForPcDiagnostics {
2770 let mut diag_min_abs = f64::INFINITY;
2771 let mut diag_max_abs = 0.0f64;
2772 let mut zero_local_rows = 0usize;
2773 let mut structural_diag_missing = 0usize;
2774 let mut diag_tiny_or_missing_count = 0usize;
2775 let tiny_diag_threshold = 1e-14f64;
2776 let row_ptr = matrix.row_ptr();
2777 let col_idx = matrix.col_idx();
2778 let values = matrix.values();
2779 for r in 0..matrix.nrows() {
2780 if row_ptr[r] == row_ptr[r + 1] {
2781 zero_local_rows += 1;
2782 }
2783 let mut diag = None;
2784 for nz in row_ptr[r]..row_ptr[r + 1] {
2785 if col_idx[nz] == r {
2786 diag = Some(values[nz].abs());
2787 break;
2788 }
2789 }
2790 match diag {
2791 Some(v) => {
2792 diag_min_abs = diag_min_abs.min(v);
2793 diag_max_abs = diag_max_abs.max(v);
2794 if v <= tiny_diag_threshold {
2795 diag_tiny_or_missing_count += 1;
2796 }
2797 }
2798 None => {
2799 structural_diag_missing += 1;
2800 diag_tiny_or_missing_count += 1;
2801 }
2802 }
2803 }
2804 if !diag_min_abs.is_finite() {
2805 diag_min_abs = 0.0;
2806 }
2807 let nnz_local_block = values.len();
2808 let nnz_ratio = nnz_local_block as f64 / (nnz_local_rows.max(1) as f64);
2809 CsrForPcDiagnostics {
2810 nnz_local_block,
2811 nnz_local_rows,
2812 nnz_ratio,
2813 zero_local_rows,
2814 zero_global_rows,
2815 structural_diag_missing,
2816 diag_min_abs,
2817 diag_max_abs,
2818 diag_tiny_or_missing_count,
2819 }
2820 }
2821
2822 fn rank_spread_from_locals(locals: &[f64], rank0_local: f64) -> RankSpread {
2823 let global_sum = locals.iter().sum::<f64>();
2824 let rank_min = locals.iter().copied().fold(f64::INFINITY, f64::min);
2825 let rank_max = locals.iter().copied().fold(f64::NEG_INFINITY, f64::max);
2826 RankSpread {
2827 global_sum,
2828 rank_min: if rank_min.is_finite() { rank_min } else { 0.0 },
2829 rank_max: if rank_max.is_finite() { rank_max } else { 0.0 },
2830 rank0_local,
2831 }
2832 }
2833
2834 fn reduce_rank_spread<C: Comm<Vec = Vec<f64>>>(comm: &C, local: f64) -> RankSpread {
2835 if comm.size() <= 1 {
2836 return RankSpread {
2837 global_sum: local,
2838 rank_min: local,
2839 rank_max: local,
2840 rank0_local: local,
2841 };
2842 }
2843 let global_sum = comm.all_reduce_f64(local);
2844 let mut gathered = Vec::new();
2845 comm.gather(&[local], &mut gathered, 0);
2846 let (rank_min, rank_max) = if comm.rank() == 0 && !gathered.is_empty() {
2847 (
2848 gathered.iter().copied().fold(f64::INFINITY, f64::min),
2849 gathered.iter().copied().fold(f64::NEG_INFINITY, f64::max),
2850 )
2851 } else {
2852 (0.0, 0.0)
2853 };
2854 let rank0_local = if comm.rank() == 0 { local } else { 0.0 };
2855 RankSpread {
2856 global_sum,
2857 rank_min,
2858 rank_max,
2859 rank0_local,
2860 }
2861 }
2862
2863 fn reduce_csr_for_pc_diagnostics<C: Comm<Vec = Vec<f64>>>(
2864 comm: &C,
2865 local: &CsrForPcDiagnostics,
2866 pivot_perturbation_count: Option<usize>,
2867 ) -> CsrForPcDiagnosticsGlobal {
2868 let zero_local_rows_rank_count =
2869 reduce_rank_spread(comm, if local.zero_local_rows > 0 { 1.0 } else { 0.0 }).global_sum;
2870 CsrForPcDiagnosticsGlobal {
2871 nnz_local_block: reduce_rank_spread(comm, local.nnz_local_block as f64),
2872 nnz_local_rows: reduce_rank_spread(comm, local.nnz_local_rows as f64),
2873 nnz_ratio: reduce_rank_spread(comm, local.nnz_ratio),
2874 zero_local_rows_rank_count,
2875 zero_global_rows: reduce_rank_spread(comm, local.zero_global_rows as f64),
2876 structural_diag_missing: reduce_rank_spread(comm, local.structural_diag_missing as f64),
2877 diag_min_abs: reduce_rank_spread(comm, local.diag_min_abs),
2878 diag_max_abs: reduce_rank_spread(comm, local.diag_max_abs),
2879 diag_tiny_or_missing_count: reduce_rank_spread(
2880 comm,
2881 local.diag_tiny_or_missing_count as f64,
2882 ),
2883 pivot_perturbation_count: pivot_perturbation_count
2884 .map(|v| reduce_rank_spread(comm, v as f64)),
2885 }
2886 }
2887
2888 fn format_csr_for_pc_diagnostics(
2889 method_label: &str,
2890 diag: &CsrForPcDiagnosticsGlobal,
2891 ) -> String {
2892 let pivot_text = diag
2893 .pivot_perturbation_count
2894 .as_ref()
2895 .map(|v| {
2896 format!(
2897 "global_sum={:.0}, rank_min={:.0}, rank_max={:.0}, rank0_local={:.0}",
2898 v.global_sum, v.rank_min, v.rank_max, v.rank0_local
2899 )
2900 })
2901 .unwrap_or_else(|| "N/A".to_string());
2902 let nnz_ratio_global =
2903 diag.nnz_local_block.global_sum / diag.nnz_local_rows.global_sum.max(1.0);
2904 format!(
2905 "[diag][rank0] {method_label}: nnz(A_ii):global_sum={:.0},rank_min={:.0},rank_max={:.0},rank0_local={:.0}; nnz(local rows):global_sum={:.0},rank_min={:.0},rank_max={:.0},rank0_local={:.0}; nnz_ratio_global={:.3e},rank_local_min={:.3e},rank_local_max={:.3e},rank0_local={:.3e}; zero_local_rows_rank_count={:.0}; zero_global_rows:global_sum={:.0},rank_min={:.0},rank_max={:.0},rank0_local={:.0}; structural_diag_missing:global_sum={:.0},rank_min={:.0},rank_max={:.0},rank0_local={:.0}; |diag|min:global_sum={:.3e},rank_min={:.3e},rank_max={:.3e},rank0_local={:.3e}; |diag|max:global_sum={:.3e},rank_min={:.3e},rank_max={:.3e},rank0_local={:.3e}; tiny/missing diag:global_sum={:.0},rank_min={:.0},rank_max={:.0},rank0_local={:.0}; pivot perturbations={}",
2906 diag.nnz_local_block.global_sum,
2907 diag.nnz_local_block.rank_min,
2908 diag.nnz_local_block.rank_max,
2909 diag.nnz_local_block.rank0_local,
2910 diag.nnz_local_rows.global_sum,
2911 diag.nnz_local_rows.rank_min,
2912 diag.nnz_local_rows.rank_max,
2913 diag.nnz_local_rows.rank0_local,
2914 nnz_ratio_global,
2915 diag.nnz_ratio.rank_min,
2916 diag.nnz_ratio.rank_max,
2917 diag.nnz_ratio.rank0_local,
2918 diag.zero_local_rows_rank_count,
2919 diag.zero_global_rows.global_sum,
2920 diag.zero_global_rows.rank_min,
2921 diag.zero_global_rows.rank_max,
2922 diag.zero_global_rows.rank0_local,
2923 diag.structural_diag_missing.global_sum,
2924 diag.structural_diag_missing.rank_min,
2925 diag.structural_diag_missing.rank_max,
2926 diag.structural_diag_missing.rank0_local,
2927 diag.diag_min_abs.global_sum,
2928 diag.diag_min_abs.rank_min,
2929 diag.diag_min_abs.rank_max,
2930 diag.diag_min_abs.rank0_local,
2931 diag.diag_max_abs.global_sum,
2932 diag.diag_max_abs.rank_min,
2933 diag.diag_max_abs.rank_max,
2934 diag.diag_max_abs.rank0_local,
2935 diag.diag_tiny_or_missing_count.global_sum,
2936 diag.diag_tiny_or_missing_count.rank_min,
2937 diag.diag_tiny_or_missing_count.rank_max,
2938 diag.diag_tiny_or_missing_count.rank0_local,
2939 pivot_text
2940 )
2941 }
2942
2943 fn print_csr_for_pc_diagnostics(method_label: &str, diag: &CsrForPcDiagnosticsGlobal) {
2944 println!("{}", format_csr_for_pc_diagnostics(method_label, diag));
2945 }
2946
2947 fn configured_fgmres_solver(spec: &RunSpec, bench_cfg: &BenchmarkConfig) -> FgmresSolver {
2948 let mut solver = FgmresSolver::new(bench_cfg.rtol, bench_cfg.maxits, spec.restart);
2949 solver.variant = spec.variant;
2950 solver.residual_check_policy = spec.residual_check_policy;
2951 solver.orthog = spec.orthog;
2952 solver.reorth = spec.reorth;
2953 solver.atol = bench_cfg.atol;
2954 solver.dtol = 1e6;
2955 solver.haptol = bench_cfg.fgmres_haptol;
2956 solver.min_inner_before_fallback = bench_cfg.min_inner_before_fallback.max(1);
2957 solver.stagnation_policy =
2958 if bench_cfg.run_mode == RunMode::Correctness || !bench_cfg.allow_stagnation_fallback {
2959 FgmresStagnationPolicy::Disabled
2960 } else {
2961 match spec.variant {
2962 FgmresVariant::Classical => FgmresStagnationPolicy::RestartClassicalToo,
2963 FgmresVariant::Pipelined => FgmresStagnationPolicy::PipelineFallbackOnly,
2964 }
2965 };
2966 solver
2967 }
2968
2969 fn apply_fgmres_dist_plan_policy(solver: &mut FgmresSolver, problem: &Problem) {
2970 solver.apply_distcsr_policy(&problem.dist_plan_diagnostics, problem.comm.size());
2971 }
2972
2973 fn gmres_variant_from_fgmres(variant: FgmresVariant) -> PlainGmresVariant {
2974 match variant {
2975 FgmresVariant::Classical => PlainGmresVariant::Classical,
2976 FgmresVariant::Pipelined => PlainGmresVariant::Pipelined,
2977 }
2978 }
2979
2980 fn gmres_orthog_from_fgmres(orthog: OrthogMethod) -> GmresOrthog {
2981 match orthog {
2982 OrthogMethod::ClassicalGS => GmresOrthog::Cgs,
2983 OrthogMethod::ModifiedGS => GmresOrthog::Mgs,
2984 }
2985 }
2986
2987 fn configured_gmres_solver(spec: &RunSpec, bench_cfg: &BenchmarkConfig) -> GmresSolver {
2988 let mut solver = GmresSolver::new(spec.restart, bench_cfg.rtol, bench_cfg.maxits);
2989 solver.variant = gmres_variant_from_fgmres(spec.variant);
2990 solver.orthog = gmres_orthog_from_fgmres(spec.orthog);
2991 solver.reorth = spec.reorth;
2992 solver.conv.atol = bench_cfg.atol;
2993 solver.conv.dtol = 1e6;
2994 solver.haptol = bench_cfg.fgmres_haptol;
2995 solver
2996 }
2997
2998 #[allow(clippy::too_many_arguments)]
2999 fn solve_with_selected_ksp(
3000 spec: &RunSpec,
3001 bench_cfg: &BenchmarkConfig,
3002 problem: &Problem,
3003 op: &(dyn KLinOp<Scalar = S> + '_),
3004 mut pc: Option<&mut dyn KPreconditioner<Scalar = S>>,
3005 b: &[S],
3006 x: &mut [S],
3007 pc_side: PcSide,
3008 monitors: Option<&[Box<MonitorCallback<R>>]>,
3009 mut work: Option<&mut Workspace>,
3010 ) -> Result<kryst::utils::convergence::SolveStats<R>, KError> {
3011 match spec.ksp {
3012 KspKind::Gmres => {
3013 let mut solver = configured_gmres_solver(spec, bench_cfg);
3014 if let Some(w) = work.as_deref_mut() {
3015 solver.setup_workspace(w);
3016 }
3017 let pc_ref = pc
3018 .as_deref_mut()
3019 .map(|p| p as &dyn KPreconditioner<Scalar = S>);
3020 solver.solve(op, pc_ref, b, x, pc_side, &problem.comm, monitors, work)
3021 }
3022 KspKind::Fgmres => {
3023 let mut solver = configured_fgmres_solver(spec, bench_cfg);
3024 if bench_cfg.dist_policy == DistPolicyMode::Auto {
3025 apply_fgmres_dist_plan_policy(&mut solver, problem);
3026 }
3027 if let Some(w) = work.as_deref_mut() {
3028 solver.setup_workspace(w);
3029 }
3030 solver.solve_k(op, pc, b, x, pc_side, &problem.comm, monitors, work)
3031 }
3032 }
3033 }
3034
3035 fn dump_residual_history(path: &Path, history: &RunResidualHistory) -> Result<(), KError> {
3036 let mut f = File::create(path)
3037 .map_err(|e| KError::InvalidInput(format!("failed to create history file: {e}")))?;
3038 if path.extension().and_then(|e| e.to_str()) == Some("json") {
3039 writeln!(f, "[").map_err(|e| KError::InvalidInput(format!("write failed: {e}")))?;
3040 for (i, e) in history.entries.iter().enumerate() {
3041 writeln!(f, " {{\"iter\":{},\"recurrence_residual\":{},\"true_residual\":{},\"checkpoint\":{}}}{}", e.iter, e.recurrence_residual, e.true_residual.map(|v| v.to_string()).unwrap_or_else(|| "null".to_string()), e.checkpoint, if i + 1 == history.entries.len() {""} else {","})
3042 .map_err(|e| KError::InvalidInput(format!("write failed: {e}")))?;
3043 }
3044 writeln!(f, "]").map_err(|e| KError::InvalidInput(format!("write failed: {e}")))?;
3045 } else {
3046 writeln!(f, "iter,recurrence_residual,true_residual,checkpoint")
3047 .map_err(|e| KError::InvalidInput(format!("write failed: {e}")))?;
3048 for e in &history.entries {
3049 writeln!(
3050 f,
3051 "{},{},{},{}",
3052 e.iter,
3053 e.recurrence_residual,
3054 e.true_residual.map(|v| v.to_string()).unwrap_or_default(),
3055 e.checkpoint
3056 )
3057 .map_err(|e| KError::InvalidInput(format!("write failed: {e}")))?;
3058 }
3059 }
3060 Ok(())
3061 }
3062
3063 fn median(samples: &mut [f64]) -> f64 {
3064 samples.sort_by(f64::total_cmp);
3065 let n = samples.len();
3066 if n % 2 == 1 {
3067 samples[n / 2]
3068 } else {
3069 (samples[n / 2 - 1] + samples[n / 2]) * 0.5
3070 }
3071 }
3072
3073 fn render_verdict(flag: Option<bool>) -> &'static str {
3074 match flag {
3075 Some(true) => "OK",
3076 Some(false) => "FAIL",
3077 None => "N/A",
3078 }
3079 }
3080
3081 fn render_reason(row: &ResultRow, problem: &Problem) -> String {
3082 if row.reason == ConvergedReason::ConvergedRtol
3083 && problem.generated_case
3084 && problem.comm.size() > 1
3085 && (row.global_ok == Some(false) || row.x_ok == Some(false))
3086 {
3087 "ConvergedRtolDistOpButFailedGlobalReference".to_string()
3088 } else {
3089 format!("{:?}", row.reason)
3090 }
3091 }
3092
3093 fn render_result_row(row: &ResultRow, mode: RunMode, problem: &Problem) -> String {
3094 let rst = row
3095 .restart_count
3096 .map(|v| v.to_string())
3097 .unwrap_or_else(|| "N/A".to_string());
3098 let inn = row
3099 .inner_iterations_last_cycle
3100 .map(|v| v.to_string())
3101 .unwrap_or_else(|| "N/A".to_string());
3102 let pfb = row
3103 .pipeline_fallbacks
3104 .map(|v| v.to_string())
3105 .unwrap_or_else(|| "N/A".to_string());
3106 let include_dof_col = matches!(
3107 problem.backend,
3108 CsrBackend::Serial | CsrBackend::Distributed
3109 );
3110 if mode == RunMode::Scalability {
3111 let dof = row
3112 .dof_per_sec
3113 .map(|v| format!("{v:.2e}"))
3114 .unwrap_or_else(|| "N/A".to_string());
3115 return format!(
3116 "{:<7} {:<8} {:<6} {:<42} {:<36} {:<34} {:>9.3} {:>9.3} {:>7} {:>5} {:>5} {:>5} {:>17} {:>14.2e} {:>12}",
3117 row.operator_storage,
3118 row.execution_backend,
3119 row.pc_domain,
3120 row.pc_apply,
3121 row.method,
3122 row.effective_policy,
3123 row.median_solve_secs,
3124 row.min_solve_secs,
3125 row.iterations,
3126 rst,
3127 inn,
3128 pfb,
3129 format!(
3130 "{}/{}/{}",
3131 row.reductions,
3132 row.overlapped_reduction_waits,
3133 row.model_predicted_reductions
3134 .map(|v| v.to_string())
3135 .unwrap_or_else(|| "n/a".to_string())
3136 ),
3137 row.reported_residual,
3138 dof
3139 );
3140 }
3141 let explicit_true = row
3142 .explicit_true_residual
3143 .map(|v| format!("{v:.2e}"))
3144 .unwrap_or_else(|| "N/A".to_string());
3145 let explicit_true_rel = row
3146 .explicit_true_residual_rel
3147 .map(|v| format!("{v:.2e}"))
3148 .unwrap_or_else(|| "N/A".to_string());
3149 let x_error_rel = row
3150 .x_error_rel
3151 .map(|v| format!("{v:.2e}"))
3152 .unwrap_or_else(|| "N/A".to_string());
3153 let global_true = row
3154 .global_true_residual
3155 .map(|v| format!("{v:.2e}"))
3156 .unwrap_or_else(|| "N/A".to_string());
3157 let global_true_rel = row
3158 .global_true_residual_rel
3159 .map(|v| format!("{v:.2e}"))
3160 .unwrap_or_else(|| "N/A".to_string());
3161 let global_x_error_rel = row
3162 .global_x_error_rel
3163 .map(|v| format!("{v:.2e}"))
3164 .unwrap_or_else(|| "N/A".to_string());
3165 let dist_ok = render_verdict(row.dist_ok);
3166 let global_ok = render_verdict(row.global_ok);
3167 let x_ok = render_verdict(row.x_ok);
3168 let reason = render_reason(row, problem);
3169 if include_dof_col {
3170 let dof = row
3171 .dof_per_sec
3172 .map(|v| format!("{v:.2e}"))
3173 .unwrap_or_else(|| "N/A".to_string());
3174 format!(
3175 "{:<7} {:<8} {:<6} {:<42} {:<36} {:<34} {:<34} {:>9.3} {:>9.3} {:>9.3} {:>7} {:>5} {:>5} {:>5} {:>14.2e} {:>14} {:>14} {:>14} {:>14} {:>14} {:>14} {:>8} {:>8} {:>8} {:>26} {:>12}",
3176 row.operator_storage,
3177 row.execution_backend,
3178 row.pc_domain,
3179 row.pc_apply,
3180 row.method,
3181 row.requested_policy,
3182 row.effective_policy,
3183 row.setup_secs,
3184 row.median_solve_secs,
3185 row.min_solve_secs,
3186 row.iterations,
3187 rst,
3188 inn,
3189 pfb,
3190 row.reported_residual,
3191 explicit_true,
3192 explicit_true_rel,
3193 x_error_rel,
3194 global_true,
3195 global_true_rel,
3196 global_x_error_rel,
3197 dist_ok,
3198 global_ok,
3199 x_ok,
3200 reason,
3201 dof
3202 )
3203 } else {
3204 format!(
3205 "{:<7} {:<8} {:<6} {:<42} {:<36} {:<34} {:<34} {:>9.3} {:>9.3} {:>9.3} {:>7} {:>5} {:>5} {:>5} {:>14.2e} {:>14} {:>14} {:>14} {:>14} {:>14} {:>14} {:>8} {:>8} {:>8} {:>26}",
3206 row.operator_storage,
3207 row.execution_backend,
3208 row.pc_domain,
3209 row.pc_apply,
3210 row.method,
3211 row.requested_policy,
3212 row.effective_policy,
3213 row.setup_secs,
3214 row.median_solve_secs,
3215 row.min_solve_secs,
3216 row.iterations,
3217 rst,
3218 inn,
3219 pfb,
3220 row.reported_residual,
3221 explicit_true,
3222 explicit_true_rel,
3223 x_error_rel,
3224 global_true,
3225 global_true_rel,
3226 global_x_error_rel,
3227 dist_ok,
3228 global_ok,
3229 x_ok,
3230 reason
3231 )
3232 }
3233 }
3234
3235 fn render_failure_result_row(spec: &RunSpec, mode: RunMode, problem: &Problem) -> String {
3236 let include_dof_col = matches!(
3237 problem.backend,
3238 CsrBackend::Serial | CsrBackend::Distributed
3239 );
3240 if mode == RunMode::Scalability {
3241 return format!(
3242 "{:<7} {:<8} {:<6} {:<42} {:<36} {:<34} {:>9} {:>9} {:>7} {:>5} {:>5} {:>5} {:>17} {:>14} {:>12}",
3243 operator_storage_label(problem),
3244 execution_backend_label(problem),
3245 pc_domain_label(spec.pc, problem),
3246 pc_apply_label(spec.pc, problem),
3247 spec.method_label(),
3248 "N/A",
3249 "FAIL",
3250 "FAIL",
3251 "N/A",
3252 "N/A",
3253 "N/A",
3254 "N/A",
3255 "N/A",
3256 "N/A",
3257 "N/A"
3258 );
3259 }
3260 if include_dof_col {
3261 format!(
3262 "{:<7} {:<8} {:<6} {:<42} {:<36} {:<34} {:<34} {:>9} {:>9} {:>9} {:>7} {:>5} {:>5} {:>5} {:>14} {:>14} {:>14} {:>14} {:>14} {:>14} {:>14} {:>8} {:>8} {:>8} {:>26} {:>12}",
3263 operator_storage_label(problem),
3264 execution_backend_label(problem),
3265 pc_domain_label(spec.pc, problem),
3266 pc_apply_label(spec.pc, problem),
3267 spec.method_label(),
3268 spec.requested_policy_label(),
3269 "N/A",
3270 "FAIL",
3271 "FAIL",
3272 "FAIL",
3273 "N/A",
3274 "N/A",
3275 "N/A",
3276 "N/A",
3277 "N/A",
3278 "N/A",
3279 "N/A",
3280 "N/A",
3281 "N/A",
3282 "N/A",
3283 "N/A",
3284 "N/A",
3285 "N/A",
3286 "N/A",
3287 "N/A",
3288 "N/A"
3289 )
3290 } else {
3291 format!(
3292 "{:<7} {:<8} {:<6} {:<42} {:<36} {:<34} {:<34} {:>9} {:>9} {:>9} {:>7} {:>5} {:>5} {:>5} {:>14} {:>14} {:>14} {:>14} {:>14} {:>14} {:>14} {:>8} {:>8} {:>8} {:>26}",
3293 operator_storage_label(problem),
3294 execution_backend_label(problem),
3295 pc_domain_label(spec.pc, problem),
3296 pc_apply_label(spec.pc, problem),
3297 spec.method_label(),
3298 spec.requested_policy_label(),
3299 "N/A",
3300 "FAIL",
3301 "FAIL",
3302 "FAIL",
3303 "N/A",
3304 "N/A",
3305 "N/A",
3306 "N/A",
3307 "N/A",
3308 "N/A",
3309 "N/A",
3310 "N/A",
3311 "N/A",
3312 "N/A",
3313 "N/A",
3314 "N/A",
3315 "N/A",
3316 "N/A",
3317 "N/A"
3318 )
3319 }
3320 }
3321
3322 fn solution_reference_diagnostics(matrix: &SparseCsrMatrix<S>) -> SolutionReferenceDiagnostics {
3323 let tiny_diag_threshold = 1e-14f64;
3324 let row_ptr = matrix.row_ptr();
3325 let col_idx = matrix.col_idx();
3326 let values = matrix.values();
3327 let mut zero_row_count = 0usize;
3328 let mut missing_diagonal_count = 0usize;
3329 let mut tiny_diagonal_count = 0usize;
3330
3331 for row in 0..matrix.nrows() {
3332 let start = row_ptr[row];
3333 let end = row_ptr[row + 1];
3334 if start == end {
3335 zero_row_count += 1;
3336 }
3337 let mut diag = None;
3338 for nz in start..end {
3339 if col_idx[nz] == row {
3340 diag = Some(values[nz].abs());
3341 break;
3342 }
3343 }
3344 match diag {
3345 Some(v) if v <= tiny_diag_threshold => tiny_diagonal_count += 1,
3346 Some(_) => {}
3347 None => missing_diagonal_count += 1,
3348 }
3349 }
3350
3351 SolutionReferenceDiagnostics {
3352 global_rows: matrix.nrows(),
3353 global_cols: matrix.ncols(),
3354 zero_row_count,
3355 missing_diagonal_count,
3356 tiny_diagonal_count,
3357 }
3358 }
3359
3360 fn build_shifted_poisson_case(
3361 comm: &UniverseComm,
3362 nx: usize,
3363 ny: usize,
3364 alpha: f64,
3365 beta: f64,
3366 convection_x: f64,
3367 convection_y: f64,
3368 ) -> Result<Problem, KError> {
3369 if nx == 0 || ny == 0 {
3370 return Err(KError::InvalidInput(
3371 "shifted Poisson grid dimensions must be positive".into(),
3372 ));
3373 }
3374 let n = nx.checked_mul(ny).ok_or_else(|| {
3375 KError::InvalidInput(format!("shifted Poisson grid {nx}x{ny} overflows usize"))
3376 })?;
3377 if n < comm.size() {
3378 return Err(KError::InvalidInput(format!(
3379 "shifted Poisson grid {nx}x{ny} has {n} rows, fewer than {} MPI ranks",
3380 comm.size()
3381 )));
3382 }
3383
3384 let csr_sparse = build_shifted_poisson_csr(nx, ny, alpha, beta, convection_x, convection_y);
3385 let solution_reference = solution_reference_diagnostics(&csr_sparse);
3386 let row_part = DistCsrOp::partition_rows_balanced(n, comm);
3387 let row_start = row_part[comm.rank()];
3388 let row_end = row_part[comm.rank() + 1];
3389 let local_csr = slice_csr_rows(&csr_sparse, row_start, row_end);
3390 let local_pc_block = slice_csr_rows_owned_cols(&csr_sparse, row_start, row_end);
3391
3392 let op = DistCsrOp::from_local_rows(n, row_start, &local_csr, &row_part, comm.clone())?;
3393 let dist_plan_diagnostics = op.plan_diagnostics().clone();
3394 let op_arc: Arc<dyn KLinOp<Scalar = S>> = Arc::new(op);
3395
3396 let ones = vec![S::one(); n];
3397 let mut rhs_global = vec![S::zero(); n];
3398 csr_sparse.spmv(&ones, &mut rhs_global);
3399 let rhs = rhs_global[row_start..row_end].to_vec();
3400
3401 let local_n = row_end - row_start;
3402 let backend = classify_backend(comm.size(), local_n, n);
3403
3404 Ok(Problem {
3405 op: op_arc,
3406 dist_plan_diagnostics,
3407 rhs,
3408 rhs_source: RhsSource::GeneratedAOnes,
3409 solution_reference,
3410 csr_for_pc: Arc::new(local_pc_block),
3411 global_csr: Arc::new(csr_sparse),
3412 local_rows_nnz: local_csr.values().len(),
3413 zero_global_rows_local: count_zero_rows(&local_csr),
3414 local_n,
3415 global_n: n,
3416 global_row_start: row_start,
3417 comm: comm.clone(),
3418 backend,
3419 backend_descr: format!(
3420 "Generated shifted Poisson CSR ({}x{} grid, shift={alpha:.3}+{beta:.3}i, convection=({convection_x:.3},{convection_y:.3}))",
3421 nx, ny
3422 ),
3423 generated_case: true,
3424 })
3425 }
3426
3427 fn build_shifted_poisson_csr(
3428 nx: usize,
3429 ny: usize,
3430 alpha: f64,
3431 beta: f64,
3432 convection_x: f64,
3433 convection_y: f64,
3434 ) -> SparseCsrMatrix<S> {
3435 let n = nx * ny;
3436 let shift = S::from_parts(alpha, beta);
3437 let mut row_ptr = Vec::with_capacity(n + 1);
3438 let mut col_idx = Vec::with_capacity(n * 5);
3439 let mut values = Vec::with_capacity(n * 5);
3440 row_ptr.push(0);
3441
3442 for iy in 0..ny {
3443 for ix in 0..nx {
3444 let row = iy * nx + ix;
3445 let mut entries = Vec::with_capacity(5);
3446 if iy > 0 {
3447 entries.push((row - nx, S::from_real(-1.0 - 0.5 * convection_y)));
3448 }
3449 if ix > 0 {
3450 entries.push((row - 1, S::from_real(-1.0 - 0.5 * convection_x)));
3451 }
3452 entries.push((row, S::from_real(4.0) + shift));
3453 if ix + 1 < nx {
3454 entries.push((row + 1, S::from_real(-1.0 + 0.5 * convection_x)));
3455 }
3456 if iy + 1 < ny {
3457 entries.push((row + nx, S::from_real(-1.0 + 0.5 * convection_y)));
3458 }
3459 entries.sort_by_key(|&(col, _)| col);
3460 for (col, val) in entries {
3461 col_idx.push(col);
3462 values.push(val);
3463 }
3464 row_ptr.push(col_idx.len());
3465 }
3466 }
3467
3468 SparseCsrMatrix::from_csr(n, n, row_ptr, col_idx, values)
3469 }
3470
3471 fn load_problem_complex(
3472 mat_path: &Path,
3473 comm: &UniverseComm,
3474 _run_mode: RunMode,
3475 _mark_replicated_check: bool,
3476 ) -> Result<Problem, KError> {
3477 let mm = read_matrix_market(mat_path)?;
3478 let csr_sparse: SparseCsrMatrix<S> = mm.to_csr_matrix_scalar()?;
3479 let nrows = csr_sparse.nrows();
3480 let ncols = csr_sparse.ncols();
3481 let solution_reference = solution_reference_diagnostics(&csr_sparse);
3482
3483 let row_part = DistCsrOp::partition_rows_balanced(nrows, comm);
3484 let row_start = row_part[comm.rank()];
3485 let row_end = row_part[comm.rank() + 1];
3486 let local_csr = slice_csr_rows(&csr_sparse, row_start, row_end);
3487 let local_pc_block = slice_csr_rows_owned_cols(&csr_sparse, row_start, row_end);
3488
3489 let op = DistCsrOp::from_local_rows(nrows, row_start, &local_csr, &row_part, comm.clone())?;
3490 let dist_plan_diagnostics = op.plan_diagnostics().clone();
3491 let op_arc: Arc<dyn KLinOp<Scalar = S>> = Arc::new(op);
3492
3493 let (rhs_global, rhs_source) = match try_load_rhs_s(mat_path, nrows) {
3494 Some(vec) => (vec, RhsSource::LoadedFromFile),
3495 None => {
3496 let ones = vec![S::one(); ncols];
3497 let mut b = vec![S::zero(); nrows];
3498 csr_sparse.spmv(&ones, &mut b);
3499 (b, RhsSource::GeneratedAOnes)
3500 }
3501 };
3502 let rhs = rhs_global[row_start..row_end].to_vec();
3503
3504 let local_n = row_end - row_start;
3505 let global_n = nrows;
3506 let backend = classify_backend(comm.size(), local_n, global_n);
3507
3508 Ok(Problem {
3509 op: op_arc,
3510 dist_plan_diagnostics,
3511 rhs,
3512 rhs_source,
3513 solution_reference,
3514 csr_for_pc: Arc::new(local_pc_block),
3515 global_csr: Arc::new(csr_sparse),
3516 local_rows_nnz: local_csr.values().len(),
3517 zero_global_rows_local: count_zero_rows(&local_csr),
3518 local_n,
3519 global_n,
3520 global_row_start: row_start,
3521 comm: comm.clone(),
3522 backend,
3523 backend_descr: "Distributed CSR (complex)".to_string(),
3524 generated_case: false,
3525 })
3526 }
3527
3528 fn count_zero_rows(matrix: &SparseCsrMatrix<S>) -> usize {
3529 let row_ptr = matrix.row_ptr();
3530 (0..matrix.nrows())
3531 .filter(|&r| row_ptr[r] == row_ptr[r + 1])
3532 .count()
3533 }
3534
3535 fn slice_csr_rows(matrix: &SparseCsrMatrix<S>, start: usize, end: usize) -> SparseCsrMatrix<S> {
3536 let row_ptr = matrix.row_ptr();
3537 let col_idx = matrix.col_idx();
3538 let values = matrix.values();
3539 let start_nnz = row_ptr[start];
3540 let end_nnz = row_ptr[end];
3541
3542 let mut local_rp = Vec::with_capacity(end - start + 1);
3543 for r in start..=end {
3544 local_rp.push(row_ptr[r] - start_nnz);
3545 }
3546 let local_ci = col_idx[start_nnz..end_nnz].to_vec();
3547 let local_vals = values[start_nnz..end_nnz].to_vec();
3548
3549 SparseCsrMatrix::from_csr(end - start, matrix.ncols(), local_rp, local_ci, local_vals)
3550 }
3551
3552 fn slice_csr_rows_owned_cols(
3553 matrix: &SparseCsrMatrix<S>,
3554 row_start: usize,
3555 row_end: usize,
3556 ) -> SparseCsrMatrix<S> {
3557 let row_ptr = matrix.row_ptr();
3558 let col_idx = matrix.col_idx();
3559 let values = matrix.values();
3560 let local_n = row_end - row_start;
3561
3562 let mut local_rp = Vec::with_capacity(local_n + 1);
3563 let mut local_ci = Vec::new();
3564 let mut local_vals = Vec::new();
3565 local_rp.push(0);
3566
3567 for global_r in row_start..row_end {
3568 let mut row_nnz = 0usize;
3569 for nz in row_ptr[global_r]..row_ptr[global_r + 1] {
3570 let global_c = col_idx[nz];
3571 if (row_start..row_end).contains(&global_c) {
3572 local_ci.push(global_c - row_start);
3573 local_vals.push(values[nz]);
3574 row_nnz += 1;
3575 }
3576 }
3577 local_rp.push(local_rp.last().copied().unwrap_or(0) + row_nnz);
3578 }
3579
3580 SparseCsrMatrix::from_csr(local_n, local_n, local_rp, local_ci, local_vals)
3581 }
3582
3583 fn partition_rows_balanced_for_size(n_global: usize, size: usize) -> Vec<usize> {
3584 let base = n_global / size;
3585 let rem = n_global % size;
3586 let mut out = Vec::with_capacity(size + 1);
3587 out.push(0);
3588 for rank in 0..size {
3589 let take = base + usize::from(rank < rem);
3590 out.push(out[rank] + take);
3591 }
3592 out
3593 }
3594
3595 fn classify_backend(size: usize, local_n: usize, global_n: usize) -> CsrBackend {
3596 if size <= 1 {
3597 CsrBackend::Serial
3598 } else if local_n == global_n {
3599 CsrBackend::Replicated
3600 } else {
3601 CsrBackend::Distributed
3602 }
3603 }
3604
3605 fn try_load_rhs_s(mat_path: &Path, n: usize) -> Option<Vec<S>> {
3606 let mut rhs_path = mat_path.to_path_buf();
3607 if let Some(stem) = mat_path.file_stem().and_then(|s| s.to_str()) {
3608 rhs_path.set_file_name(format!("{stem}_rhs.mtx"));
3609 if rhs_path.exists() {
3610 if let Ok(mm) = read_matrix_market(&rhs_path) {
3611 if let Ok(v) = mm.to_vector_scalar() {
3612 if v.len() == n {
3613 return Some(v);
3614 }
3615 }
3616 }
3617 }
3618 }
3619 None
3620 }
3621
3622 fn assert_shifted_poisson_valid_nonzero_diagonal(matrix: &SparseCsrMatrix<S>) {
3623 assert_eq!(matrix.nrows(), matrix.ncols());
3624 assert_eq!(matrix.row_ptr().len(), matrix.nrows() + 1);
3625 assert!(matrix.values().len() >= matrix.nrows());
3626
3627 for row in 0..matrix.nrows() {
3628 let start = matrix.row_ptr()[row];
3629 let end = matrix.row_ptr()[row + 1];
3630 let diag = (start..end)
3631 .find(|&nz| matrix.col_idx()[nz] == row)
3632 .map(|nz| matrix.values()[nz]);
3633 assert!(
3634 diag.is_some_and(|v| v.abs() > 0.0),
3635 "row {row} must have a nonzero diagonal"
3636 );
3637 }
3638 }
3639
3640 #[test]
3641 fn shifted_poisson_generated_easy_and_hard_matrices_are_valid() {
3642 for (alpha, beta) in [(1.0, 0.25), (0.01, 0.25)] {
3643 let matrix = build_shifted_poisson_csr(4, 3, alpha, beta, 0.0, 0.0);
3644 assert_eq!(matrix.nrows(), 12);
3645 assert_eq!(matrix.ncols(), 12);
3646 assert_shifted_poisson_valid_nonzero_diagonal(&matrix);
3647 }
3648 }
3649
3650 #[test]
3651 fn shifted_poisson_zero_convection_preserves_existing_stencil() {
3652 let matrix = build_shifted_poisson_csr(3, 3, 1.0, 0.25, 0.0, 0.0);
3653 let row = 4;
3654 let start = matrix.row_ptr()[row];
3655 let end = matrix.row_ptr()[row + 1];
3656 let entries = (start..end)
3657 .map(|nz| (matrix.col_idx()[nz], matrix.values()[nz]))
3658 .collect::<Vec<_>>();
3659
3660 assert_eq!(entries.len(), 5);
3661 assert_eq!(entries[0].0, 1);
3662 assert_eq!(entries[0].1.real(), -1.0);
3663 assert_eq!(entries[1].0, 3);
3664 assert_eq!(entries[1].1.real(), -1.0);
3665 assert_eq!(entries[2].0, 4);
3666 assert_eq!(entries[2].1.real(), 5.0);
3667 assert_eq!(entries[2].1.imag(), 0.25);
3668 assert_eq!(entries[3].0, 5);
3669 assert_eq!(entries[3].1.real(), -1.0);
3670 assert_eq!(entries[4].0, 7);
3671 assert_eq!(entries[4].1.real(), -1.0);
3672 }
3673
3674 #[test]
3675 fn shifted_poisson_partition_has_nonzero_local_rows_for_typical_rank_counts() {
3676 let matrix = build_shifted_poisson_csr(4, 4, 1.0, 0.25, 0.0, 0.0);
3677 for size in [1usize, 2, 3, 4, 8] {
3678 let part = partition_rows_balanced_for_size(matrix.nrows(), size);
3679 for rank in 0..size {
3680 let row_start = part[rank];
3681 let row_end = part[rank + 1];
3682 let local = slice_csr_rows(&matrix, row_start, row_end);
3683 assert!(
3684 local.nrows() > 0,
3685 "rank {rank}/{size} should own at least one generated Poisson row"
3686 );
3687 assert!(
3688 local.values().len() > 0,
3689 "rank {rank}/{size} should own generated Poisson nonzeros"
3690 );
3691 }
3692 }
3693 }
3694
3695 #[test]
3696 fn shifted_poisson_dist_spmv_matches_global_spmv_for_ones_and_random_x() {
3697 #[cfg(feature = "mpi")]
3698 let comm = UniverseComm::Mpi(Arc::new(MpiComm::new()));
3699 #[cfg(not(feature = "mpi"))]
3700 let comm = UniverseComm::NoComm(NoComm);
3701
3702 let nx = 8usize;
3703 let ny = 7usize;
3704 let alpha = 1.0;
3705 let beta = 0.25;
3706 let global_csr = build_shifted_poisson_csr(nx, ny, alpha, beta, 0.0, 0.0);
3707 let global_n = global_csr.nrows();
3708 if global_n < comm.size() {
3709 eprintln!(
3710 "skipping shifted Poisson distributed SpMV check: {global_n} rows for {} ranks",
3711 comm.size()
3712 );
3713 return;
3714 }
3715
3716 let row_part = DistCsrOp::partition_rows_balanced(global_n, &comm);
3717 let global_row_start = row_part[comm.rank()];
3718 let global_row_end = row_part[comm.rank() + 1];
3719 let local_n = global_row_end - global_row_start;
3720 let local_csr = slice_csr_rows(&global_csr, global_row_start, global_row_end);
3721 let op = DistCsrOp::from_local_rows(
3722 global_n,
3723 global_row_start,
3724 &local_csr,
3725 &row_part,
3726 comm.clone(),
3727 )
3728 .expect("build distributed shifted Poisson CSR operator");
3729
3730 let ones = vec![S::one(); global_n];
3731 let ramp = (0..global_n)
3732 .map(|i| S::from_parts(0.125 * (i as f64 + 1.0), -0.0625 * (i as f64 + 3.0)))
3733 .collect::<Vec<_>>();
3734 let mut state = 0x9E37_79B9_7F4A_7C15_u64;
3735 let random = (0..global_n)
3736 .map(|_| {
3737 state = state
3738 .wrapping_mul(6364136223846793005)
3739 .wrapping_add(1442695040888963407);
3740 let re = ((state >> 33) as f64) / ((1_u64 << 31) as f64) - 0.5;
3741 state = state
3742 .wrapping_mul(6364136223846793005)
3743 .wrapping_add(1442695040888963407);
3744 let im = ((state >> 33) as f64) / ((1_u64 << 31) as f64) - 0.5;
3745 S::from_parts(re, im)
3746 })
3747 .collect::<Vec<_>>();
3748
3749 let cases = [
3750 ("ones", ones),
3751 ("complex ramp", ramp),
3752 ("pseudo-random", random),
3753 ];
3754 let mut scratch = BridgeScratch::default();
3755 for (label, x_global) in cases {
3756 let x_local = x_global[global_row_start..global_row_end].to_vec();
3757 let mut y_dist = vec![S::zero(); local_n];
3758 op.matvec_s(&x_local, &mut y_dist, &mut scratch);
3759
3760 let mut y_global = vec![S::zero(); global_n];
3761 global_csr.spmv(&x_global, &mut y_global);
3762 let expected = &y_global[global_row_start..global_row_end];
3763
3764 assert_eq!(
3765 y_dist.len(),
3766 expected.len(),
3767 "{label} local result length mismatch on rank {}",
3768 comm.rank()
3769 );
3770 for (local_row, (&actual, &want)) in y_dist.iter().zip(expected).enumerate() {
3771 let diff = (actual - want).abs();
3772 assert!(
3773 diff <= 1.0e-12,
3774 "{label} SpMV mismatch on rank {}, local row {local_row} (global row {}): actual={actual:?}, expected={want:?}, diff={diff:e}",
3775 comm.rank(),
3776 global_row_start + local_row
3777 );
3778 }
3779 }
3780 }
3781
3782 #[test]
3783 fn shifted_poisson_problem_rhs_matches_a_times_ones() {
3784 #[cfg(feature = "mpi")]
3785 let comm = UniverseComm::Mpi(Arc::new(MpiComm::new()));
3786 #[cfg(not(feature = "mpi"))]
3787 let comm = UniverseComm::NoComm(NoComm);
3788
3789 let nx = 5usize;
3790 let ny = 4usize;
3791 let alpha = 1.0;
3792 let beta = 0.25;
3793 let problem = build_shifted_poisson_case(&comm, nx, ny, alpha, beta, 0.0, 0.0)
3794 .expect("build generated shifted Poisson problem");
3795 let matrix = build_shifted_poisson_csr(nx, ny, alpha, beta, 0.0, 0.0);
3796 let ones = vec![S::one(); matrix.ncols()];
3797 let mut expected_global = vec![S::zero(); matrix.nrows()];
3798 matrix.spmv(&ones, &mut expected_global);
3799 let expected =
3800 &expected_global[problem.global_row_start..problem.global_row_start + problem.local_n];
3801
3802 assert_eq!(problem.global_n, nx * ny);
3803 assert_eq!(problem.local_n, problem.rhs.len());
3804 assert_eq!(problem.rhs_source, RhsSource::GeneratedAOnes);
3805 assert!(problem.generated_case);
3806 assert_eq!(problem.rhs, expected);
3807 }
3808
3809 #[test]
3810 fn shifted_poisson_default_pcs_include_requested_generated_set() {
3811 #[cfg(feature = "mpi")]
3812 let comm = UniverseComm::Mpi(Arc::new(MpiComm::new()));
3813 #[cfg(not(feature = "mpi"))]
3814 let comm = UniverseComm::NoComm(NoComm);
3815
3816 let problem = build_shifted_poisson_case(&comm, 4, 4, 1.0, 0.25, 0.0, 0.0)
3817 .expect("build generated shifted Poisson problem");
3818 let cfg = BenchmarkConfig {
3819 restarts: vec![20],
3820 ..BenchmarkConfig::default()
3821 };
3822 let pcs = RunSpec::build_default_matrix(&cfg, &problem)
3823 .into_iter()
3824 .map(|spec| spec.pc)
3825 .collect::<Vec<_>>();
3826
3827 assert!(pcs.contains(&PcKind::None));
3828 assert!(pcs.contains(&PcKind::JacobiWeak));
3829 assert!(pcs.contains(&PcKind::LocalIluk { k: 1 }));
3830 if problem.comm.size() > 1 {
3831 assert!(pcs.contains(&PcKind::MpiBlockJacobiIlu0Local));
3832 } else {
3833 assert!(pcs.contains(&PcKind::Ilu0Local));
3834 }
3835 }
3836
3837 #[test]
3838 fn qc324_owned_pc_block_is_local_square_on_4_way_partition() {
3839 let mat_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/mtx/qc324.mtx");
3840 if !mat_path.exists() {
3841 eprintln!("qc324.mtx unavailable; skipping local PC block-shape check.");
3842 return;
3843 }
3844
3845 let mm = read_matrix_market(&mat_path).expect("read qc324");
3846 let csr_sparse: SparseCsrMatrix<S> = mm.to_csr_matrix_scalar().expect("qc324 to CSR");
3847 let n = csr_sparse.nrows();
3848 let part = partition_rows_balanced_for_size(n, 4);
3849
3850 for rank in 0..4 {
3851 let row_start = part[rank];
3852 let row_end = part[rank + 1];
3853 let local_n = row_end - row_start;
3854 let pc_block = slice_csr_rows_owned_cols(&csr_sparse, row_start, row_end);
3855 eprintln!(
3856 "qc324 rank {rank}: rows=[{row_start}..{row_end}), pc_block=({} x {})",
3857 pc_block.nrows(),
3858 pc_block.ncols()
3859 );
3860 assert_eq!(pc_block.nrows(), local_n, "rank {rank} local PC rows");
3861 assert_eq!(pc_block.ncols(), local_n, "rank {rank} local PC cols");
3862 assert!(
3863 pc_block.col_idx().iter().all(|&c| c < local_n),
3864 "rank {rank} has out-of-range local column index"
3865 );
3866 }
3867 }
3868
3869 #[cfg(feature = "mpi")]
3870 #[test]
3871 fn replicated_full_ilu_apply_matches_serial_reference_under_mpi() {
3872 let comm = UniverseComm::Mpi(Arc::new(MpiComm::new()));
3873 let rank = comm.rank();
3874 let size = comm.size();
3875 let global_n = 4usize;
3876 let row_part = partition_rows_balanced_for_size(global_n, size);
3877 let row_start = row_part[rank];
3878 let row_end = row_part[rank + 1];
3879 let local_n = row_end - row_start;
3880
3881 let matrix = SparseCsrMatrix::from_csr(
3882 global_n,
3883 global_n,
3884 vec![0, 1, 2, 3, 4],
3885 vec![0, 1, 2, 3],
3886 vec![
3887 S::from_parts(2.0, 0.0),
3888 S::from_parts(4.0, 0.0),
3889 S::from_parts(5.0, 0.0),
3890 S::from_parts(10.0, 0.0),
3891 ],
3892 );
3893 let x_global = vec![
3894 S::from_parts(2.0, 1.0),
3895 S::from_parts(8.0, -4.0),
3896 S::from_parts(15.0, 5.0),
3897 S::from_parts(40.0, -10.0),
3898 ];
3899 let x_local = x_global[row_start..row_end].to_vec();
3900
3901 let mut cfg = IluCsrConfig::default();
3902 cfg.kind = IluKind::Ilu0;
3903 let mut serial_ilu = IluCsr::new_with_config(cfg.clone());
3904 serial_ilu.setup(&matrix).expect("serial ILU setup");
3905 let mut replicated_ilu = IluCsr::new_with_config(cfg);
3906 replicated_ilu.setup(&matrix).expect("replicated ILU setup");
3907
3908 let mut scratch = BridgeScratch::default();
3909 let mut expected_global = vec![S::zero(); global_n];
3910 serial_ilu
3911 .apply_s(PcSide::Right, &x_global, &mut expected_global, &mut scratch)
3912 .expect("serial ILU apply");
3913
3914 let mut y_local = vec![S::zero(); local_n];
3915 let scratch_in = std::sync::Mutex::new(vec![S::zero(); global_n]);
3916 let scratch_out = std::sync::Mutex::new(vec![S::zero(); global_n]);
3917 apply_replicated_full_ilu_owned_segment(
3918 &replicated_ilu,
3919 &comm,
3920 PcSide::Right,
3921 &x_local,
3922 &mut y_local,
3923 &mut scratch,
3924 global_n,
3925 row_start,
3926 local_n,
3927 &scratch_in,
3928 &scratch_out,
3929 )
3930 .expect("replicated ILU apply");
3931
3932 let expected_local = &expected_global[row_start..row_end];
3933 assert_eq!(y_local.len(), expected_local.len());
3934 for (got, expected) in y_local.iter().zip(expected_local.iter()) {
3935 assert!(
3936 (*got - *expected).abs() <= 1.0e-12,
3937 "rank {rank} replicated apply mismatch: got={got:?}, expected={expected:?}"
3938 );
3939 }
3940 }
3941
3942 #[test]
3943 fn replicated_full_ilu0_setup_helper_uses_global_dimensions_for_shifted_poisson() {
3944 #[cfg(feature = "mpi")]
3945 let comm = UniverseComm::Mpi(Arc::new(MpiComm::new()));
3946 #[cfg(not(feature = "mpi"))]
3947 let comm = UniverseComm::NoComm(NoComm);
3948
3949 let problem = build_shifted_poisson_case(&comm, 5, 5, 1.0, 0.25, 0.0, 0.0)
3950 .expect("build generated shifted Poisson problem");
3951 assert_eq!(problem.global_n, 25);
3952 assert_eq!(
3953 problem.global_n % 4,
3954 1,
3955 "test matrix should not divide evenly over 4 ranks"
3956 );
3957 assert_eq!(problem.csr_for_pc.nrows(), problem.local_n);
3958 assert_eq!(problem.csr_for_pc.ncols(), problem.local_n);
3959 assert_eq!(problem.global_csr.nrows(), problem.global_n);
3960 assert_eq!(problem.global_csr.ncols(), problem.global_n);
3961
3962 let spec = RunSpec {
3963 restart: 5,
3964 ksp: KspKind::Gmres,
3965 variant: FgmresVariant::Classical,
3966 residual_check_policy: ResidualCheckPolicy::OnConvergence,
3967 orthog: OrthogMethod::ClassicalGS,
3968 reorth: ReorthPolicy::IfNeeded,
3969 pc_side: PcSide::Right,
3970 pc: PcKind::ReplicatedFullIlu0,
3971 };
3972 let bench_cfg = BenchmarkConfig {
3973 run_mode: RunMode::Scalability,
3974 warmup_runs: 0,
3975 measured_runs: 0,
3976 ..BenchmarkConfig::default()
3977 };
3978 let pc_csr = problem.csr_for_pc.clone();
3979 let global_pc_csr = problem.global_csr.clone();
3980 let csr_pc_diag = csr_for_pc_diagnostics(
3981 pc_csr.as_ref(),
3982 problem.local_rows_nnz,
3983 problem.zero_global_rows_local,
3984 );
3985
3986 let pc = setup_preconditioner_for_run_once(
3987 &problem,
3988 &spec,
3989 &bench_cfg,
3990 &pc_csr,
3991 &global_pc_csr,
3992 &csr_pc_diag,
3993 )
3994 .expect("replicated full ILU0 setup through run_once helper")
3995 .expect("replicated full ILU0 preconditioner handle");
3996
3997 assert!(
3998 matches!(pc, PcHandle::ReplicatedFull { .. }),
3999 "expected replicated full preconditioner handle"
4000 );
4001 assert_eq!(
4002 KPreconditioner::dims(&pc),
4003 (problem.global_n, problem.global_n)
4004 );
4005
4006 let x_local = (0..problem.local_n)
4007 .map(|i| S::from_parts(1.0 + (problem.global_row_start + i) as f64, -0.25))
4008 .collect::<Vec<_>>();
4009 let mut y_local = vec![S::zero(); problem.local_n];
4010 let mut scratch = BridgeScratch::default();
4011 pc.apply_s(PcSide::Right, &x_local, &mut y_local, &mut scratch)
4012 .expect(
4013 "replicated full ILU0 applies to owned local vector without dimension mismatch",
4014 );
4015 }
4016
4017 #[test]
4018 fn replicated_full_ilu_dispatches_separately_from_local_ilu() {
4019 assert_eq!(
4020 PcKind::ReplicatedFullIlu0.dispatch_branch(),
4021 PcDispatchBranch::ReplicatedFullIlu
4022 );
4023 assert_eq!(
4024 PcKind::ReplicatedFullIluk { k: 2 }.dispatch_branch(),
4025 PcDispatchBranch::ReplicatedFullIlu
4026 );
4027 assert_eq!(
4028 PcKind::Ilu0Local.dispatch_branch(),
4029 PcDispatchBranch::Ilu0Local
4030 );
4031 assert_eq!(
4032 PcKind::MpiBlockJacobiIlu0Local.dispatch_branch(),
4033 PcDispatchBranch::Ilu0Local
4034 );
4035 assert_eq!(
4036 PcKind::LocalIluk { k: 1 }.dispatch_branch(),
4037 PcDispatchBranch::Ilu0Local
4038 );
4039 assert_eq!(
4040 PcKind::IlutLocal.dispatch_branch(),
4041 PcDispatchBranch::IlutLocal
4042 );
4043 }
4044
4045 #[test]
4046 fn replicated_full_ilu_uses_global_csr_even_when_local_pc_block_is_rectangular() {
4047 #[cfg(feature = "mpi")]
4048 let comm = UniverseComm::Mpi(Arc::new(MpiComm::new()));
4049 #[cfg(not(feature = "mpi"))]
4050 let comm = UniverseComm::NoComm(NoComm);
4051
4052 let op_local = SparseCsrMatrix::from_csr(
4053 2,
4054 2,
4055 vec![0, 1, 2],
4056 vec![0, 1],
4057 vec![S::from_parts(2.0, 0.0), S::from_parts(3.0, 0.0)],
4058 );
4059 let row_part = vec![0, 2];
4060 let op = DistCsrOp::from_local_rows(2, 0, &op_local, &row_part, comm.clone())
4061 .expect("build local dist op");
4062
4063 let rectangular_pc = SparseCsrMatrix::from_csr(
4064 2,
4065 3,
4066 vec![0, 1, 2],
4067 vec![0, 1],
4068 vec![S::from_parts(2.0, 0.0), S::from_parts(3.0, 0.0)],
4069 );
4070 let problem = Problem {
4071 op: Arc::new(op),
4072 dist_plan_diagnostics: DistributedPlanDiagnostics {
4073 overlap_mode: kryst::matrix::dist_csr::HaloOverlapMode::Disabled,
4074 kernel_strategy: kryst::matrix::dist_csr::DistLocalKernelStrategy::RowSplitScalar,
4075 local_spmv_kernel: None,
4076 row_locality_ratio: 1.0,
4077 border_ratio: 0.0,
4078 halo_recv_volume: 0,
4079 halo_send_volume: 0,
4080 expected_communication_fraction: 0.0,
4081 expected_computation_fraction: 1.0,
4082 },
4083 rhs: vec![S::from_parts(2.0, 0.0), S::from_parts(3.0, 0.0)],
4084 rhs_source: RhsSource::GeneratedAOnes,
4085 solution_reference: solution_reference_diagnostics(&op_local),
4086 csr_for_pc: Arc::new(rectangular_pc),
4087 global_csr: Arc::new(op_local.clone()),
4088 local_rows_nnz: op_local.values().len(),
4089 zero_global_rows_local: count_zero_rows(&op_local),
4090 local_n: 2,
4091 global_n: 2,
4092 global_row_start: 0,
4093 comm,
4094 backend: CsrBackend::Serial,
4095 backend_descr: "unit-test".to_string(),
4096 generated_case: false,
4097 };
4098 let spec = RunSpec {
4099 restart: 5,
4100 ksp: KspKind::Gmres,
4101 variant: FgmresVariant::Classical,
4102 residual_check_policy: ResidualCheckPolicy::OnConvergence,
4103 orthog: OrthogMethod::ClassicalGS,
4104 reorth: ReorthPolicy::IfNeeded,
4105 pc_side: PcSide::Right,
4106 pc: PcKind::ReplicatedFullIlu0,
4107 };
4108 let bench_cfg = BenchmarkConfig {
4109 warmup_runs: 0,
4110 measured_runs: 1,
4111 maxits: 10,
4112 ..BenchmarkConfig::default()
4113 };
4114
4115 run_once(&problem, &spec, &bench_cfg)
4116 .expect("replicated full ILU should set up from global CSR, not local PC CSR");
4117 }
4118
4119 #[test]
4120 fn local_ilu_rejects_rectangular_owned_block() {
4121 #[cfg(feature = "mpi")]
4122 let comm = UniverseComm::Mpi(Arc::new(MpiComm::new()));
4123 #[cfg(not(feature = "mpi"))]
4124 let comm = UniverseComm::NoComm(NoComm);
4125
4126 let op_local =
4127 SparseCsrMatrix::from_csr(2, 2, vec![0, 1, 2], vec![0, 1], vec![S::one(), S::one()]);
4128 let row_part = vec![0, 2];
4129 let op = DistCsrOp::from_local_rows(2, 0, &op_local, &row_part, comm.clone())
4130 .expect("build local dist op");
4131
4132 let rectangular_pc =
4133 SparseCsrMatrix::from_csr(2, 3, vec![0, 1, 2], vec![0, 1], vec![S::one(), S::one()]);
4134 let problem = Problem {
4135 op: Arc::new(op),
4136 dist_plan_diagnostics: DistributedPlanDiagnostics {
4137 overlap_mode: kryst::matrix::dist_csr::HaloOverlapMode::Disabled,
4138 kernel_strategy: kryst::matrix::dist_csr::DistLocalKernelStrategy::RowSplitScalar,
4139 local_spmv_kernel: None,
4140 row_locality_ratio: 1.0,
4141 border_ratio: 0.0,
4142 halo_recv_volume: 0,
4143 halo_send_volume: 0,
4144 expected_communication_fraction: 0.0,
4145 expected_computation_fraction: 1.0,
4146 },
4147 rhs: vec![S::one(), S::one()],
4148 rhs_source: RhsSource::GeneratedAOnes,
4149 solution_reference: solution_reference_diagnostics(&rectangular_pc),
4150 csr_for_pc: Arc::new(rectangular_pc.clone()),
4151 global_csr: Arc::new(op_local.clone()),
4152 local_rows_nnz: op_local.values().len(),
4153 zero_global_rows_local: count_zero_rows(&op_local),
4154 local_n: 2,
4155 global_n: 2,
4156 global_row_start: 0,
4157 comm,
4158 backend: CsrBackend::Serial,
4159 backend_descr: "unit-test".to_string(),
4160 generated_case: false,
4161 };
4162 let spec = RunSpec {
4163 restart: 10,
4164 ksp: KspKind::Gmres,
4165 variant: FgmresVariant::Classical,
4166 residual_check_policy: ResidualCheckPolicy::OnConvergence,
4167 orthog: OrthogMethod::ClassicalGS,
4168 reorth: ReorthPolicy::IfNeeded,
4169 pc_side: PcSide::Right,
4170 pc: PcKind::Ilu0Local,
4171 };
4172 let bench_cfg = BenchmarkConfig {
4173 warmup_runs: 0,
4174 measured_runs: 1,
4175 ..BenchmarkConfig::default()
4176 };
4177
4178 match run_once(&problem, &spec, &bench_cfg) {
4179 Err(KError::InvalidInput(msg)) => {
4180 assert_eq!(msg, "local ILU requires square owned block");
4181 }
4182 Err(other) => panic!("unexpected error variant: {other:?}"),
4183 Ok(_) => panic!("expected rectangular ILU error"),
4184 }
4185 }
4186
4187 #[test]
4188 fn run_once_reports_requested_and_effective_policy_after_dist_policy_mutation() {
4189 #[cfg(feature = "mpi")]
4190 let comm = UniverseComm::Mpi(Arc::new(MpiComm::new()));
4191 #[cfg(not(feature = "mpi"))]
4192 let comm = UniverseComm::NoComm(NoComm);
4193
4194 let op_local =
4195 SparseCsrMatrix::from_csr(2, 2, vec![0, 1, 2], vec![0, 1], vec![S::one(), S::one()]);
4196 let row_part = vec![0, 2];
4197 let op = DistCsrOp::from_local_rows(2, 0, &op_local, &row_part, comm.clone())
4198 .expect("build local dist op");
4199
4200 let problem = Problem {
4201 op: Arc::new(op),
4202 dist_plan_diagnostics: DistributedPlanDiagnostics {
4203 overlap_mode: kryst::matrix::dist_csr::HaloOverlapMode::Disabled,
4204 kernel_strategy: kryst::matrix::dist_csr::DistLocalKernelStrategy::RowSplitScalar,
4205 local_spmv_kernel: None,
4206 row_locality_ratio: 1.0,
4207 border_ratio: 0.0,
4208 halo_recv_volume: 0,
4209 halo_send_volume: 0,
4210 expected_communication_fraction: 0.1,
4211 expected_computation_fraction: 0.5,
4212 },
4213 rhs: vec![S::one(), S::one()],
4214 rhs_source: RhsSource::GeneratedAOnes,
4215 solution_reference: solution_reference_diagnostics(&op_local),
4216 csr_for_pc: Arc::new(op_local.clone()),
4217 global_csr: Arc::new(op_local.clone()),
4218 local_rows_nnz: 2,
4219 zero_global_rows_local: count_zero_rows(&op_local),
4220 local_n: 2,
4221 global_n: 2,
4222 global_row_start: 0,
4223 comm,
4224 backend: CsrBackend::Serial,
4225 backend_descr: "unit-test".to_string(),
4226 generated_case: false,
4227 };
4228 let spec = RunSpec {
4229 restart: 5,
4230 ksp: KspKind::Fgmres,
4231 variant: FgmresVariant::Classical,
4232 residual_check_policy: ResidualCheckPolicy::OnConvergence,
4233 orthog: OrthogMethod::ClassicalGS,
4234 reorth: ReorthPolicy::IfNeeded,
4235 pc_side: PcSide::Right,
4236 pc: PcKind::None,
4237 };
4238 let bench_cfg = BenchmarkConfig {
4239 warmup_runs: 0,
4240 measured_runs: 1,
4241 ksp: KspKind::Fgmres,
4242 dist_policy: DistPolicyMode::Auto,
4243 ..BenchmarkConfig::default()
4244 };
4245
4246 let row = run_once(&problem, &spec, &bench_cfg).expect("run once");
4247 assert!(row.requested_policy.contains("restart=5"));
4248 assert!(
4249 row.requested_policy
4250 .contains("residual-check=on-convergence")
4251 );
4252 assert!(row.effective_policy.contains("restart=16"));
4253 assert!(
4254 row.effective_policy
4255 .contains("residual-check=every-iteration")
4256 );
4257
4258 let rendered = render_result_row(&row, RunMode::Correctness, &problem);
4259 assert!(rendered.contains(&row.requested_policy));
4260 assert!(rendered.contains(&row.effective_policy));
4261 assert_eq!(row.pc_apply, "none");
4262 assert_eq!(row.dist_ok, Some(true));
4263 assert_eq!(row.global_ok, Some(true));
4264 assert_eq!(row.x_ok, Some(true));
4265 assert!(rendered.contains("OK OK OK"));
4266 assert!(rendered.contains("csr-cx"));
4267 assert!(rendered.contains("ser"));
4268 assert!(rendered.contains("none"));
4269 }
4270
4271 #[test]
4272 fn generated_aones_empty_row_suppresses_x_error_rel() {
4273 #[cfg(feature = "mpi")]
4274 let comm = UniverseComm::Mpi(Arc::new(MpiComm::new()));
4275 #[cfg(not(feature = "mpi"))]
4276 let comm = UniverseComm::NoComm(NoComm);
4277
4278 let op_local = SparseCsrMatrix::from_csr(2, 2, vec![0, 1, 1], vec![0], vec![S::one()]);
4279 let row_part = vec![0, 2];
4280 let op = DistCsrOp::from_local_rows(2, 0, &op_local, &row_part, comm.clone())
4281 .expect("build local dist op");
4282 let diagnostics = solution_reference_diagnostics(&op_local);
4283 assert_eq!(diagnostics.zero_row_count, 1);
4284 assert_eq!(diagnostics.missing_diagonal_count, 1);
4285 assert!(!diagnostics.valid_for_x_error());
4286
4287 let problem = Problem {
4288 op: Arc::new(op),
4289 dist_plan_diagnostics: DistributedPlanDiagnostics {
4290 overlap_mode: kryst::matrix::dist_csr::HaloOverlapMode::Disabled,
4291 kernel_strategy: kryst::matrix::dist_csr::DistLocalKernelStrategy::RowSplitScalar,
4292 local_spmv_kernel: None,
4293 row_locality_ratio: 1.0,
4294 border_ratio: 0.0,
4295 halo_recv_volume: 0,
4296 halo_send_volume: 0,
4297 expected_communication_fraction: 0.0,
4298 expected_computation_fraction: 1.0,
4299 },
4300 rhs: vec![S::one(), S::zero()],
4301 rhs_source: RhsSource::GeneratedAOnes,
4302 solution_reference: diagnostics,
4303 csr_for_pc: Arc::new(op_local.clone()),
4304 global_csr: Arc::new(op_local.clone()),
4305 local_rows_nnz: 1,
4306 zero_global_rows_local: count_zero_rows(&op_local),
4307 local_n: 2,
4308 global_n: 2,
4309 global_row_start: 0,
4310 comm,
4311 backend: CsrBackend::Serial,
4312 backend_descr: "unit-test".to_string(),
4313 generated_case: false,
4314 };
4315 let spec = RunSpec {
4316 restart: 5,
4317 ksp: KspKind::Gmres,
4318 variant: FgmresVariant::Classical,
4319 residual_check_policy: ResidualCheckPolicy::OnConvergence,
4320 orthog: OrthogMethod::ClassicalGS,
4321 reorth: ReorthPolicy::IfNeeded,
4322 pc_side: PcSide::Right,
4323 pc: PcKind::None,
4324 };
4325 let bench_cfg = BenchmarkConfig {
4326 run_mode: RunMode::Correctness,
4327 warmup_runs: 0,
4328 measured_runs: 1,
4329 ..BenchmarkConfig::default()
4330 };
4331
4332 let row = run_once(&problem, &spec, &bench_cfg).expect("run once");
4333 assert_eq!(problem.rhs_source, RhsSource::GeneratedAOnes);
4334 assert_eq!(row.x_error_rel, None);
4335 assert_eq!(row.global_x_error_rel, None);
4336 assert_eq!(row.x_ok, None);
4337 let rendered = render_result_row(&row, RunMode::Correctness, &problem);
4338 assert!(rendered.contains("OK OK N/A"));
4339 }
4340
4341 #[test]
4342 fn correctness_default_dist_policy_off_preserves_requested_restart() {
4343 #[cfg(feature = "mpi")]
4344 let comm = UniverseComm::Mpi(Arc::new(MpiComm::new()));
4345 #[cfg(not(feature = "mpi"))]
4346 let comm = UniverseComm::NoComm(NoComm);
4347
4348 let op_local =
4349 SparseCsrMatrix::from_csr(2, 2, vec![0, 1, 2], vec![0, 1], vec![S::one(), S::one()]);
4350 let row_part = vec![0, 2];
4351 let op = DistCsrOp::from_local_rows(2, 0, &op_local, &row_part, comm.clone())
4352 .expect("build local dist op");
4353
4354 let problem = Problem {
4355 op: Arc::new(op),
4356 dist_plan_diagnostics: DistributedPlanDiagnostics {
4357 overlap_mode: kryst::matrix::dist_csr::HaloOverlapMode::Disabled,
4358 kernel_strategy: kryst::matrix::dist_csr::DistLocalKernelStrategy::RowSplitScalar,
4359 local_spmv_kernel: None,
4360 row_locality_ratio: 1.0,
4361 border_ratio: 0.0,
4362 halo_recv_volume: 0,
4363 halo_send_volume: 0,
4364 expected_communication_fraction: 0.1,
4365 expected_computation_fraction: 0.5,
4366 },
4367 rhs: vec![S::one(), S::one()],
4368 rhs_source: RhsSource::GeneratedAOnes,
4369 solution_reference: solution_reference_diagnostics(&op_local),
4370 csr_for_pc: Arc::new(op_local.clone()),
4371 global_csr: Arc::new(op_local.clone()),
4372 local_rows_nnz: 2,
4373 zero_global_rows_local: count_zero_rows(&op_local),
4374 local_n: 2,
4375 global_n: 2,
4376 global_row_start: 0,
4377 comm,
4378 backend: CsrBackend::Serial,
4379 backend_descr: "unit-test".to_string(),
4380 generated_case: false,
4381 };
4382 let spec = RunSpec {
4383 restart: 5,
4384 ksp: KspKind::Gmres,
4385 variant: FgmresVariant::Classical,
4386 residual_check_policy: ResidualCheckPolicy::OnConvergence,
4387 orthog: OrthogMethod::ClassicalGS,
4388 reorth: ReorthPolicy::IfNeeded,
4389 pc_side: PcSide::Right,
4390 pc: PcKind::None,
4391 };
4392 let bench_cfg = BenchmarkConfig {
4393 run_mode: RunMode::Correctness,
4394 warmup_runs: 0,
4395 measured_runs: 1,
4396 ..BenchmarkConfig::default()
4397 };
4398
4399 assert_eq!(bench_cfg.dist_policy, DistPolicyMode::Off);
4400 let row = run_once(&problem, &spec, &bench_cfg).expect("run once");
4401 assert!(row.requested_policy.contains("restart=5"));
4402 assert!(row.effective_policy.contains("restart=5"));
4403 assert!(
4404 row.effective_policy
4405 .contains("residual-check=on-convergence")
4406 );
4407 }
4408
4409 #[test]
4410 fn metadata_labels_snapshot_serial() {
4411 assert_eq!(
4412 pc_domain_label_for_size(PcKind::ReplicatedFullIlu0, 1),
4413 "full"
4414 );
4415 assert_eq!(
4416 pc_domain_label_for_size(PcKind::ReplicatedFullIluk { k: 1 }, 1),
4417 "full"
4418 );
4419 assert_eq!(pc_domain_label_for_size(PcKind::Ilu0Local, 1), "full");
4420 assert_eq!(pc_domain_label_for_size(PcKind::IlutLocal, 1), "full");
4421 assert_eq!(
4422 pc_domain_label_for_size(PcKind::LocalIluk { k: 1 }, 1),
4423 "full"
4424 );
4425 assert_eq!(
4426 pc_domain_label_for_size(PcKind::MpiBlockJacobiIlu0Local, 1),
4427 "own0"
4428 );
4429 assert_eq!(pc_domain_label_for_size(PcKind::None, 1), "n/a");
4430 assert_eq!(pc_domain_label_for_size(PcKind::JacobiWeak, 1), "n/a");
4431 assert_eq!(
4432 pc_apply_label_for_size(PcKind::ReplicatedFullIlu0, 1),
4433 "replicated/full"
4434 );
4435 }
4436
4437 #[test]
4438 fn metadata_labels_snapshot_mpi() {
4439 assert_eq!(
4440 pc_domain_label_for_size(PcKind::ReplicatedFullIlu0, 2),
4441 "full"
4442 );
4443 assert_eq!(
4444 pc_apply_label_for_size(PcKind::ReplicatedFullIlu0, 2),
4445 "replicated/full/allgather, not scalable"
4446 );
4447 assert_eq!(
4448 pc_domain_label_for_size(PcKind::ReplicatedFullIluk { k: 1 }, 2),
4449 "full"
4450 );
4451 assert_eq!(
4452 pc_apply_label_for_size(PcKind::ReplicatedFullIluk { k: 1 }, 2),
4453 "replicated/full/allgather, not scalable"
4454 );
4455 assert_eq!(pc_domain_label_for_size(PcKind::Ilu0Local, 2), "own0");
4456 assert_eq!(pc_domain_label_for_size(PcKind::IlutLocal, 2), "own0");
4457 assert_eq!(
4458 pc_domain_label_for_size(PcKind::LocalIluk { k: 1 }, 2),
4459 "own0"
4460 );
4461 assert_eq!(
4462 pc_domain_label_for_size(PcKind::MpiBlockJacobiIlu0Local, 2),
4463 "own0"
4464 );
4465 assert_eq!(
4466 pc_apply_label_for_size(PcKind::Ilu0Local, 2),
4467 "owned-block/overlap0"
4468 );
4469 assert_eq!(
4470 pc_domain_label_for_size(PcKind::AsmIlu0Overlap { overlap: 1 }, 2),
4471 "own+ghost1"
4472 );
4473 assert_eq!(
4474 pc_domain_label_for_size(PcKind::AsmIlu0Overlap { overlap: 2 }, 2),
4475 "own+ghost2"
4476 );
4477 assert_eq!(
4478 pc_apply_label_for_size(PcKind::AsmIlu0Overlap { overlap: 1 }, 2),
4479 "asm/overlap-gather/owned-output"
4480 );
4481 assert_eq!(
4482 pc_apply_label_for_size(PcKind::RasIlu0Overlap { overlap: 1 }, 2),
4483 "ras/overlap-gather/restricted-owned"
4484 );
4485 assert_eq!(pc_domain_label_for_size(PcKind::None, 2), "n/a");
4486 assert_eq!(pc_apply_label_for_size(PcKind::None, 2), "none");
4487 assert_eq!(
4488 pc_apply_label_for_size(PcKind::JacobiWeak, 2),
4489 "jacobi/local"
4490 );
4491 }
4492
4493 #[test]
4494 fn row_scaled_mode_scales_pc_setup_matrix_rows() {
4495 let pc = SparseCsrMatrix::from_csr(
4496 2,
4497 2,
4498 vec![0, 2, 4],
4499 vec![0, 1, 0, 1],
4500 vec![
4501 S::new(2.0, 0.0),
4502 S::new(0.0, 0.0),
4503 S::new(0.0, 0.0),
4504 S::new(4.0, 0.0),
4505 ],
4506 );
4507 let row_scale = compute_row_scaling(&pc, 1e-15);
4508 let pc_scaled = scale_csr_rows(&pc, &row_scale);
4509
4510 assert_eq!(row_scale, vec![0.5, 0.25]);
4511 assert_eq!(pc_scaled.values()[0], S::one());
4512 assert_eq!(pc_scaled.values()[3], S::one());
4513 assert_ne!(pc_scaled.values(), pc.values());
4514 }
4515
4516 #[test]
4517 fn mpi_mode_run_matrix_has_no_duplicate_semantic_pc_experiments() {
4518 let cfg = BenchmarkConfig::default();
4519 let mpi_mode = true;
4520 let pcs = if cfg.pcs.is_empty() {
4521 match cfg.run_mode {
4522 RunMode::Correctness => {
4523 if mpi_mode {
4524 vec![
4525 PcKind::ReplicatedFullIlu0,
4526 PcKind::None,
4527 PcKind::JacobiWeak,
4528 PcKind::MpiBlockJacobiIlu0Local,
4529 ]
4530 } else {
4531 vec![PcKind::Ilu0Local, PcKind::JacobiWeak, PcKind::None]
4532 }
4533 }
4534 RunMode::Scalability => {
4535 if mpi_mode {
4536 vec![PcKind::MpiBlockJacobiIlu0Local]
4537 } else {
4538 vec![PcKind::Ilu0Local]
4539 }
4540 }
4541 RunMode::Robustness => {
4542 if mpi_mode {
4543 vec![PcKind::MpiBlockJacobiIlu0Local, PcKind::JacobiWeak]
4544 } else {
4545 vec![PcKind::Ilu0Local, PcKind::JacobiWeak]
4546 }
4547 }
4548 }
4549 } else {
4550 cfg.pcs.clone()
4551 };
4552
4553 assert_eq!(
4554 pcs,
4555 vec![
4556 PcKind::ReplicatedFullIlu0,
4557 PcKind::None,
4558 PcKind::JacobiWeak,
4559 PcKind::MpiBlockJacobiIlu0Local,
4560 ]
4561 );
4562
4563 let mut seen = std::collections::BTreeSet::new();
4564 for &restart in &cfg.restarts {
4565 for &variant in &cfg.variants {
4566 for &orthog in &cfg.orthogs {
4567 for &reorth in &cfg.reorths {
4568 for &pc in &pcs {
4569 let key = format!(
4570 "restart={restart}|variant={}|orthog={}|reorth={}|pc={}",
4571 variant_label(variant),
4572 orthog_label(orthog),
4573 reorth_label(reorth),
4574 pc.semantic_experiment_key(mpi_mode)
4575 );
4576 assert!(
4577 seen.insert(key.clone()),
4578 "duplicate semantic benchmark row: {key}"
4579 );
4580 }
4581 }
4582 }
4583 }
4584 }
4585 }
4586
4587 #[test]
4588 fn parse_pc_accepts_documented_sweep_names() {
4589 assert_eq!(
4590 parse_pc_csv(
4591 "--pcs",
4592 "replicated-full-ilu0,none,jacobi,block-jacobi-ilu0"
4593 )
4594 .expect("parse documented correctness preconditioners"),
4595 vec![
4596 PcKind::ReplicatedFullIlu0,
4597 PcKind::None,
4598 PcKind::JacobiWeak,
4599 PcKind::MpiBlockJacobiIlu0Local,
4600 ]
4601 );
4602 assert_eq!(
4603 parse_pc_csv("--pcs", "none,jacobi,block-jacobi-ilu0")
4604 .expect("parse documented scalability preconditioners"),
4605 vec![
4606 PcKind::None,
4607 PcKind::JacobiWeak,
4608 PcKind::MpiBlockJacobiIlu0Local,
4609 ]
4610 );
4611 }
4612
4613 #[test]
4614 fn parse_pc_accepts_replicated_full_ilu0_alias() {
4615 assert_eq!(
4616 parse_pc("replicated-full-ilu0").expect("parse replicated-full-ilu0"),
4617 PcKind::ReplicatedFullIlu0
4618 );
4619 assert_eq!(
4620 parse_pc("replicated-ilu0").expect("parse replicated-ilu0"),
4621 PcKind::ReplicatedFullIlu0
4622 );
4623 }
4624
4625 #[test]
4626 fn parse_pc_accepts_block_jacobi_iluk_aliases() {
4627 assert_eq!(
4628 parse_pc("block-jacobi-iluk1").expect("parse block-jacobi-iluk1"),
4629 PcKind::LocalIluk { k: 1 }
4630 );
4631 assert_eq!(
4632 parse_pc("block-jacobi-iluk1-overlap0").expect("parse block-jacobi-iluk1-overlap0"),
4633 PcKind::LocalIluk { k: 1 }
4634 );
4635 assert_eq!(
4636 parse_pc("block-jacobi-iluk:2").expect("parse block-jacobi-iluk:2"),
4637 PcKind::LocalIluk { k: 2 }
4638 );
4639 assert_eq!(
4640 parse_pc("block-jacobi-iluk3-overlap0").expect("parse block-jacobi-iluk3-overlap0"),
4641 PcKind::LocalIluk { k: 3 }
4642 );
4643 }
4644
4645 #[test]
4646 fn parse_pc_accepts_overlap_aliases() {
4647 assert_eq!(
4648 parse_pc("asm-ilu0-overlap1").expect("parse asm-ilu0-overlap1"),
4649 PcKind::AsmIlu0Overlap { overlap: 1 }
4650 );
4651 assert_eq!(
4652 parse_pc("asm-ilu0-overlap2").expect("parse asm-ilu0-overlap2"),
4653 PcKind::AsmIlu0Overlap { overlap: 2 }
4654 );
4655 assert_eq!(
4656 parse_pc("ras-ilu0-overlap1").expect("parse ras-ilu0-overlap1"),
4657 PcKind::RasIlu0Overlap { overlap: 1 }
4658 );
4659 assert_eq!(
4660 parse_pc("ras-iluk1-overlap1").expect("parse ras-iluk1-overlap1"),
4661 PcKind::RasIlukOverlap { k: 1, overlap: 1 }
4662 );
4663 }
4664
4665 #[test]
4666 fn local_iluk_label_renders_block_jacobi_alias() {
4667 assert_eq!(
4668 PcKind::LocalIluk { k: 3 }.label(),
4669 "block-jacobi-iluk3-overlap0"
4670 );
4671 }
4672
4673 #[test]
4674 fn preconditioner_dispatch_variants_are_distinct_or_explicit_aliases() {
4675 let variants = [
4676 PcKind::None,
4677 PcKind::JacobiWeak,
4678 PcKind::Ilu0Local,
4679 PcKind::IlutLocal,
4680 PcKind::MpiBlockJacobiIlu0Local,
4681 ];
4682
4683 for (idx, left) in variants.iter().copied().enumerate() {
4684 for right in variants.iter().copied().skip(idx + 1) {
4685 if left.dispatch_branch() == right.dispatch_branch() {
4686 let explicit_alias = left.explicit_alias_of() == Some(right)
4687 || right.explicit_alias_of() == Some(left);
4688 assert!(
4689 explicit_alias,
4690 "dispatch collision without alias marker: {left:?} vs {right:?}"
4691 );
4692 }
4693 }
4694 }
4695 }
4696
4697 #[test]
4698 fn method_label_distinguishes_gmres_and_fgmres() {
4699 let base = RunSpec {
4700 restart: 50,
4701 ksp: KspKind::Gmres,
4702 variant: FgmresVariant::Classical,
4703 residual_check_policy: ResidualCheckPolicy::OnConvergence,
4704 orthog: OrthogMethod::ClassicalGS,
4705 reorth: ReorthPolicy::IfNeeded,
4706 pc_side: PcSide::Right,
4707 pc: PcKind::JacobiWeak,
4708 };
4709 assert!(base.method_label().starts_with("GMRES+"));
4710
4711 let flexible = RunSpec {
4712 ksp: KspKind::Fgmres,
4713 ..base
4714 };
4715 assert!(flexible.method_label().starts_with("FGMRES+"));
4716 }
4717
4718 #[test]
4719 fn fixed_pc_default_run_specs_select_gmres() {
4720 #[cfg(feature = "mpi")]
4721 let comm = UniverseComm::Mpi(Arc::new(MpiComm::new()));
4722 #[cfg(not(feature = "mpi"))]
4723 let comm = UniverseComm::NoComm(NoComm);
4724
4725 let problem = build_shifted_poisson_case(&comm, 4, 4, 1.0, 0.25, 0.0, 0.0)
4726 .expect("build generated shifted Poisson problem");
4727 let cfg = BenchmarkConfig {
4728 restarts: vec![8],
4729 pcs: vec![PcKind::None, PcKind::JacobiWeak, PcKind::Ilu0Local],
4730 ..BenchmarkConfig::default()
4731 };
4732 let runs = RunSpec::build_default_matrix(&cfg, &problem);
4733 assert_eq!(runs.len(), 3);
4734 assert!(runs.iter().all(|spec| spec.ksp == KspKind::Gmres));
4735 }
4736
4737 #[test]
4738 fn ilut_complex_label_explicitly_marks_real_projection_fallback_path() {
4739 let label = PcKind::IlutLocal.label();
4740 assert!(label.contains(ILUT_REAL_PROJECTION_FALLBACK_LABEL));
4741 assert!(label.contains("degraded/provisional"));
4742 assert!(label.contains("real-projection fallback"));
4743 }
4744
4745 #[test]
4746 fn ilut_method_row_and_export_labels_are_self_contained() {
4747 let spec = RunSpec {
4748 restart: 50,
4749 ksp: KspKind::Gmres,
4750 variant: FgmresVariant::Classical,
4751 residual_check_policy: ResidualCheckPolicy::OnConvergence,
4752 orthog: OrthogMethod::ClassicalGS,
4753 reorth: ReorthPolicy::IfNeeded,
4754 pc_side: PcSide::Right,
4755 pc: PcKind::IlutLocal,
4756 };
4757
4758 assert!(
4759 spec.method_label()
4760 .contains(ILUT_REAL_PROJECTION_FALLBACK_LABEL)
4761 );
4762 assert!(
4763 PcKind::IlutLocal
4764 .semantic_experiment_key(false)
4765 .contains("real-projection-fallback")
4766 );
4767 }
4768
4769 #[test]
4770 fn correctness_mode_disables_stagnation_fallback_and_keeps_restart_target() {
4771 let spec = RunSpec {
4772 restart: 50,
4773 ksp: KspKind::Gmres,
4774 variant: FgmresVariant::Classical,
4775 residual_check_policy: ResidualCheckPolicy::OnConvergence,
4776 orthog: OrthogMethod::ClassicalGS,
4777 reorth: ReorthPolicy::IfNeeded,
4778 pc_side: PcSide::Right,
4779 pc: PcKind::None,
4780 };
4781 let cfg = BenchmarkConfig {
4782 run_mode: RunMode::Correctness,
4783 allow_stagnation_fallback: true,
4784 min_inner_before_fallback: 12,
4785 ..BenchmarkConfig::default()
4786 };
4787 let solver = configured_fgmres_solver(&spec, &cfg);
4788 assert_eq!(solver.restart, 50);
4789 assert_eq!(solver.stagnation_policy, FgmresStagnationPolicy::Disabled);
4790 assert_eq!(solver.min_inner_before_fallback, 12);
4791 }
4792
4793 #[test]
4794 fn benchmark_config_defaults_dist_policy_from_run_mode() {
4795 let default_cfg = BenchmarkConfig::default();
4796 assert_eq!(default_cfg.dist_policy, DistPolicyMode::Off);
4797
4798 let scalability_cfg =
4799 BenchmarkConfig::from_args(vec!["--mode".to_string(), "scalability".to_string()])
4800 .expect("parse scalability mode");
4801 assert_eq!(scalability_cfg.dist_policy, DistPolicyMode::Auto);
4802
4803 let robustness_cfg =
4804 BenchmarkConfig::from_args(vec!["--mode".to_string(), "robustness".to_string()])
4805 .expect("parse robustness mode");
4806 assert_eq!(robustness_cfg.dist_policy, DistPolicyMode::Off);
4807 }
4808
4809 #[test]
4810 fn benchmark_config_parses_ksp_selection() {
4811 let gmres_cfg = BenchmarkConfig::from_args(vec!["--ksp".to_string(), "gmres".to_string()])
4812 .expect("parse --ksp gmres");
4813 assert_eq!(gmres_cfg.ksp, KspKind::Gmres);
4814
4815 let fgmres_cfg =
4816 BenchmarkConfig::from_args(vec!["--ksp".to_string(), "fgmres".to_string()])
4817 .expect("parse --ksp fgmres");
4818 assert_eq!(fgmres_cfg.ksp, KspKind::Fgmres);
4819 }
4820
4821 #[test]
4822 fn benchmark_config_rejects_invalid_ksp_selection() {
4823 let err = BenchmarkConfig::from_args(vec!["--ksp".to_string(), "bicgstab".to_string()])
4824 .expect_err("invalid --ksp should be rejected");
4825 match err {
4826 KError::InvalidInput(msg) => assert!(msg.contains("--ksp") || msg.contains("KSP")),
4827 other => panic!("unexpected error variant: {other:?}"),
4828 }
4829 }
4830
4831 #[test]
4832 fn benchmark_config_parses_explicit_dist_policy_override() {
4833 let cfg = BenchmarkConfig::from_args(vec![
4834 "--mode".to_string(),
4835 "scalability".to_string(),
4836 "--dist-policy".to_string(),
4837 "off".to_string(),
4838 ])
4839 .expect("parse explicit dist policy");
4840 assert_eq!(cfg.dist_policy, DistPolicyMode::Off);
4841 }
4842
4843 #[test]
4844 fn benchmark_config_parses_poisson_shift_and_convection_flags() {
4845 let cfg = BenchmarkConfig::from_args(vec![
4846 "--poisson-shift-real".to_string(),
4847 "0.01".to_string(),
4848 "--poisson-shift-imag".to_string(),
4849 "-0.5".to_string(),
4850 "--poisson-convection".to_string(),
4851 "1.25,-2.5".to_string(),
4852 ])
4853 .expect("parse shifted Poisson overrides");
4854
4855 assert_eq!(cfg.poisson_shift_real, 0.01);
4856 assert_eq!(cfg.poisson_shift_imag, -0.5);
4857 assert_eq!(cfg.poisson_convection_x, 1.25);
4858 assert_eq!(cfg.poisson_convection_y, -2.5);
4859 }
4860
4861 #[test]
4862 fn benchmark_config_rejects_invalid_poisson_convection_flag() {
4863 let err = BenchmarkConfig::from_args(vec![
4864 "--poisson-convection".to_string(),
4865 "1.0;2.0".to_string(),
4866 ])
4867 .expect_err("invalid convection pair should be rejected");
4868 match err {
4869 KError::InvalidInput(msg) => assert!(msg.contains("--poisson-convection")),
4870 other => panic!("unexpected error variant: {other:?}"),
4871 }
4872 }
4873
4874 #[test]
4875 fn benchmark_config_parses_block_jacobi_iluk_overlap_pc() {
4876 let cfg = BenchmarkConfig::from_args(vec![
4877 "--pcs".to_string(),
4878 "block-jacobi-iluk1-overlap0".to_string(),
4879 ])
4880 .expect("parse block Jacobi ILU(k) pc alias");
4881
4882 assert_eq!(cfg.pcs, vec![PcKind::LocalIluk { k: 1 }]);
4883 }
4884
4885 #[test]
4886 fn benchmark_config_uses_conservative_default_fgmres_haptol() {
4887 let cfg = BenchmarkConfig::default();
4888 assert_eq!(cfg.fgmres_haptol, 1e-30);
4889 }
4890
4891 #[test]
4892 fn benchmark_config_parses_fgmres_haptol_override() {
4893 let cfg =
4894 BenchmarkConfig::from_args(vec!["--fgmres-haptol".to_string(), "1e-22".to_string()])
4895 .expect("parse args");
4896 assert_eq!(cfg.fgmres_haptol, 1e-22);
4897 }
4898
4899 #[test]
4900 fn benchmark_config_rejects_non_positive_or_non_finite_fgmres_haptol() {
4901 for bad in ["0", "-1", "NaN", "inf"] {
4902 let err =
4903 BenchmarkConfig::from_args(vec!["--fgmres-haptol".to_string(), bad.to_string()])
4904 .expect_err("invalid --fgmres-haptol should be rejected");
4905 match err {
4906 KError::InvalidInput(msg) => assert!(msg.contains("--fgmres-haptol")),
4907 other => panic!("unexpected error variant: {other:?}"),
4908 }
4909 }
4910 }
4911
4912 #[test]
4913 fn benchmark_config_accepts_include_overlap_pcs_flag() {
4914 let cfg = BenchmarkConfig::from_args(vec!["--include-overlap-pcs".to_string()])
4915 .expect("parse args");
4916 assert!(cfg.include_overlap_pcs);
4917 }
4918
4919 #[test]
4920 fn include_overlap_pcs_is_limited_to_mpi_correctness_mode() {
4921 let correctness_cfg = BenchmarkConfig {
4922 include_overlap_pcs: true,
4923 run_mode: RunMode::Correctness,
4924 ..BenchmarkConfig::default()
4925 };
4926 assert!(should_include_overlap_pcs(&correctness_cfg, 4));
4927 assert!(!should_include_overlap_pcs(&correctness_cfg, 1));
4928
4929 let scalability_cfg = BenchmarkConfig {
4930 include_overlap_pcs: true,
4931 run_mode: RunMode::Scalability,
4932 ..BenchmarkConfig::default()
4933 };
4934 assert!(!should_include_overlap_pcs(&scalability_cfg, 4));
4935 }
4936
4937 #[test]
4938 fn include_overlap_pcs_appends_verdict_gated_mpi_rows_after_block_jacobi_baseline() {
4939 let mut pcs = vec![PcKind::ReplicatedFullIlu0, PcKind::None, PcKind::JacobiWeak];
4940 append_mpi_correctness_overlap_pcs(&mut pcs);
4941 assert_eq!(
4942 pcs,
4943 vec![
4944 PcKind::ReplicatedFullIlu0,
4945 PcKind::None,
4946 PcKind::JacobiWeak,
4947 PcKind::MpiBlockJacobiIlu0Local,
4948 PcKind::AsmIlu0Overlap { overlap: 1 },
4949 PcKind::RasIlu0Overlap { overlap: 1 },
4950 PcKind::RasIlukOverlap { k: 1, overlap: 1 },
4951 ]
4952 );
4953 }
4954
4955 #[test]
4956 fn overlap_pcs_dispatch_through_overlap_ilu_branch() {
4957 for pc in MPI_CORRECTNESS_OVERLAP_PCS {
4958 assert_eq!(pc.dispatch_branch(), PcDispatchBranch::OverlapIlu);
4959 }
4960 }
4961
4962 #[test]
4963 fn benchmark_config_accepts_mark_replicated_check_flag() {
4964 let cfg = BenchmarkConfig::from_args(vec!["--mark-replicated-check".to_string()])
4965 .expect("parse args");
4966 assert!(cfg.mark_replicated_check);
4967 }
4968
4969 #[test]
4970 fn benchmark_config_rejects_deprecated_correctness_replicated_check_flag() {
4971 let err = BenchmarkConfig::from_args(vec!["--correctness-replicated-check".to_string()])
4972 .expect_err("deprecated flag should be rejected");
4973 match err {
4974 KError::InvalidInput(msg) => {
4975 assert!(msg.contains("renamed to --mark-replicated-check"))
4976 }
4977 other => panic!("unexpected error variant: {other:?}"),
4978 }
4979 }
4980
4981 #[test]
4982 fn diagnostics_format_includes_global_and_rank_spread_fields() {
4983 let diag = CsrForPcDiagnosticsGlobal {
4984 nnz_local_block: rank_spread_from_locals(&[12.0, 30.0], 12.0),
4985 nnz_local_rows: rank_spread_from_locals(&[10.0, 20.0], 10.0),
4986 nnz_ratio: rank_spread_from_locals(&[1.2, 1.5], 1.2),
4987 zero_local_rows_rank_count: 1.0,
4988 zero_global_rows: rank_spread_from_locals(&[0.0, 2.0], 0.0),
4989 structural_diag_missing: rank_spread_from_locals(&[1.0, 3.0], 1.0),
4990 diag_min_abs: rank_spread_from_locals(&[1e-8, 1e-10], 1e-8),
4991 diag_max_abs: rank_spread_from_locals(&[9.0, 11.0], 9.0),
4992 diag_tiny_or_missing_count: rank_spread_from_locals(&[1.0, 4.0], 1.0),
4993 pivot_perturbation_count: Some(rank_spread_from_locals(&[0.0, 3.0], 0.0)),
4994 };
4995 let text = format_csr_for_pc_diagnostics("ilu0-local", &diag);
4996 assert!(text.contains("global_sum="));
4997 assert!(text.contains("rank_min="));
4998 assert!(text.contains("rank_max="));
4999 assert!(text.contains("rank0_local="));
5000 assert!(text.contains("nnz_ratio_global=1.400e0"));
5001 assert!(text.contains("rank_local_min=1.200e0"));
5002 assert!(text.contains("rank_local_max=1.500e0"));
5003 assert!(!text.contains("nnz_ratio:global_sum="));
5004 assert!(text.contains("zero_local_rows_rank_count=1"));
5005 assert!(text.contains("zero_global_rows:global_sum=2"));
5006 assert!(text.contains("structural_diag_missing:global_sum=4"));
5007 }
5008
5009 #[test]
5010 fn diagnostics_capture_nonzero_off_rank_contribution() {
5011 let spread = rank_spread_from_locals(&[5.0, 0.0, 7.0], 5.0);
5012 assert_eq!(spread.rank0_local, 5.0);
5013 assert_eq!(spread.global_sum, 12.0);
5014 assert!(spread.global_sum > spread.rank0_local);
5015 assert_eq!(spread.rank_max, 7.0);
5016 }
5017}
5018
5019#[cfg(feature = "complex")]
5020fn main() -> Result<(), KError> {
5021 complex_demo::run()
5022}