1use std::collections::VecDeque;
2use std::time::Instant;
3
4use hybit_core::{
5 l2_norm, HybitError, LinearOperator, MatrixBackend, Preconditioner, PreconditionerKind,
6 SolveReport, SolveStatus, SolverKind, SolverOptions,
7};
8use hybit_krylov::{
9 parallel_vector_worker_count, pcg_with_workspace, pcg_with_workspace_parallel_vectors,
10 KrylovOutcome, PcgWorkspace,
11};
12use hybit_matrix::{
13 analyze_csr32, AbtmConfig, AbtmMatrix, Csr32Matrix, DofMask, MatrixProfile,
14 ParallelCsr32Operator,
15};
16pub use hybit_precond::RigidBodyAggregation;
17use hybit_precond::{
18 recommend_rigid_body_aggregate_nodes, HybridPreconditioner, JacobiPreconditioner,
19 ParallelRigidBodyTwoLevelPreconditioner, RigidBodyTwoLevelBlockJacobiPreconditioner,
20};
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum BackendPolicy {
24 Auto,
25 Csr32,
26 Abtm,
27}
28
29#[derive(Clone, Copy, Debug)]
30pub struct HybridOptions {
31 pub enabled: bool,
32 pub probe_iterations: usize,
33 pub escalation_residual_ratio: f64,
34 pub coupling_risk_threshold: f64,
35 pub scale_jump_threshold: f64,
36 pub residual_seed_fraction: f64,
37 pub max_local_region_size: usize,
38 pub max_local_regions: usize,
39 pub overlap_layers: usize,
40}
41
42impl Default for HybridOptions {
43 fn default() -> Self {
44 Self {
45 enabled: true,
46 probe_iterations: 12,
47 escalation_residual_ratio: 0.50,
48 coupling_risk_threshold: 0.90,
49 scale_jump_threshold: 100.0,
50 residual_seed_fraction: 0.25,
51 max_local_region_size: 128,
52 max_local_regions: 8,
53 overlap_layers: 1,
54 }
55 }
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub enum StructuralSpmvPolicy {
61 Auto,
63 Serial,
65 Parallel,
67}
68
69pub const STRUCTURAL_PARALLEL_SPMV_MIN_NNZ: usize = 1_000_000;
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub enum StructuralPreconditionerPolicy {
76 Auto,
78 Serial,
80 Parallel,
82}
83
84pub const STRUCTURAL_PARALLEL_PRECONDITIONER_MIN_NNZ: usize = 1_000_000;
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub enum StructuralPcgVectorPolicy {
91 Auto,
94 Serial,
96 Parallel,
98}
99
100pub const STRUCTURAL_PARALLEL_PCG_VECTOR_MIN_N: usize = 131_072;
102pub const STRUCTURAL_PARALLEL_PCG_VECTOR_MIN_THREADS: usize = 4;
104
105#[derive(Clone, Copy, Debug)]
106pub struct StructuralOptions {
107 pub target_coarse_dimension: usize,
109 pub aggregation: RigidBodyAggregation,
111 pub spmv_policy: StructuralSpmvPolicy,
113 pub preconditioner_policy: StructuralPreconditionerPolicy,
115 pub pcg_vector_policy: StructuralPcgVectorPolicy,
117}
118
119impl Default for StructuralOptions {
120 fn default() -> Self {
121 Self {
122 target_coarse_dimension: 1536,
123 aggregation: RigidBodyAggregation::Auto,
124 spmv_policy: StructuralSpmvPolicy::Auto,
125 preconditioner_policy: StructuralPreconditionerPolicy::Auto,
126 pcg_vector_policy: StructuralPcgVectorPolicy::Auto,
127 }
128 }
129}
130
131impl StructuralOptions {
132 pub fn validate(&self) -> Result<(), HybitError> {
133 if self.target_coarse_dimension < 6 {
134 return Err(HybitError::InvalidArgument(
135 "target_coarse_dimension must be at least 6",
136 ));
137 }
138 Ok(())
139 }
140}
141
142impl HybridOptions {
143 pub fn validate(&self) -> Result<(), HybitError> {
144 if self.probe_iterations == 0 {
145 return Err(HybitError::InvalidArgument("probe_iterations must be > 0"));
146 }
147 if !self.escalation_residual_ratio.is_finite() || self.escalation_residual_ratio <= 0.0 {
148 return Err(HybitError::InvalidArgument(
149 "escalation_residual_ratio must be finite and > 0",
150 ));
151 }
152 if !self.coupling_risk_threshold.is_finite() || self.coupling_risk_threshold < 0.0 {
153 return Err(HybitError::InvalidArgument(
154 "coupling_risk_threshold must be finite and >= 0",
155 ));
156 }
157 if !self.scale_jump_threshold.is_finite() || self.scale_jump_threshold < 1.0 {
158 return Err(HybitError::InvalidArgument(
159 "scale_jump_threshold must be finite and >= 1",
160 ));
161 }
162 if !self.residual_seed_fraction.is_finite()
163 || self.residual_seed_fraction <= 0.0
164 || self.residual_seed_fraction > 1.0
165 {
166 return Err(HybitError::InvalidArgument(
167 "residual_seed_fraction must be in (0, 1]",
168 ));
169 }
170 if self.max_local_region_size == 0 || self.max_local_regions == 0 {
171 return Err(HybitError::InvalidArgument(
172 "local region limits must be > 0",
173 ));
174 }
175 if self.overlap_layers > 8 {
176 return Err(HybitError::InvalidArgument("overlap_layers must be <= 8"));
177 }
178 Ok(())
179 }
180}
181
182#[derive(Clone, Debug)]
183pub struct HybitSolver {
184 options: SolverOptions,
185 backend_policy: BackendPolicy,
186 hybrid_options: HybridOptions,
187 structural_options: StructuralOptions,
188}
189
190impl Default for HybitSolver {
191 fn default() -> Self {
192 Self {
193 options: SolverOptions::default(),
194 backend_policy: BackendPolicy::Auto,
195 hybrid_options: HybridOptions::default(),
196 structural_options: StructuralOptions::default(),
197 }
198 }
199}
200
201#[derive(Clone, Debug)]
202pub struct HybitAnalysis {
203 profile: MatrixProfile,
204 backend: MatrixBackend,
205 structure_signature: u64,
206 value_signature: u64,
207 analysis_seconds: f64,
208}
209
210impl HybitAnalysis {
211 pub fn profile(&self) -> &MatrixProfile {
212 &self.profile
213 }
214 pub fn backend(&self) -> MatrixBackend {
215 self.backend
216 }
217 pub fn analysis_seconds(&self) -> f64 {
218 self.analysis_seconds
219 }
220}
221
222#[derive(Debug)]
223pub struct HybitPreparedSystem {
224 options: SolverOptions,
225 hybrid_options: HybridOptions,
226 backend: MatrixBackend,
227 structure_signature: u64,
228 value_signature: u64,
229 analysis_seconds: f64,
230 prepare_seconds: f64,
231 jacobi: JacobiPreconditioner,
232 abtm: Option<AbtmMatrix>,
233 hybrid: Option<HybridPreconditioner>,
234 workspace: PcgWorkspace,
235 solve_sequence: usize,
236}
237
238impl HybitPreparedSystem {
239 pub fn backend(&self) -> MatrixBackend {
240 self.backend
241 }
242 pub fn analysis_seconds(&self) -> f64 {
243 self.analysis_seconds
244 }
245 pub fn prepare_seconds(&self) -> f64 {
246 self.prepare_seconds
247 }
248 pub fn solve_count(&self) -> usize {
249 self.solve_sequence
250 }
251 pub fn krylov_workspace_bytes(&self) -> usize {
252 self.workspace.bytes()
253 }
254 pub fn has_cached_hybrid(&self) -> bool {
255 self.hybrid.is_some()
256 }
257
258 fn validate_matrix(&self, matrix: &Csr32Matrix) -> Result<(), HybitError> {
259 let (structure, values) = matrix_signatures(matrix);
260 if structure != self.structure_signature {
261 return Err(HybitError::InvalidArgument(
262 "prepared context matrix structure changed; analyze and prepare again",
263 ));
264 }
265 if values != self.value_signature {
266 return Err(HybitError::InvalidArgument("prepared context matrix values changed; prepare again before reusing local factors"));
267 }
268 Ok(())
269 }
270
271 pub fn solve(
272 &mut self,
273 matrix: &Csr32Matrix,
274 b: &[f64],
275 x: &mut [f64],
276 ) -> Result<SolveReport, HybitError> {
277 self.validate_matrix(matrix)?;
278 if b.len() != matrix.nrows() {
279 return Err(HybitError::DimensionMismatch {
280 expected: matrix.nrows(),
281 actual: b.len(),
282 });
283 }
284 if x.len() != matrix.ncols() {
285 return Err(HybitError::DimensionMismatch {
286 expected: matrix.ncols(),
287 actual: x.len(),
288 });
289 }
290 self.solve_sequence += 1;
291 let sequence = self.solve_sequence;
292 let charge_context_setup = sequence == 1;
293
294 if let Some(hybrid) = self.hybrid.as_ref() {
298 let start = Instant::now();
299 let outcome = pcg_with_workspace(
300 operator_for_backend(matrix, self.abtm.as_ref(), self.backend),
301 hybrid,
302 b,
303 x,
304 self.options,
305 &mut self.workspace,
306 )?;
307 let elapsed = start.elapsed().as_secs_f64();
308 let metrics = ReportMetrics {
309 analysis_seconds: if charge_context_setup {
310 self.analysis_seconds
311 } else {
312 0.0
313 },
314 prepare_seconds: if charge_context_setup {
315 self.prepare_seconds
316 } else {
317 0.0
318 },
319 restart_seconds: elapsed,
320 local_direct_regions: hybrid.region_count(),
321 largest_local_region: hybrid.largest_region(),
322 local_factor_dofs: hybrid.local_dofs(),
323 unique_local_factor_dofs: hybrid.unique_local_dofs(),
324 local_factor_bytes: hybrid.factor_bytes(),
325 overlap_layers: self.hybrid_options.overlap_layers,
326 preconditioner_reused: true,
327 solve_sequence: sequence,
328 krylov_workspace_bytes: self.workspace.bytes(),
329 ..ReportMetrics::default()
330 };
331 return Ok(report_from_outcome(
332 outcome,
333 SolverKind::Hybrid,
334 PreconditionerKind::Hybrid,
335 self.backend,
336 b,
337 metrics,
338 ));
339 }
340
341 self.solve_uncached(matrix, b, x, sequence, charge_context_setup)
342 }
343
344 fn solve_uncached(
345 &mut self,
346 matrix: &Csr32Matrix,
347 b: &[f64],
348 x: &mut [f64],
349 sequence: usize,
350 charge_context_setup: bool,
351 ) -> Result<SolveReport, HybitError> {
352 let probe_budget = if self.options.max_iterations <= 1 {
353 self.options.max_iterations
354 } else {
355 self.hybrid_options
356 .probe_iterations
357 .min(self.options.max_iterations - 1)
358 .max(1)
359 };
360 let mut probe_options = self.options;
361 probe_options.max_iterations = probe_budget;
362
363 let probe_start = Instant::now();
364 let probe = pcg_with_workspace(
365 operator_for_backend(matrix, self.abtm.as_ref(), self.backend),
366 &self.jacobi,
367 b,
368 x,
369 probe_options,
370 &mut self.workspace,
371 )?;
372 let probe_seconds = probe_start.elapsed().as_secs_f64();
373 let probe_iterations = probe.iterations;
374 let probe_final_residual = probe.final_residual;
375 let base_metrics = ReportMetrics {
376 analysis_seconds: if charge_context_setup {
377 self.analysis_seconds
378 } else {
379 0.0
380 },
381 prepare_seconds: if charge_context_setup {
382 self.prepare_seconds
383 } else {
384 0.0
385 },
386 probe_seconds,
387 probe_iterations,
388 probe_final_residual,
389 overlap_layers: self.hybrid_options.overlap_layers,
390 solve_sequence: sequence,
391 krylov_workspace_bytes: self.workspace.bytes(),
392 ..ReportMetrics::default()
393 };
394
395 if probe.status == SolveStatus::Converged || probe.iterations >= self.options.max_iterations
396 {
397 return Ok(report_from_outcome(
398 probe,
399 SolverKind::Pcg,
400 PreconditionerKind::Jacobi,
401 self.backend,
402 b,
403 base_metrics,
404 ));
405 }
406
407 let poor_progress = self.hybrid_options.enabled
408 && probe.status == SolveStatus::MaxIterations
409 && probe.initial_residual > 0.0
410 && probe.final_residual / probe.initial_residual
411 > self.hybrid_options.escalation_residual_ratio;
412
413 let remaining = self.options.max_iterations.saturating_sub(probe_iterations);
414 if !poor_progress || remaining == 0 {
415 let (continuation, continuation_seconds) = run_continuation(
416 matrix,
417 self.abtm.as_ref(),
418 self.backend,
419 &self.jacobi,
420 b,
421 x,
422 self.options,
423 remaining,
424 &mut self.workspace,
425 )?;
426 let outcome = combine_outcomes(probe, continuation);
427 let mut metrics = base_metrics;
428 metrics.restart_seconds = continuation_seconds;
429 return Ok(report_from_outcome(
430 outcome,
431 SolverKind::Pcg,
432 PreconditionerKind::Jacobi,
433 self.backend,
434 b,
435 metrics,
436 ));
437 }
438
439 let diagnostics_start = Instant::now();
440 let residual = residual(matrix, b, x)?;
441 let risk = numerical_risk_mask(matrix, self.hybrid_options)?;
442 let seeds = residual_seed_mask(&residual, self.hybrid_options.residual_seed_fraction)?;
443 let selected =
444 select_risk_components(matrix, &risk, &seeds, &residual, self.hybrid_options)?;
445 let hard_dofs = selected.count_ones();
446 let core_regions = extract_core_regions(matrix, &selected, &residual, self.hybrid_options)?;
447 if self.abtm.is_none() {
448 self.abtm = Some(AbtmMatrix::from_csr32(matrix, AbtmConfig::default())?);
449 }
450 let abtm = self
451 .abtm
452 .as_ref()
453 .expect("ABTM topology initialized for hybrid escalation");
454 let regions =
455 expand_regions_with_overlap(abtm, &core_regions, &residual, self.hybrid_options)?;
456 let diagnostics_seconds = diagnostics_start.elapsed().as_secs_f64();
457
458 if regions.is_empty() {
459 let (continuation, continuation_seconds) = run_continuation(
460 matrix,
461 self.abtm.as_ref(),
462 self.backend,
463 &self.jacobi,
464 b,
465 x,
466 self.options,
467 remaining,
468 &mut self.workspace,
469 )?;
470 let outcome = combine_outcomes(probe, continuation);
471 let mut metrics = base_metrics;
472 metrics.diagnostics_seconds = diagnostics_seconds;
473 metrics.restart_seconds = continuation_seconds;
474 metrics.hard_dofs = hard_dofs;
475 return Ok(report_from_outcome(
476 outcome,
477 SolverKind::Pcg,
478 PreconditionerKind::Jacobi,
479 self.backend,
480 b,
481 metrics,
482 ));
483 }
484
485 let factor_start = Instant::now();
486 let hybrid = match HybridPreconditioner::from_csr32(matrix, regions) {
487 Ok(hybrid) => hybrid,
488 Err(HybitError::NumericalBreakdown(_)) | Err(HybitError::InvalidMatrix(_)) => {
489 let local_factor_seconds = factor_start.elapsed().as_secs_f64();
490 let (continuation, continuation_seconds) = run_continuation(
491 matrix,
492 self.abtm.as_ref(),
493 self.backend,
494 &self.jacobi,
495 b,
496 x,
497 self.options,
498 remaining,
499 &mut self.workspace,
500 )?;
501 let outcome = combine_outcomes(probe, continuation);
502 let mut metrics = base_metrics;
503 metrics.diagnostics_seconds = diagnostics_seconds;
504 metrics.local_factor_seconds = local_factor_seconds;
505 metrics.restart_seconds = continuation_seconds;
506 metrics.hard_dofs = hard_dofs;
507 return Ok(report_from_outcome(
508 outcome,
509 SolverKind::Pcg,
510 PreconditionerKind::Jacobi,
511 self.backend,
512 b,
513 metrics,
514 ));
515 }
516 Err(err) => return Err(err),
517 };
518 let local_factor_seconds = factor_start.elapsed().as_secs_f64();
519
520 let mut stage_options = self.options;
521 stage_options.max_iterations = remaining;
522 let restart_start = Instant::now();
523 let stage = pcg_with_workspace(
524 operator_for_backend(matrix, self.abtm.as_ref(), self.backend),
525 &hybrid,
526 b,
527 x,
528 stage_options,
529 &mut self.workspace,
530 )?;
531 let restart_seconds = restart_start.elapsed().as_secs_f64();
532 let cacheable_hybrid = stage.status != SolveStatus::Breakdown;
533 let outcome = combine_outcomes(probe, stage);
534
535 let metrics = ReportMetrics {
536 analysis_seconds: base_metrics.analysis_seconds,
537 prepare_seconds: base_metrics.prepare_seconds,
538 probe_seconds,
539 diagnostics_seconds,
540 local_factor_seconds,
541 restart_seconds,
542 escalations: 1,
543 probe_iterations,
544 probe_final_residual,
545 hard_dofs,
546 local_direct_regions: hybrid.region_count(),
547 largest_local_region: hybrid.largest_region(),
548 local_factor_dofs: hybrid.local_dofs(),
549 unique_local_factor_dofs: hybrid.unique_local_dofs(),
550 local_factor_bytes: hybrid.factor_bytes(),
551 overlap_layers: self.hybrid_options.overlap_layers,
552 preconditioner_reused: false,
553 solve_sequence: sequence,
554 krylov_workspace_bytes: self.workspace.bytes(),
555 };
556
557 if cacheable_hybrid {
561 self.hybrid = Some(hybrid);
562 }
563
564 Ok(report_from_outcome(
565 outcome,
566 SolverKind::Hybrid,
567 PreconditionerKind::Hybrid,
568 self.backend,
569 b,
570 metrics,
571 ))
572 }
573}
574
575#[derive(Debug)]
576pub struct HybitPreparedStructuralSystem {
577 options: SolverOptions,
578 backend: MatrixBackend,
579 structure_signature: u64,
580 value_signature: u64,
581 analysis_seconds: f64,
582 prepare_seconds: f64,
583 aggregate_nodes: usize,
584 preconditioner: RigidBodyTwoLevelBlockJacobiPreconditioner,
585 abtm: Option<AbtmMatrix>,
586 effective_spmv_policy: StructuralSpmvPolicy,
587 effective_preconditioner_policy: StructuralPreconditionerPolicy,
588 effective_pcg_vector_policy: StructuralPcgVectorPolicy,
589 workspace: PcgWorkspace,
590 solve_sequence: usize,
591}
592
593impl HybitPreparedStructuralSystem {
594 pub fn backend(&self) -> MatrixBackend {
595 self.backend
596 }
597 pub fn analysis_seconds(&self) -> f64 {
598 self.analysis_seconds
599 }
600 pub fn prepare_seconds(&self) -> f64 {
601 self.prepare_seconds
602 }
603 pub fn solve_count(&self) -> usize {
604 self.solve_sequence
605 }
606 pub fn krylov_workspace_bytes(&self) -> usize {
607 self.workspace.bytes()
608 }
609 pub fn aggregate_nodes(&self) -> usize {
610 self.aggregate_nodes
611 }
612 pub fn aggregate_count(&self) -> usize {
613 self.preconditioner.aggregate_count()
614 }
615 pub fn min_aggregate_nodes(&self) -> usize {
616 self.preconditioner.min_aggregate_nodes()
617 }
618 pub fn max_aggregate_nodes(&self) -> usize {
619 self.preconditioner.max_aggregate_nodes()
620 }
621 pub fn aggregation(&self) -> RigidBodyAggregation {
622 self.preconditioner.aggregation()
623 }
624 pub fn coarse_dimension(&self) -> usize {
625 self.preconditioner.coarse_dimension()
626 }
627 pub fn preconditioner_bytes(&self) -> usize {
628 self.preconditioner.factor_bytes()
629 }
630 pub fn base_factor_bytes(&self) -> usize {
631 self.preconditioner.base_factor_bytes()
632 }
633 pub fn coarse_factor_bytes(&self) -> usize {
634 self.preconditioner.coarse_factor_bytes()
635 }
636 pub fn geometry_bytes(&self) -> usize {
637 self.preconditioner.geometry_bytes()
638 }
639 pub fn spmv_policy(&self) -> StructuralSpmvPolicy {
641 self.effective_spmv_policy
642 }
643 pub fn parallel_spmv_enabled(&self) -> bool {
644 self.effective_spmv_policy == StructuralSpmvPolicy::Parallel
645 }
646 pub fn structural_preconditioner_policy(&self) -> StructuralPreconditionerPolicy {
648 self.effective_preconditioner_policy
649 }
650 pub fn parallel_preconditioner_enabled(&self) -> bool {
651 self.effective_preconditioner_policy == StructuralPreconditionerPolicy::Parallel
652 }
653 pub fn parallel_preconditioner_index_bytes(&self) -> usize {
654 self.preconditioner.parallel_index_bytes()
655 }
656 pub fn pcg_vector_policy(&self) -> StructuralPcgVectorPolicy {
658 self.effective_pcg_vector_policy
659 }
660 pub fn parallel_pcg_vectors_enabled(&self) -> bool {
661 self.effective_pcg_vector_policy == StructuralPcgVectorPolicy::Parallel
662 }
663
664 fn validate_matrix(&self, matrix: &Csr32Matrix) -> Result<(), HybitError> {
665 let (structure, values) = matrix_signatures(matrix);
666 if structure != self.structure_signature {
667 return Err(HybitError::InvalidArgument(
668 "prepared structural context matrix structure changed; analyze and prepare again",
669 ));
670 }
671 if values != self.value_signature {
672 return Err(HybitError::InvalidArgument(
673 "prepared structural context matrix values changed; prepare again before reusing coarse factors",
674 ));
675 }
676 Ok(())
677 }
678
679 pub fn solve(
680 &mut self,
681 matrix: &Csr32Matrix,
682 b: &[f64],
683 x: &mut [f64],
684 ) -> Result<SolveReport, HybitError> {
685 self.validate_matrix(matrix)?;
686 if b.len() != matrix.nrows() {
687 return Err(HybitError::DimensionMismatch {
688 expected: matrix.nrows(),
689 actual: b.len(),
690 });
691 }
692 if x.len() != matrix.ncols() {
693 return Err(HybitError::DimensionMismatch {
694 expected: matrix.ncols(),
695 actual: x.len(),
696 });
697 }
698
699 self.solve_sequence += 1;
700 let sequence = self.solve_sequence;
701 let charge_context_setup = sequence == 1;
702 let start = Instant::now();
703 let outcome = match (
704 self.effective_spmv_policy,
705 self.effective_preconditioner_policy,
706 ) {
707 (StructuralSpmvPolicy::Parallel, StructuralPreconditionerPolicy::Parallel) => {
708 let operator = ParallelCsr32Operator::new(matrix);
709 let preconditioner =
710 ParallelRigidBodyTwoLevelPreconditioner::new(&self.preconditioner)?;
711 run_structural_pcg(
712 self.effective_pcg_vector_policy,
713 &operator,
714 &preconditioner,
715 b,
716 x,
717 self.options,
718 &mut self.workspace,
719 )?
720 }
721 (StructuralSpmvPolicy::Parallel, StructuralPreconditionerPolicy::Serial) => {
722 let operator = ParallelCsr32Operator::new(matrix);
723 run_structural_pcg(
724 self.effective_pcg_vector_policy,
725 &operator,
726 &self.preconditioner,
727 b,
728 x,
729 self.options,
730 &mut self.workspace,
731 )?
732 }
733 (StructuralSpmvPolicy::Serial, StructuralPreconditionerPolicy::Parallel) => {
734 let preconditioner =
735 ParallelRigidBodyTwoLevelPreconditioner::new(&self.preconditioner)?;
736 run_structural_pcg(
737 self.effective_pcg_vector_policy,
738 operator_for_backend(matrix, self.abtm.as_ref(), self.backend),
739 &preconditioner,
740 b,
741 x,
742 self.options,
743 &mut self.workspace,
744 )?
745 }
746 (StructuralSpmvPolicy::Serial, StructuralPreconditionerPolicy::Serial) => {
747 run_structural_pcg(
748 self.effective_pcg_vector_policy,
749 operator_for_backend(matrix, self.abtm.as_ref(), self.backend),
750 &self.preconditioner,
751 b,
752 x,
753 self.options,
754 &mut self.workspace,
755 )?
756 }
757 _ => unreachable!("structural execution policies are resolved during prepare"),
758 };
759 let elapsed = start.elapsed().as_secs_f64();
760 let metrics = ReportMetrics {
761 analysis_seconds: if charge_context_setup {
762 self.analysis_seconds
763 } else {
764 0.0
765 },
766 prepare_seconds: if charge_context_setup {
767 self.prepare_seconds
768 } else {
769 0.0
770 },
771 restart_seconds: elapsed,
772 preconditioner_reused: sequence > 1,
773 solve_sequence: sequence,
774 krylov_workspace_bytes: self.workspace.bytes(),
775 ..ReportMetrics::default()
776 };
777 Ok(report_from_outcome(
778 outcome,
779 SolverKind::Pcg,
780 PreconditionerKind::RigidBodyTwoLevel,
781 self.backend,
782 b,
783 metrics,
784 ))
785 }
786}
787
788#[derive(Clone, Copy, Debug, Default)]
789struct ReportMetrics {
790 analysis_seconds: f64,
791 prepare_seconds: f64,
792 probe_seconds: f64,
793 diagnostics_seconds: f64,
794 local_factor_seconds: f64,
795 restart_seconds: f64,
796 escalations: usize,
797 probe_iterations: usize,
798 probe_final_residual: f64,
799 hard_dofs: usize,
800 local_direct_regions: usize,
801 largest_local_region: usize,
802 local_factor_dofs: usize,
803 unique_local_factor_dofs: usize,
804 local_factor_bytes: usize,
805 overlap_layers: usize,
806 preconditioner_reused: bool,
807 solve_sequence: usize,
808 krylov_workspace_bytes: usize,
809}
810
811fn structural_graph_auto_can_fallback(error: &HybitError) -> bool {
812 match error {
813 HybitError::NumericalBreakdown(_) => true,
814 HybitError::InvalidArgument(message) => matches!(
815 *message,
816 "a structural graph component contains fewer than three nodes"
817 ),
818 _ => false,
819 }
820}
821
822impl HybitSolver {
823 pub fn new() -> Self {
824 Self::default()
825 }
826 pub fn options(&self) -> SolverOptions {
827 self.options
828 }
829 pub fn hybrid_options(&self) -> HybridOptions {
830 self.hybrid_options
831 }
832 pub fn structural_options(&self) -> StructuralOptions {
833 self.structural_options
834 }
835
836 pub fn set_options(&mut self, options: SolverOptions) -> Result<(), HybitError> {
837 options.validate()?;
838 self.options = options;
839 Ok(())
840 }
841
842 pub fn set_hybrid_options(&mut self, options: HybridOptions) -> Result<(), HybitError> {
843 options.validate()?;
844 self.hybrid_options = options;
845 Ok(())
846 }
847
848 pub fn set_structural_options(&mut self, options: StructuralOptions) -> Result<(), HybitError> {
849 options.validate()?;
850 self.structural_options = options;
851 Ok(())
852 }
853
854 pub fn set_backend_policy(&mut self, policy: BackendPolicy) {
855 self.backend_policy = policy;
856 }
857
858 pub fn analyze_csr32(&self, matrix: &Csr32Matrix) -> Result<HybitAnalysis, HybitError> {
859 self.options.validate()?;
860 self.hybrid_options.validate()?;
861 let start = Instant::now();
862 let profile = analyze_csr32(matrix)?;
863 if !profile.square {
864 return Err(HybitError::InvalidMatrix(
865 "AutoSolver currently supports square SPD systems",
866 ));
867 }
868 if !profile.full_diagonal || !profile.positive_diagonal {
869 return Err(HybitError::InvalidMatrix(
870 "PCG path requires a complete positive diagonal",
871 ));
872 }
873 let backend = match self.backend_policy {
874 BackendPolicy::Auto | BackendPolicy::Csr32 => MatrixBackend::Csr32,
875 BackendPolicy::Abtm => MatrixBackend::Abtm,
876 };
877 let (structure_signature, value_signature) = matrix_signatures(matrix);
878 Ok(HybitAnalysis {
879 profile,
880 backend,
881 structure_signature,
882 value_signature,
883 analysis_seconds: start.elapsed().as_secs_f64(),
884 })
885 }
886
887 pub fn analyze(&self, matrix: &Csr32Matrix) -> Result<MatrixProfile, HybitError> {
889 Ok(self.analyze_csr32(matrix)?.profile)
890 }
891
892 pub fn prepare_csr32(
893 &self,
894 matrix: &Csr32Matrix,
895 analysis: &HybitAnalysis,
896 ) -> Result<HybitPreparedSystem, HybitError> {
897 self.options.validate()?;
898 self.hybrid_options.validate()?;
899 let (structure_signature, value_signature) = matrix_signatures(matrix);
900 if structure_signature != analysis.structure_signature
901 || value_signature != analysis.value_signature
902 {
903 return Err(HybitError::InvalidArgument(
904 "matrix changed between analyze and prepare",
905 ));
906 }
907 let start = Instant::now();
908 let jacobi = JacobiPreconditioner::from_csr32(matrix)?;
909 let abtm = if analysis.backend == MatrixBackend::Abtm {
913 Some(AbtmMatrix::from_csr32(matrix, AbtmConfig::default())?)
914 } else {
915 None
916 };
917 let workspace = PcgWorkspace::new(matrix.nrows());
918 let prepare_seconds = start.elapsed().as_secs_f64();
919 Ok(HybitPreparedSystem {
920 options: self.options,
921 hybrid_options: self.hybrid_options,
922 backend: analysis.backend,
923 structure_signature,
924 value_signature,
925 analysis_seconds: analysis.analysis_seconds,
926 prepare_seconds,
927 jacobi,
928 abtm,
929 hybrid: None,
930 workspace,
931 solve_sequence: 0,
932 })
933 }
934
935 pub fn prepare(&self, matrix: &Csr32Matrix) -> Result<HybitPreparedSystem, HybitError> {
936 let analysis = self.analyze_csr32(matrix)?;
937 self.prepare_csr32(matrix, &analysis)
938 }
939
940 pub fn prepare_structural_csr32(
941 &self,
942 matrix: &Csr32Matrix,
943 analysis: &HybitAnalysis,
944 coordinates: &[[f64; 3]],
945 ) -> Result<HybitPreparedStructuralSystem, HybitError> {
946 self.options.validate()?;
947 self.structural_options.validate()?;
948 let (structure_signature, value_signature) = matrix_signatures(matrix);
949 if structure_signature != analysis.structure_signature
950 || value_signature != analysis.value_signature
951 {
952 return Err(HybitError::InvalidArgument(
953 "matrix changed between analyze and structural prepare",
954 ));
955 }
956 let expected = coordinates
957 .len()
958 .checked_mul(3)
959 .ok_or(HybitError::SizeOverflow)?;
960 if matrix.nrows() != expected || matrix.ncols() != expected {
961 return Err(HybitError::DimensionMismatch {
962 expected: matrix.nrows(),
963 actual: expected,
964 });
965 }
966
967 let start = Instant::now();
968 let aggregate_nodes = recommend_rigid_body_aggregate_nodes(
969 coordinates.len(),
970 self.structural_options.target_coarse_dimension,
971 )?;
972 let preconditioner = match self.structural_options.aggregation {
973 RigidBodyAggregation::Auto => {
974 match RigidBodyTwoLevelBlockJacobiPreconditioner::from_csr32_graph(
975 matrix,
976 coordinates,
977 aggregate_nodes,
978 ) {
979 Ok(preconditioner) => preconditioner,
980 Err(err) if structural_graph_auto_can_fallback(&err) => {
981 RigidBodyTwoLevelBlockJacobiPreconditioner::from_csr32(
989 matrix,
990 coordinates,
991 aggregate_nodes,
992 )?
993 }
994 Err(err) => return Err(err),
995 }
996 }
997 RigidBodyAggregation::Contiguous => {
998 RigidBodyTwoLevelBlockJacobiPreconditioner::from_csr32(
999 matrix,
1000 coordinates,
1001 aggregate_nodes,
1002 )?
1003 }
1004 RigidBodyAggregation::Graph => {
1005 RigidBodyTwoLevelBlockJacobiPreconditioner::from_csr32_graph(
1006 matrix,
1007 coordinates,
1008 aggregate_nodes,
1009 )?
1010 }
1011 };
1012 let abtm = if analysis.backend == MatrixBackend::Abtm {
1013 Some(AbtmMatrix::from_csr32(matrix, AbtmConfig::default())?)
1014 } else {
1015 None
1016 };
1017 let effective_spmv_policy = match self.structural_options.spmv_policy {
1018 StructuralSpmvPolicy::Auto => {
1019 if analysis.backend == MatrixBackend::Csr32
1020 && matrix.nnz() >= STRUCTURAL_PARALLEL_SPMV_MIN_NNZ
1021 {
1022 StructuralSpmvPolicy::Parallel
1023 } else {
1024 StructuralSpmvPolicy::Serial
1025 }
1026 }
1027 StructuralSpmvPolicy::Serial => StructuralSpmvPolicy::Serial,
1028 StructuralSpmvPolicy::Parallel => {
1029 if analysis.backend != MatrixBackend::Csr32 {
1030 return Err(HybitError::InvalidArgument(
1031 "parallel structural SpMV requires the CSR32 backend",
1032 ));
1033 }
1034 StructuralSpmvPolicy::Parallel
1035 }
1036 };
1037 let effective_preconditioner_policy = match self.structural_options.preconditioner_policy {
1038 StructuralPreconditionerPolicy::Auto => {
1039 if matrix.nnz() >= STRUCTURAL_PARALLEL_PRECONDITIONER_MIN_NNZ {
1040 StructuralPreconditionerPolicy::Parallel
1041 } else {
1042 StructuralPreconditionerPolicy::Serial
1043 }
1044 }
1045 StructuralPreconditionerPolicy::Serial => StructuralPreconditionerPolicy::Serial,
1046 StructuralPreconditionerPolicy::Parallel => StructuralPreconditionerPolicy::Parallel,
1047 };
1048 if effective_preconditioner_policy == StructuralPreconditionerPolicy::Parallel {
1049 let _ = ParallelRigidBodyTwoLevelPreconditioner::new(&preconditioner)?;
1052 }
1053 let effective_pcg_vector_policy = match self.structural_options.pcg_vector_policy {
1054 StructuralPcgVectorPolicy::Auto => {
1055 if matrix.nrows() >= STRUCTURAL_PARALLEL_PCG_VECTOR_MIN_N
1056 && parallel_vector_worker_count() >= STRUCTURAL_PARALLEL_PCG_VECTOR_MIN_THREADS
1057 {
1058 StructuralPcgVectorPolicy::Parallel
1059 } else {
1060 StructuralPcgVectorPolicy::Serial
1061 }
1062 }
1063 StructuralPcgVectorPolicy::Serial => StructuralPcgVectorPolicy::Serial,
1064 StructuralPcgVectorPolicy::Parallel => StructuralPcgVectorPolicy::Parallel,
1065 };
1066 let workspace = PcgWorkspace::new(matrix.nrows());
1067 let prepare_seconds = start.elapsed().as_secs_f64();
1068
1069 Ok(HybitPreparedStructuralSystem {
1070 options: self.options,
1071 backend: analysis.backend,
1072 structure_signature,
1073 value_signature,
1074 analysis_seconds: analysis.analysis_seconds,
1075 prepare_seconds,
1076 aggregate_nodes,
1077 preconditioner,
1078 abtm,
1079 effective_spmv_policy,
1080 effective_preconditioner_policy,
1081 effective_pcg_vector_policy,
1082 workspace,
1083 solve_sequence: 0,
1084 })
1085 }
1086
1087 pub fn prepare_structural(
1088 &self,
1089 matrix: &Csr32Matrix,
1090 coordinates: &[[f64; 3]],
1091 ) -> Result<HybitPreparedStructuralSystem, HybitError> {
1092 let analysis = self.analyze_csr32(matrix)?;
1093 self.prepare_structural_csr32(matrix, &analysis, coordinates)
1094 }
1095
1096 pub fn solve_structural_csr32(
1097 &self,
1098 matrix: &Csr32Matrix,
1099 coordinates: &[[f64; 3]],
1100 b: &[f64],
1101 x: &mut [f64],
1102 ) -> Result<SolveReport, HybitError> {
1103 let analysis = self.analyze_csr32(matrix)?;
1104 let mut prepared = self.prepare_structural_csr32(matrix, &analysis, coordinates)?;
1105 prepared.solve(matrix, b, x)
1106 }
1107
1108 pub fn solve_csr32(
1109 &self,
1110 matrix: &Csr32Matrix,
1111 b: &[f64],
1112 x: &mut [f64],
1113 ) -> Result<SolveReport, HybitError> {
1114 let analysis = self.analyze_csr32(matrix)?;
1115 let mut prepared = self.prepare_csr32(matrix, &analysis)?;
1116 prepared.solve(matrix, b, x)
1117 }
1118}
1119
1120fn run_structural_pcg(
1121 vector_policy: StructuralPcgVectorPolicy,
1122 operator: &dyn LinearOperator,
1123 preconditioner: &dyn Preconditioner,
1124 b: &[f64],
1125 x: &mut [f64],
1126 options: SolverOptions,
1127 workspace: &mut PcgWorkspace,
1128) -> Result<KrylovOutcome, HybitError> {
1129 match vector_policy {
1130 StructuralPcgVectorPolicy::Parallel => {
1131 pcg_with_workspace_parallel_vectors(operator, preconditioner, b, x, options, workspace)
1132 }
1133 StructuralPcgVectorPolicy::Serial => {
1134 pcg_with_workspace(operator, preconditioner, b, x, options, workspace)
1135 }
1136 StructuralPcgVectorPolicy::Auto => {
1137 unreachable!("structural PCG vector policy is resolved during prepare")
1138 }
1139 }
1140}
1141
1142fn operator_for_backend<'a>(
1143 matrix: &'a Csr32Matrix,
1144 abtm: Option<&'a AbtmMatrix>,
1145 backend: MatrixBackend,
1146) -> &'a dyn LinearOperator {
1147 match backend {
1148 MatrixBackend::Csr32 => matrix,
1149 MatrixBackend::Abtm => abtm.expect("ABTM storage initialized"),
1150 MatrixBackend::MatrixFree => unreachable!(),
1151 }
1152}
1153
1154#[allow(clippy::too_many_arguments)]
1155fn run_continuation(
1156 matrix: &Csr32Matrix,
1157 abtm: Option<&AbtmMatrix>,
1158 backend: MatrixBackend,
1159 jacobi: &JacobiPreconditioner,
1160 b: &[f64],
1161 x: &mut [f64],
1162 base_options: SolverOptions,
1163 remaining: usize,
1164 workspace: &mut PcgWorkspace,
1165) -> Result<(KrylovOutcome, f64), HybitError> {
1166 if remaining == 0 {
1167 let r = residual(matrix, b, x)?;
1168 let norm = l2_norm(&r);
1169 return Ok((
1170 KrylovOutcome {
1171 status: SolveStatus::MaxIterations,
1172 iterations: 0,
1173 initial_residual: norm,
1174 final_residual: norm,
1175 },
1176 0.0,
1177 ));
1178 }
1179 let mut options = base_options;
1180 options.max_iterations = remaining;
1181 let start = Instant::now();
1182 let outcome = pcg_with_workspace(
1183 operator_for_backend(matrix, abtm, backend),
1184 jacobi,
1185 b,
1186 x,
1187 options,
1188 workspace,
1189 )?;
1190 Ok((outcome, start.elapsed().as_secs_f64()))
1191}
1192
1193fn combine_outcomes(first: KrylovOutcome, second: KrylovOutcome) -> KrylovOutcome {
1194 KrylovOutcome {
1195 status: second.status,
1196 iterations: first.iterations + second.iterations,
1197 initial_residual: first.initial_residual,
1198 final_residual: second.final_residual,
1199 }
1200}
1201
1202fn report_from_outcome(
1203 outcome: KrylovOutcome,
1204 solver: SolverKind,
1205 preconditioner: PreconditionerKind,
1206 backend: MatrixBackend,
1207 b: &[f64],
1208 metrics: ReportMetrics,
1209) -> SolveReport {
1210 let b_norm = l2_norm(b);
1211 let relative_residual = if b_norm == 0.0 {
1212 outcome.final_residual
1213 } else {
1214 outcome.final_residual / b_norm
1215 };
1216 let setup_seconds = metrics.analysis_seconds
1217 + metrics.prepare_seconds
1218 + metrics.diagnostics_seconds
1219 + metrics.local_factor_seconds;
1220 let solve_seconds = metrics.probe_seconds + metrics.restart_seconds;
1221 SolveReport {
1222 status: outcome.status,
1223 solver,
1224 preconditioner,
1225 backend,
1226 iterations: outcome.iterations,
1227 initial_residual: outcome.initial_residual,
1228 final_residual: outcome.final_residual,
1229 relative_residual,
1230 setup_seconds,
1231 solve_seconds,
1232 analysis_seconds: metrics.analysis_seconds,
1233 prepare_seconds: metrics.prepare_seconds,
1234 probe_seconds: metrics.probe_seconds,
1235 diagnostics_seconds: metrics.diagnostics_seconds,
1236 local_factor_seconds: metrics.local_factor_seconds,
1237 restart_seconds: metrics.restart_seconds,
1238 escalations: metrics.escalations,
1239 probe_iterations: metrics.probe_iterations,
1240 probe_final_residual: metrics.probe_final_residual,
1241 hard_dofs: metrics.hard_dofs,
1242 local_direct_regions: metrics.local_direct_regions,
1243 largest_local_region: metrics.largest_local_region,
1244 local_factor_dofs: metrics.local_factor_dofs,
1245 unique_local_factor_dofs: metrics.unique_local_factor_dofs,
1246 local_factor_bytes: metrics.local_factor_bytes,
1247 overlap_layers: metrics.overlap_layers,
1248 preconditioner_reused: metrics.preconditioner_reused,
1249 solve_sequence: metrics.solve_sequence,
1250 krylov_workspace_bytes: metrics.krylov_workspace_bytes,
1251 }
1252}
1253
1254fn fnv_mix(mut h: u64, value: u64) -> u64 {
1255 const PRIME: u64 = 0x100000001b3;
1256 for b in value.to_le_bytes() {
1257 h ^= b as u64;
1258 h = h.wrapping_mul(PRIME);
1259 }
1260 h
1261}
1262
1263fn matrix_signatures(matrix: &Csr32Matrix) -> (u64, u64) {
1264 let mut structure = 0xcbf29ce484222325u64;
1265 structure = fnv_mix(structure, matrix.nrows() as u64);
1266 structure = fnv_mix(structure, matrix.ncols() as u64);
1267 structure = fnv_mix(structure, matrix.nnz() as u64);
1268 for &v in matrix.row_ptr() {
1269 structure = fnv_mix(structure, v as u64);
1270 }
1271 for &v in matrix.col_idx() {
1272 structure = fnv_mix(structure, v as u64);
1273 }
1274
1275 let mut values = 0xcbf29ce484222325u64;
1276 values = fnv_mix(values, structure);
1277 for &v in matrix.values() {
1278 values = fnv_mix(values, v.to_bits());
1279 }
1280 (structure, values)
1281}
1282fn residual(matrix: &Csr32Matrix, b: &[f64], x: &[f64]) -> Result<Vec<f64>, HybitError> {
1283 let mut ax = vec![0.0; matrix.nrows()];
1284 matrix.apply(x, &mut ax)?;
1285 Ok(b.iter().zip(ax).map(|(&bi, ai)| bi - ai).collect())
1286}
1287
1288fn numerical_risk_mask(
1289 matrix: &Csr32Matrix,
1290 options: HybridOptions,
1291) -> Result<DofMask, HybitError> {
1292 let diagonal = matrix.diagonal()?;
1293 let n = matrix.nrows();
1294 let mut risk = DofMask::new(n);
1295 for row in 0..n {
1296 let diag = diagonal[row].abs();
1297 if diag == 0.0 {
1298 continue;
1299 }
1300 let start = matrix.row_ptr()[row] as usize;
1301 let end = matrix.row_ptr()[row + 1] as usize;
1302 let mut offdiag_sum = 0.0;
1303 let mut max_scale_jump = 1.0f64;
1304 for p in start..end {
1305 let col = matrix.col_idx()[p] as usize;
1306 if col == row {
1307 continue;
1308 }
1309 offdiag_sum += matrix.values()[p].abs();
1310 let neighbor_diag = diagonal[col].abs();
1311 if neighbor_diag > 0.0 {
1312 max_scale_jump =
1313 max_scale_jump.max((diag / neighbor_diag).max(neighbor_diag / diag));
1314 }
1315 }
1316 let coupling = offdiag_sum / diag;
1317 if coupling >= options.coupling_risk_threshold
1318 || max_scale_jump >= options.scale_jump_threshold
1319 {
1320 risk.set(row, true)?;
1321 }
1322 }
1323 Ok(risk)
1324}
1325
1326fn residual_seed_mask(residual: &[f64], fraction: f64) -> Result<DofMask, HybitError> {
1327 let max_abs = residual.iter().fold(0.0f64, |m, &v| m.max(v.abs()));
1328 let mut seeds = DofMask::new(residual.len());
1329 if max_abs == 0.0 {
1330 return Ok(seeds);
1331 }
1332 let threshold = fraction * max_abs;
1333 for (i, &value) in residual.iter().enumerate() {
1334 if value.abs() >= threshold {
1335 seeds.set(i, true)?;
1336 }
1337 }
1338 Ok(seeds)
1339}
1340
1341fn select_risk_components(
1342 matrix: &Csr32Matrix,
1343 risk: &DofMask,
1344 seeds: &DofMask,
1345 residual: &[f64],
1346 options: HybridOptions,
1347) -> Result<DofMask, HybitError> {
1348 let n = matrix.nrows();
1349 let mut selected = DofMask::new(n);
1350 let mut visited = vec![false; n];
1351 let cap = options.max_local_region_size.max(1);
1352
1353 for start in risk.indices() {
1354 if visited[start] {
1355 continue;
1356 }
1357 let mut queue = VecDeque::new();
1358 let mut component = Vec::new();
1359 queue.push_back(start);
1360 visited[start] = true;
1361 let mut touches_seed = false;
1362 while let Some(row) = queue.pop_front() {
1363 component.push(row);
1364 touches_seed |= seeds.contains(row);
1365 let rs = matrix.row_ptr()[row] as usize;
1366 let re = matrix.row_ptr()[row + 1] as usize;
1367 for p in rs..re {
1368 let col = matrix.col_idx()[p] as usize;
1369 if col < n && risk.contains(col) && !visited[col] {
1370 visited[col] = true;
1371 queue.push_back(col);
1372 }
1373 }
1374 }
1375 if !touches_seed {
1376 continue;
1377 }
1378
1379 if component.len() <= cap {
1380 for dof in component {
1381 selected.set(dof, true)?;
1382 }
1383 } else {
1384 let root = *component
1385 .iter()
1386 .max_by(|&&a, &&b| residual[a].abs().total_cmp(&residual[b].abs()))
1387 .expect("component is non-empty");
1388 let mut local_seen = vec![false; n];
1389 let mut local_queue = VecDeque::new();
1390 local_queue.push_back(root);
1391 local_seen[root] = true;
1392 let mut count = 0usize;
1393 while let Some(row) = local_queue.pop_front() {
1394 if count >= cap {
1395 break;
1396 }
1397 selected.set(row, true)?;
1398 count += 1;
1399 let rs = matrix.row_ptr()[row] as usize;
1400 let re = matrix.row_ptr()[row + 1] as usize;
1401 for p in rs..re {
1402 let col = matrix.col_idx()[p] as usize;
1403 if col < n && risk.contains(col) && !local_seen[col] {
1404 local_seen[col] = true;
1405 local_queue.push_back(col);
1406 }
1407 }
1408 }
1409 }
1410 }
1411
1412 if selected.is_empty() {
1413 selected.union_assign(seeds)?;
1414 }
1415 Ok(selected)
1416}
1417
1418fn extract_core_regions(
1419 matrix: &Csr32Matrix,
1420 mask: &DofMask,
1421 residual: &[f64],
1422 options: HybridOptions,
1423) -> Result<Vec<Vec<usize>>, HybitError> {
1424 let n = matrix.nrows();
1425 let mut visited = vec![false; n];
1426 let mut regions: Vec<Vec<usize>> = Vec::new();
1427
1428 for start in mask.indices() {
1429 if visited[start] {
1430 continue;
1431 }
1432 let mut queue = VecDeque::new();
1433 let mut component = Vec::new();
1434 queue.push_back(start);
1435 visited[start] = true;
1436 while let Some(row) = queue.pop_front() {
1437 component.push(row);
1438 let rs = matrix.row_ptr()[row] as usize;
1439 let re = matrix.row_ptr()[row + 1] as usize;
1440 for p in rs..re {
1441 let col = matrix.col_idx()[p] as usize;
1442 if col < n && mask.contains(col) && !visited[col] {
1443 visited[col] = true;
1444 queue.push_back(col);
1445 }
1446 }
1447 }
1448 for chunk in component.chunks(options.max_local_region_size) {
1449 if !chunk.is_empty() {
1450 regions.push(chunk.to_vec());
1451 }
1452 }
1453 }
1454
1455 regions.sort_by(|a, b| {
1456 let sa = a.iter().fold(0.0f64, |m, &i| m.max(residual[i].abs()));
1457 let sb = b.iter().fold(0.0f64, |m, &i| m.max(residual[i].abs()));
1458 sb.total_cmp(&sa)
1459 });
1460 regions.truncate(options.max_local_regions);
1461 Ok(regions)
1462}
1463
1464fn expand_regions_with_overlap(
1465 abtm: &AbtmMatrix,
1466 core_regions: &[Vec<usize>],
1467 residual: &[f64],
1468 options: HybridOptions,
1469) -> Result<Vec<Vec<usize>>, HybitError> {
1470 let mut result = Vec::with_capacity(core_regions.len());
1471 for core in core_regions {
1472 let mut mask = DofMask::from_indices(abtm.rows(), core)?;
1473 for _ in 0..options.overlap_layers {
1474 mask = abtm.expand_mask_one_hop(&mask)?;
1475 }
1476 let mut expanded = mask.indices();
1477 if expanded.len() > options.max_local_region_size {
1478 let mut keep = core.clone();
1481 keep.sort_unstable();
1482 keep.dedup();
1483 if keep.len() > options.max_local_region_size {
1484 keep.truncate(options.max_local_region_size);
1485 } else {
1486 expanded.retain(|dof| keep.binary_search(dof).is_err());
1487 expanded.sort_by(|&a, &b| residual[b].abs().total_cmp(&residual[a].abs()));
1488 let room = options.max_local_region_size - keep.len();
1489 keep.extend(expanded.into_iter().take(room));
1490 }
1491 expanded = keep;
1492 }
1493 expanded.sort_unstable();
1494 expanded.dedup();
1495 if !expanded.is_empty() {
1496 result.push(expanded);
1497 }
1498 }
1499 Ok(result)
1500}
1501
1502#[cfg(test)]
1503mod tests {
1504 use super::*;
1505 use hybit_core::Preconditioner;
1506 use hybit_krylov::pcg;
1507
1508 fn poisson_1d(n: usize) -> Csr32Matrix {
1509 let mut row_ptr = Vec::with_capacity(n + 1);
1510 let mut col_idx = Vec::new();
1511 let mut values = Vec::new();
1512 row_ptr.push(0);
1513 for i in 0..n {
1514 if i > 0 {
1515 col_idx.push((i - 1) as u32);
1516 values.push(-1.0);
1517 }
1518 col_idx.push(i as u32);
1519 values.push(2.0);
1520 if i + 1 < n {
1521 col_idx.push((i + 1) as u32);
1522 values.push(-1.0);
1523 }
1524 row_ptr.push(col_idx.len() as u32);
1525 }
1526 Csr32Matrix::new(n, n, row_ptr, col_idx, values).unwrap()
1527 }
1528
1529 fn block_diagonal(
1530 easy_before: usize,
1531 hard_sizes: &[usize],
1532 easy_between: usize,
1533 ) -> Csr32Matrix {
1534 let n = easy_before
1535 + hard_sizes.iter().sum::<usize>()
1536 + easy_between * hard_sizes.len().saturating_sub(1);
1537 let mut row_ptr = Vec::with_capacity(n + 1);
1538 let mut col_idx = Vec::new();
1539 let mut values = Vec::new();
1540 row_ptr.push(0);
1541 let mut row = 0usize;
1542 for _ in 0..easy_before {
1543 col_idx.push(row as u32);
1544 values.push(1.0);
1545 row += 1;
1546 row_ptr.push(col_idx.len() as u32);
1547 }
1548 for (bi, &hard) in hard_sizes.iter().enumerate() {
1549 let base = row;
1550 for local in 0..hard {
1551 let i = base + local;
1552 if local > 0 {
1553 col_idx.push((i - 1) as u32);
1554 values.push(-1.0);
1555 }
1556 col_idx.push(i as u32);
1557 values.push(2.0);
1558 if local + 1 < hard {
1559 col_idx.push((i + 1) as u32);
1560 values.push(-1.0);
1561 }
1562 row += 1;
1563 row_ptr.push(col_idx.len() as u32);
1564 }
1565 if bi + 1 < hard_sizes.len() {
1566 for _ in 0..easy_between {
1567 col_idx.push(row as u32);
1568 values.push(1.0);
1569 row += 1;
1570 row_ptr.push(col_idx.len() as u32);
1571 }
1572 }
1573 }
1574 Csr32Matrix::new(n, n, row_ptr, col_idx, values).unwrap()
1575 }
1576
1577 #[test]
1578 fn auto_solver_converges_on_spd_system() {
1579 let a = poisson_1d(64);
1580 let b = vec![1.0; 64];
1581 let mut x = vec![0.0; 64];
1582 let solver = HybitSolver::new();
1583 let report = solver.solve_csr32(&a, &b, &mut x).unwrap();
1584 assert!(report.converged());
1585 assert!(report.krylov_workspace_bytes > 0);
1586 }
1587
1588 #[test]
1589 fn forced_abtm_converges() {
1590 let a = poisson_1d(64);
1591 let b = vec![1.0; 64];
1592 let mut x = vec![0.0; 64];
1593 let mut solver = HybitSolver::new();
1594 solver.set_backend_policy(BackendPolicy::Abtm);
1595 let report = solver.solve_csr32(&a, &b, &mut x).unwrap();
1596 assert!(report.converged());
1597 assert_eq!(report.backend, MatrixBackend::Abtm);
1598 }
1599
1600 #[test]
1601 fn selective_direct_escalation_beats_plain_jacobi_pcg() {
1602 let a = block_diagonal(32, &[64], 0);
1603 let b = vec![1.0; a.nrows()];
1604 let options = SolverOptions {
1605 relative_tolerance: 1.0e-10,
1606 absolute_tolerance: 0.0,
1607 max_iterations: 100,
1608 };
1609 let jacobi = JacobiPreconditioner::from_csr32(&a).unwrap();
1610 let mut x_plain = vec![0.0; a.nrows()];
1611 let plain = pcg(&a, &jacobi, &b, &mut x_plain, options).unwrap();
1612
1613 let mut solver = HybitSolver::new();
1614 solver.set_options(options).unwrap();
1615 let mut x = vec![0.0; a.nrows()];
1616 let report = solver.solve_csr32(&a, &b, &mut x).unwrap();
1617 assert!(report.converged());
1618 assert_eq!(report.preconditioner, PreconditionerKind::Hybrid);
1619 assert!(report.iterations < plain.iterations);
1620 }
1621
1622 #[test]
1623 fn multi_region_overlap_detects_two_hard_blocks() {
1624 let a = block_diagonal(16, &[48, 48], 8);
1625 let b = vec![1.0; a.nrows()];
1626 let options = SolverOptions {
1627 relative_tolerance: 1.0e-10,
1628 absolute_tolerance: 0.0,
1629 max_iterations: 100,
1630 };
1631 let mut solver = HybitSolver::new();
1632 solver.set_options(options).unwrap();
1633 let mut x = vec![0.0; a.nrows()];
1634 let report = solver.solve_csr32(&a, &b, &mut x).unwrap();
1635 assert!(report.converged());
1636 assert!(report.local_direct_regions >= 2);
1637 }
1638
1639 #[test]
1640 fn hybrid_preconditioner_is_positive_on_overlapping_regions() {
1641 let a = poisson_1d(12);
1642 let hybrid =
1643 HybridPreconditioner::from_csr32(&a, vec![(0..8).collect(), (4..12).collect()])
1644 .unwrap();
1645 let r = vec![1.0; 12];
1646 let mut z = vec![0.0; 12];
1647 hybrid.apply(&r, &mut z).unwrap();
1648 let rz: f64 = r.iter().zip(&z).map(|(a, b)| a * b).sum();
1649 assert!(rz > 0.0);
1650 }
1651
1652 #[test]
1653 fn prepared_context_reuses_hybrid_factor_and_workspace() {
1654 let a = block_diagonal(32, &[64], 0);
1655 let mut solver = HybitSolver::new();
1656 solver
1657 .set_options(SolverOptions {
1658 relative_tolerance: 1.0e-10,
1659 absolute_tolerance: 0.0,
1660 max_iterations: 100,
1661 })
1662 .unwrap();
1663 let analysis = solver.analyze_csr32(&a).unwrap();
1664 let mut prepared = solver.prepare_csr32(&a, &analysis).unwrap();
1665 let workspace_bytes = prepared.krylov_workspace_bytes();
1666
1667 let b1 = vec![1.0; a.nrows()];
1668 let mut x1 = vec![0.0; a.nrows()];
1669 let first = prepared.solve(&a, &b1, &mut x1).unwrap();
1670 assert!(first.converged());
1671 assert!(prepared.has_cached_hybrid());
1672 assert!(!first.preconditioner_reused);
1673 assert!(first.local_factor_seconds >= 0.0);
1674
1675 let mut b2 = vec![1.0; a.nrows()];
1676 b2[0] = 2.0;
1677 let mut x2 = vec![0.0; a.nrows()];
1678 let second = prepared.solve(&a, &b2, &mut x2).unwrap();
1679 assert!(second.converged());
1680 assert!(second.preconditioner_reused);
1681 assert_eq!(second.probe_iterations, 0);
1682 assert_eq!(second.local_factor_seconds, 0.0);
1683 assert_eq!(second.analysis_seconds, 0.0);
1684 assert_eq!(second.prepare_seconds, 0.0);
1685 assert_eq!(second.solve_sequence, 2);
1686 assert_eq!(second.krylov_workspace_bytes, workspace_bytes);
1687 }
1688}