1use crate::algebra::prelude::*;
2use crate::config::kinds::SorMatSideKind;
3use crate::config::options::{KspOptions, PcOptions};
4use crate::error::KError;
5use crate::matrix::op::LinOp;
6#[cfg(feature = "backend-faer")]
7use crate::preconditioner::asm::AsmInnerPc;
8use crate::preconditioner::bddc::{BddcConstraintSelection, BddcScaling};
9use crate::preconditioner::mg::MgLevelPolicy;
10use crate::preconditioner::{PcSide, Preconditioner};
11use crate::utils::conditioning::ConditioningOptions;
12use std::str::FromStr;
13
14#[cfg(feature = "backend-faer")]
15use crate::preconditioner::amg::AMGConfig;
16#[cfg(feature = "backend-faer")]
17use crate::preconditioner::gamg::GamgConfig;
18
19#[cfg(not(feature = "backend-faer"))]
20#[derive(Clone, Debug)]
21pub struct AMGConfig;
22
23#[cfg(not(feature = "backend-faer"))]
24impl AMGConfig {
25 pub fn try_from_opts(_opts: &PcOptions) -> Result<Self, KError> {
26 Err(KError::Unsupported(
27 "AMG requires backend-faer; enable backend-faer to use AMG options",
28 ))
29 }
30}
31
32#[cfg(not(feature = "backend-faer"))]
33#[derive(Clone, Debug)]
34pub struct GamgConfig;
35
36#[cfg(not(feature = "backend-faer"))]
37impl GamgConfig {
38 pub fn try_from_opts(_opts: &PcOptions) -> Result<Self, KError> {
39 Err(KError::Unsupported(
40 "GAMG requires backend-faer; enable backend-faer to use GAMG options",
41 ))
42 }
43}
44
45#[cfg(feature = "backend-faer")]
46type MatSorSide = crate::preconditioner::sor::MatSorType;
47
48#[cfg(not(feature = "backend-faer"))]
49bitflags::bitflags! {
50 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
51 pub struct MatSorSide: u32 {
52 const APPLY_LOWER = 0b0001;
53 const APPLY_UPPER = 0b0010;
54 const SYMMETRIC_SWEEP = 0b0100;
55 const EISENSTAT = 0b1000;
56 }
57}
58
59#[cfg(not(feature = "backend-faer"))]
60#[derive(Clone, Copy, Debug, PartialEq)]
61pub enum AsmInnerPc {
62 Jacobi,
63 Ilu0,
64 Ilut {
65 drop_tol: R,
66 max_fill: usize,
67 },
68 Ilutp {
69 drop_tol: R,
70 max_fill: usize,
71 perm_tol: R,
72 },
73}
74
75#[cfg(feature = "backend-faer")]
76type ApproxInvKindAlias = crate::preconditioner::approxinv_csr::ApproxInvKind;
77#[cfg(not(feature = "backend-faer"))]
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub enum ApproxInvKindAlias {
80 FSAI,
81 SPAI,
82}
83
84#[cfg(test)]
85use std::cell::Cell;
86
87#[cfg(test)]
88thread_local! {
89 static CHAIN_STRICT_OVERRIDE: Cell<Option<bool>> = Cell::new(None);
90}
91
92#[cfg(test)]
93pub(crate) struct ChainStrictGuard(Option<bool>);
94
95#[cfg(test)]
96impl Drop for ChainStrictGuard {
97 fn drop(&mut self) {
98 CHAIN_STRICT_OVERRIDE.with(|cell| cell.set(self.0));
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum PcType {
105 Jacobi,
106 Ilu0,
107 None,
108 Ilu,
109 Ilut,
110 Ilutp,
111 Ilup,
112 BlockJacobi,
113 Sor,
114 Asm,
115 Chebyshev,
116 Amg,
117 ApproxInverse,
118 FieldSplit,
119 Shell,
120 Ksp,
121 Mg,
122 Bddc,
123 Gamg,
124 Lu,
125 Qr,
126 #[cfg_attr(docsrs, doc(cfg(feature = "superlu_dist")))]
127 #[cfg(feature = "superlu_dist")]
128 SuperLuDist,
129}
130
131impl FromStr for PcType {
132 type Err = KError;
133
134 fn from_str(s: &str) -> Result<Self, Self::Err> {
135 match s.to_lowercase().as_str() {
136 "jacobi" => Ok(PcType::Jacobi),
137 "ilu0" => Ok(PcType::Ilu0),
138 "none" => Ok(PcType::None),
139 "ilu" => Ok(PcType::Ilu),
140 "ilut" => Ok(PcType::Ilut),
141 "ilutp" => Ok(PcType::Ilutp),
142 "ilup" => Ok(PcType::Ilup),
143 "block_jacobi" => Ok(PcType::BlockJacobi),
144 "sor" => Ok(PcType::Sor),
145 "asm" | "ras" => Ok(PcType::Asm),
146 "chebyshev" => Ok(PcType::Chebyshev),
147 "amg" => Ok(PcType::Amg),
148 "approxinv" | "approxinverse" => Ok(PcType::ApproxInverse),
149 "fieldsplit" => Ok(PcType::FieldSplit),
150 "shell" => Ok(PcType::Shell),
151 "ksp" => Ok(PcType::Ksp),
152 "mg" => Ok(PcType::Mg),
153 "bddc" => Ok(PcType::Bddc),
154 "gamg" => Ok(PcType::Gamg),
155 "lu" => Ok(PcType::Lu),
156 "qr" => Ok(PcType::Qr),
157 "superludist" => {
158 #[cfg(feature = "superlu_dist")]
159 {
160 Ok(PcType::SuperLuDist)
161 }
162 #[cfg(not(feature = "superlu_dist"))]
163 {
164 Err(KError::Unsupported(
165 "build without feature=\"superlu_dist\"".into(),
166 ))
167 }
168 }
169 other => Err(KError::UnrecognizedPcType(other.to_string())),
170 }
171 }
172}
173
174#[derive(Debug, Clone)]
176pub struct DeferredPcInfo {
177 pub pc_type: PcType,
178 pub options: Option<PcOptions>,
179}
180
181#[derive(Debug, Clone)]
183pub struct PcContext {
184 pub pc_type: PcType,
185 pub options: Option<PcOptions>,
186}
187
188impl PcContext {
189 pub fn new(pc_type: PcType, options: Option<PcOptions>) -> Self {
190 Self { pc_type, options }
191 }
192
193 pub fn view(&self) -> crate::utils::diagnostics::PcDiagnostics {
194 crate::utils::diagnostics::PcDiagnostics::from_options(
195 Some(self.pc_type),
196 self.options.as_ref(),
197 )
198 }
199}
200
201impl From<DeferredPcInfo> for PcContext {
202 fn from(spec: DeferredPcInfo) -> Self {
203 Self::new(spec.pc_type, spec.options)
204 }
205}
206
207pub struct NoOpPreconditioner;
209
210impl Preconditioner for NoOpPreconditioner {
211 fn setup(&mut self, _a: &dyn LinOp<S = S>) -> Result<(), KError> {
212 Ok(())
213 }
214 fn apply(&self, _side: PcSide, r: &[S], z: &mut [S]) -> Result<(), KError> {
215 z.copy_from_slice(r);
216 Ok(())
217 }
218
219 fn apply_mut(&mut self, side: PcSide, x: &[S], y: &mut [S]) -> Result<(), KError> {
220 self.apply(side, x, y)
221 }
222}
223
224#[derive(Debug, Clone)]
226pub enum PcConfig {
227 None,
228 Jacobi,
229 BlockJacobi {
230 block: usize,
231 },
232 Ilu0 {
233 conditioning: ConditioningOptions,
234 },
235 Iluk {
236 level: usize,
237 conditioning: ConditioningOptions,
238 },
239 Ilut {
240 drop_tol: R,
241 max_fill: usize,
242 reordering: Option<String>,
243 conditioning: ConditioningOptions,
244 },
245 Ilutp {
246 drop_tol: R,
247 max_fill: usize,
248 perm_tol: R,
249 reordering: Option<String>,
250 conditioning: ConditioningOptions,
251 },
252 Milu0 {
253 conditioning: ConditioningOptions,
254 },
255 Sor {
256 omega: R,
257 sweeps: usize,
258 mat_side: MatSorSide,
259 },
260 Chebyshev {
261 degree: usize,
262 eig_lo: R,
263 eig_hi: R,
264 },
265 Asm {
266 overlap: usize,
267 subdomain_hint: Option<usize>,
268 block_solver: Option<String>,
269 mode: Option<String>,
270 weighting: Option<String>,
271 inner_pc: AsmInnerPc,
272 },
273 Amg {
274 config: AMGConfig,
275 conditioning: ConditioningOptions,
276 },
277 ApproxInv {
278 kind: ApproxInvKindAlias,
279 levels: usize,
280 max_per_col: usize,
281 drop_tol: R,
282 reg: R,
283 max_cond: R,
284 parallel: bool,
285 },
286 FieldSplit {
287 block_sizes: Vec<usize>,
288 child_pc_type: Option<String>,
289 options: PcOptions,
290 },
291 Shell {
292 name: Option<String>,
293 apply_transpose: Option<String>,
294 apply_conjugate_transpose: Option<String>,
295 apply_symmetric: Option<String>,
296 apply_symmetric_left: Option<String>,
297 apply_symmetric_right: Option<String>,
298 setup: Option<String>,
299 destroy: Option<String>,
300 context: Option<String>,
301 },
302 Ksp {
303 ksp_options: KspOptions,
304 pc_options: PcOptions,
305 },
306 Mg {
307 levels: usize,
308 cycle_type: Option<String>,
309 smoother: Option<String>,
310 smoother_steps: Option<usize>,
311 coarsen_type: Option<String>,
312 interpolation_type: Option<String>,
313 restriction_type: Option<String>,
314 coarse_pc_type: Option<String>,
315 coarse_ksp_type: Option<String>,
316 coarse_ksp_maxits: Option<usize>,
317 coarse_ksp_rtol: Option<R>,
318 level_policies: Vec<MgLevelPolicy>,
319 },
320 Bddc {
321 coarse_ksp_type: Option<String>,
322 coarse_pc_type: Option<String>,
323 use_vertices: bool,
324 constraint_selection: BddcConstraintSelection,
325 scaling: BddcScaling,
326 },
327 Gamg {
328 config: GamgConfig,
329 conditioning: ConditioningOptions,
330 },
331 Lu,
332 Qr,
333 #[cfg_attr(docsrs, doc(cfg(feature = "superlu_dist")))]
334 #[cfg(feature = "superlu_dist")]
335 SuperLuDist,
336}
337
338fn parse_mg_level_policy(value: &str) -> Result<MgLevelPolicy, KError> {
339 let mut policy = MgLevelPolicy::default();
340 for token in value.split(',').map(str::trim).filter(|t| !t.is_empty()) {
341 if let Some((k, v)) = token.split_once('=') {
342 match k.trim() {
343 "level" => {
344 policy.level = v.trim().parse().map_err(|_| {
345 KError::InvalidInput(format!("invalid mg policy level: {v}"))
346 })?
347 }
348 "level_key" | "family_key" => policy.level_key = Some(v.trim().to_lowercase()),
349 "smoother" => policy.smoother_type = Some(v.trim().to_lowercase()),
350 "smoother_family" | "family" => {
351 policy.smoother_family = Some(v.trim().to_lowercase())
352 }
353 "steps" | "sweeps" => {
354 policy.smoother_steps = Some(v.trim().parse().map_err(|_| {
355 KError::InvalidInput(format!("invalid mg policy steps: {v}"))
356 })?)
357 }
358 "pre_sweeps" => {
359 policy.pre_sweeps =
360 Some(v.trim().parse().map_err(|_| {
361 KError::InvalidInput(format!("invalid mg pre sweeps: {v}"))
362 })?)
363 }
364 "post_sweeps" => {
365 policy.post_sweeps = Some(v.trim().parse().map_err(|_| {
366 KError::InvalidInput(format!("invalid mg post sweeps: {v}"))
367 })?)
368 }
369 "side" | "smoother_side" => {
370 policy.smoother_side = Some(PcSide::from_str(v.trim())?)
371 }
372 "coarse_pc" => policy.coarse_pc_type = Some(v.trim().to_lowercase()),
373 "coarse_ksp" => policy.coarse_ksp_type = Some(v.trim().to_lowercase()),
374 "coarse_maxits" => {
375 policy.coarse_ksp_maxits = Some(v.trim().parse().map_err(|_| {
376 KError::InvalidInput(format!("invalid mg coarse maxits: {v}"))
377 })?)
378 }
379 "coarse_rtol" => {
380 policy.coarse_ksp_rtol = Some(v.trim().parse().map_err(|_| {
381 KError::InvalidInput(format!("invalid mg coarse rtol: {v}"))
382 })?)
383 }
384 "coarse_side" => policy.coarse_side = Some(PcSide::from_str(v.trim())?),
385 "coarse_route" | "coarse_routes" => {
386 let routes = v
387 .split('|')
388 .flat_map(|chunk| chunk.split(','))
389 .map(str::trim)
390 .filter(|s| !s.is_empty())
391 .map(|s| s.to_lowercase())
392 .collect::<Vec<_>>();
393 if !routes.is_empty() {
394 policy.coarse_routes = Some(routes);
395 }
396 }
397 "ksp" | "ksp_type" => policy.level_ksp_type = Some(v.trim().to_lowercase()),
398 "pc" | "pc_type" => policy.level_pc_type = Some(v.trim().to_lowercase()),
399 "ksp_maxits" | "maxits" => {
400 policy.level_ksp_maxits = Some(v.trim().parse().map_err(|_| {
401 KError::InvalidInput(format!("invalid mg level maxits: {v}"))
402 })?)
403 }
404 "ksp_rtol" | "rtol" => {
405 policy.level_ksp_rtol =
406 Some(v.trim().parse().map_err(|_| {
407 KError::InvalidInput(format!("invalid mg level rtol: {v}"))
408 })?)
409 }
410 _ => {}
411 }
412 }
413 }
414 Ok(policy)
415}
416
417fn mg_policy_from_scoped_level(
418 global: &PcOptions,
419 level: usize,
420 scoped: &PcOptions,
421) -> MgLevelPolicy {
422 let coarse_routes_from_policy = scoped
423 .amg_dist_coarse_policy
424 .as_deref()
425 .or(global.amg_dist_coarse_policy.as_deref())
426 .map(|policy| match policy {
427 "local" | "local_prototype" | "hybrid" => {
428 vec!["pc_apply".to_string(), "nested_ksp".to_string()]
429 }
430 "root" | "root_gather" | "auto" | "superlu_dist" => {
431 vec!["nested_ksp".to_string(), "pc_apply".to_string()]
432 }
433 _ => vec!["nested_ksp".to_string(), "pc_apply".to_string()],
434 });
435 let inherited_pc_type = scoped
436 .pc_type
437 .clone()
438 .or_else(|| scoped.amg_smoother.clone())
439 .or_else(|| global.pc_mg_smoother.clone());
440 let inherited_ksp_type = scoped
441 .pc_ksp_ksp_type
442 .clone()
443 .or_else(|| global.pc_ksp_ksp_type.clone());
444 let inherited_ksp_pc = scoped
445 .pc_ksp_pc_type
446 .clone()
447 .or_else(|| scoped.pc_type.clone())
448 .or_else(|| global.pc_ksp_pc_type.clone())
449 .or_else(|| global.pc_mg_smoother.clone());
450 MgLevelPolicy {
451 level,
452 level_key: None,
453 smoother_type: inherited_pc_type.clone().map(|v| v.to_lowercase()),
454 smoother_family: inherited_pc_type.map(|v| v.to_lowercase()),
455 smoother_steps: scoped.pc_mg_smoother_steps.or(global.pc_mg_smoother_steps),
456 pre_sweeps: scoped.amg_sweeps_down.or(global.amg_sweeps_down),
457 post_sweeps: scoped.amg_sweeps_up.or(global.amg_sweeps_up),
458 smoother_side: None,
459 coarse_pc_type: scoped
460 .pc_mg_coarse_pc_type
461 .clone()
462 .or(scoped.amg_coarse_solver.clone())
463 .or_else(|| scoped.pc_type.clone())
464 .or_else(|| global.pc_mg_coarse_pc_type.clone())
465 .or_else(|| global.amg_coarse_solver.clone())
466 .map(|v| v.to_lowercase()),
467 coarse_ksp_type: scoped
468 .pc_mg_coarse_ksp_type
469 .clone()
470 .or_else(|| global.pc_mg_coarse_ksp_type.clone())
471 .map(|v| v.to_lowercase()),
472 coarse_ksp_maxits: scoped
473 .pc_mg_coarse_ksp_maxits
474 .or(scoped.pc_ksp_maxits)
475 .or(global.pc_mg_coarse_ksp_maxits)
476 .or(global.pc_ksp_maxits),
477 coarse_ksp_rtol: scoped
478 .pc_mg_coarse_ksp_rtol
479 .or(scoped.pc_ksp_rtol)
480 .or(global.pc_mg_coarse_ksp_rtol)
481 .or(global.pc_ksp_rtol),
482 coarse_side: None,
483 coarse_routes: scoped
484 .amg_dist_coarse_solver_route
485 .as_ref()
486 .map(|v| {
487 v.split(',')
488 .map(str::trim)
489 .filter(|s| !s.is_empty())
490 .map(|s| s.to_lowercase())
491 .collect::<Vec<_>>()
492 })
493 .filter(|v| !v.is_empty())
494 .or(coarse_routes_from_policy)
495 .or_else(|| {
496 global.amg_dist_coarse_solver_route.as_ref().map(|v| {
497 v.split(',')
498 .map(str::trim)
499 .filter(|s| !s.is_empty())
500 .map(|s| s.to_lowercase())
501 .collect::<Vec<_>>()
502 })
503 })
504 .filter(|v| !v.is_empty()),
505 level_ksp_type: inherited_ksp_type.map(|v| v.to_lowercase()),
506 level_pc_type: inherited_ksp_pc.map(|v| v.to_lowercase()),
507 level_ksp_maxits: scoped.pc_ksp_maxits.or(global.pc_ksp_maxits),
508 level_ksp_rtol: scoped.pc_ksp_rtol.or(global.pc_ksp_rtol),
509 }
510}
511
512fn merge_mg_policy(dst: &mut MgLevelPolicy, src: &MgLevelPolicy) {
513 if let Some(v) = src.level_key.as_ref() {
514 dst.level_key = Some(v.clone());
515 }
516 if let Some(v) = src.smoother_type.as_ref() {
517 dst.smoother_type = Some(v.clone());
518 }
519 if let Some(v) = src.smoother_family.as_ref() {
520 dst.smoother_family = Some(v.clone());
521 }
522 if let Some(v) = src.smoother_steps {
523 dst.smoother_steps = Some(v);
524 }
525 if let Some(v) = src.pre_sweeps {
526 dst.pre_sweeps = Some(v);
527 }
528 if let Some(v) = src.post_sweeps {
529 dst.post_sweeps = Some(v);
530 }
531 if let Some(v) = src.smoother_side {
532 dst.smoother_side = Some(v);
533 }
534 if let Some(v) = src.coarse_pc_type.as_ref() {
535 dst.coarse_pc_type = Some(v.clone());
536 }
537 if let Some(v) = src.coarse_ksp_type.as_ref() {
538 dst.coarse_ksp_type = Some(v.clone());
539 }
540 if let Some(v) = src.coarse_ksp_maxits {
541 dst.coarse_ksp_maxits = Some(v);
542 }
543 if let Some(v) = src.coarse_ksp_rtol {
544 dst.coarse_ksp_rtol = Some(v);
545 }
546 if let Some(v) = src.coarse_side {
547 dst.coarse_side = Some(v);
548 }
549 if let Some(v) = src.coarse_routes.as_ref() {
550 dst.coarse_routes = Some(v.clone());
551 }
552 if let Some(v) = src.level_ksp_type.as_ref() {
553 dst.level_ksp_type = Some(v.clone());
554 }
555 if let Some(v) = src.level_pc_type.as_ref() {
556 dst.level_pc_type = Some(v.clone());
557 }
558 if let Some(v) = src.level_ksp_maxits {
559 dst.level_ksp_maxits = Some(v);
560 }
561 if let Some(v) = src.level_ksp_rtol {
562 dst.level_ksp_rtol = Some(v);
563 }
564}
565
566impl PcConfig {
567 pub fn from_type_and_options(
568 pc_type: PcType,
569 opts: Option<&PcOptions>,
570 ) -> Result<Self, KError> {
571 use PcType::*;
572 let default_opts = PcOptions::default();
573 let o = opts.unwrap_or(&default_opts);
574 let conditioning = o.conditioning_options()?;
575 Ok(match pc_type {
576 None => PcConfig::None,
577
578 Jacobi => match o.jacobi_block_size {
579 Some(b) if b > 1 => PcConfig::BlockJacobi { block: b },
580 _ => PcConfig::Jacobi,
581 },
582
583 Ilu0 => PcConfig::Ilu0 {
584 conditioning: conditioning.clone(),
585 },
586
587 Ilu => match o.ilu_variant.as_deref() {
588 Some("ilu0") | Option::None
589 if o.ilu_level.is_none() && o.ilut_drop_tol.is_none() =>
590 {
591 PcConfig::Ilu0 {
592 conditioning: conditioning.clone(),
593 }
594 }
595 Some("iluk") | Option::None if o.ilu_level.is_some() => {
596 let level = o.ilu_level.ok_or_else(|| {
597 KError::InvalidInput("iluk requires PcOptions.ilu_level".into())
598 })?;
599 PcConfig::Iluk {
600 level,
601 conditioning: conditioning.clone(),
602 }
603 }
604 Some("ilut") | Option::None if o.ilut_drop_tol.is_some() => PcConfig::Ilut {
605 drop_tol: o.ilut_drop_tol.unwrap_or(1e-4),
606 max_fill: o.ilut_max_fill.unwrap_or(20),
607 reordering: o.ilu_reordering.clone(),
608 conditioning: conditioning.clone(),
609 },
610 Some("milu0") => PcConfig::Milu0 {
611 conditioning: conditioning.clone(),
612 },
613 Some(other) => {
614 return Err(KError::InvalidInput(format!(
615 "unknown ilu_variant: {other}"
616 )));
617 }
618 Option::None => PcConfig::Ilu0 {
619 conditioning: conditioning.clone(),
620 },
621 },
622 Ilut => PcConfig::Ilut {
623 drop_tol: o.ilut_drop_tol.unwrap_or(1e-4),
624 max_fill: o.ilut_max_fill.unwrap_or(20),
625 reordering: o.ilu_reordering.clone(),
626 conditioning: conditioning.clone(),
627 },
628 Ilutp => PcConfig::Ilutp {
629 drop_tol: o.ilutp_drop_tol.unwrap_or(1e-4),
630 max_fill: o.ilutp_max_fill.unwrap_or(10),
631 perm_tol: o.ilutp_perm_tol.unwrap_or(0.1),
632 reordering: o.ilu_reordering.clone(),
633 conditioning: conditioning.clone(),
634 },
635 Ilup => PcConfig::Iluk {
636 level: o.ilu_level.unwrap_or(0),
637 conditioning: conditioning.clone(),
638 },
639
640 Sor => {
641 let mut mat_side = if let Some(ref side) = o.sor_mat_side {
642 match SorMatSideKind::from_str(side)? {
643 SorMatSideKind::Lower => MatSorSide::APPLY_LOWER,
644 SorMatSideKind::Upper => MatSorSide::APPLY_UPPER,
645 SorMatSideKind::Symmetric => MatSorSide::SYMMETRIC_SWEEP,
646 SorMatSideKind::Eisenstat => {
647 MatSorSide::SYMMETRIC_SWEEP | MatSorSide::EISENSTAT
648 }
649 }
650 } else {
651 MatSorSide::APPLY_LOWER
652 };
653 if o.sor_symmetric.unwrap_or(false) {
654 mat_side |= MatSorSide::SYMMETRIC_SWEEP;
655 }
656 let omega = o.sor_omega.unwrap_or(1.0);
657 if !(0.0..2.0).contains(&omega) {
658 return Err(KError::InvalidInput("sor_omega must be in (0,2)".into()));
659 }
660 PcConfig::Sor {
661 omega,
662 sweeps: o.sor_sweeps.unwrap_or(1),
663 mat_side,
664 }
665 }
666
667 Chebyshev => {
668 let degree = o.cheb_degree.unwrap_or(2);
669 let eig_lo = o.cheb_eig_lo.unwrap_or(0.0);
670 let eig_hi = o.cheb_eig_hi.unwrap_or(1.0);
671 if degree < 1 || eig_hi <= eig_lo || eig_lo < 0.0 {
672 return Err(KError::InvalidInput("invalid Chebyshev bounds".into()));
673 }
674 PcConfig::Chebyshev {
675 degree,
676 eig_lo,
677 eig_hi,
678 }
679 }
680
681 Asm => PcConfig::Asm {
682 overlap: o.asm_overlap.unwrap_or(0),
683 subdomain_hint: o.asm_subdomain_size,
684 block_solver: o.asm_block_solver.clone(),
685 mode: o.asm_mode.clone(),
686 weighting: o.asm_weighting.clone(),
687 inner_pc: match o.asm_inner_pc.as_deref() {
688 Some("jacobi") => AsmInnerPc::Jacobi,
689 Some("ilut") => AsmInnerPc::Ilut {
690 drop_tol: o.ilut_drop_tol.unwrap_or(1e-4),
691 max_fill: o.ilut_max_fill.unwrap_or(20),
692 },
693 Some("ilutp") => AsmInnerPc::Ilutp {
694 drop_tol: o.ilutp_drop_tol.unwrap_or(1e-4),
695 max_fill: o.ilutp_max_fill.unwrap_or(10),
696 perm_tol: o.ilutp_perm_tol.unwrap_or(0.1),
697 },
698 Some("ilu") | Some("ilu0") | std::option::Option::None => AsmInnerPc::Ilu0,
699 Some(other) => {
700 return Err(KError::InvalidInput(format!(
701 "unknown pc_asm_inner_pc: {other}"
702 )));
703 }
704 },
705 },
706 Amg => {
707 let cfg = AMGConfig::try_from_opts(o)?;
708 PcConfig::Amg {
709 config: cfg,
710 conditioning: conditioning.clone(),
711 }
712 }
713
714 ApproxInverse => {
715 let kind = match o
719 .approxinv_kind
720 .as_deref()
721 .unwrap_or("fsai")
722 .to_lowercase()
723 .as_str()
724 {
725 "fsai" => ApproxInvKindAlias::FSAI,
726 "spai" => ApproxInvKindAlias::SPAI,
727 other => {
728 return Err(KError::InvalidInput(format!(
729 "unknown pc_approxinv_kind: {other}"
730 )));
731 }
732 };
733 let levels = o.approxinv_levels.unwrap_or(1);
734 let max_per_col = o.approxinv_max_per_col.unwrap_or(20);
735 let drop_tol = o.approxinv_drop_tol.or(o.drop_tol).unwrap_or(1e-3);
736 let reg = o.approxinv_reg.unwrap_or(1e-12);
737 let max_cond = o.approxinv_max_cond.unwrap_or(1e12);
738 let parallel = o.approxinv_parallel.unwrap_or(cfg!(feature = "rayon"));
739 PcConfig::ApproxInv {
740 kind,
741 levels,
742 max_per_col,
743 drop_tol,
744 reg,
745 max_cond,
746 parallel,
747 }
748 }
749 FieldSplit => {
750 let block_sizes = o
751 .pc_fieldsplit_block_sizes
752 .clone()
753 .unwrap_or_else(|| vec![1]);
754 PcConfig::FieldSplit {
755 block_sizes,
756 child_pc_type: o.pc_fieldsplit_child_pc_type.clone(),
757 options: o.clone(),
758 }
759 }
760 Shell => PcConfig::Shell {
761 name: o.pc_shell_apply.clone().or_else(|| o.pc_shell_name.clone()),
762 apply_transpose: o.pc_shell_apply_transpose.clone(),
763 apply_conjugate_transpose: o.pc_shell_apply_conjugate_transpose.clone(),
764 apply_symmetric: o.pc_shell_apply_symmetric.clone(),
765 apply_symmetric_left: o.pc_shell_apply_symmetric_left.clone(),
766 apply_symmetric_right: o.pc_shell_apply_symmetric_right.clone(),
767 setup: o.pc_shell_setup.clone(),
768 destroy: o.pc_shell_destroy.clone(),
769 context: o.pc_shell_context.clone(),
770 },
771 Ksp => {
772 let mut ksp_options = o.resolved_pc_ksp_ksp_options();
773 if let Some(scoped) = o.pc_ksp_ksp_options.clone() {
774 ksp_options.overlay_from(scoped);
775 }
776 PcConfig::Ksp {
777 ksp_options,
778 pc_options: o.resolved_pc_ksp_pc_options(),
779 }
780 }
781 Mg => PcConfig::Mg {
782 levels: o.pc_mg_levels.unwrap_or(2),
783 cycle_type: o.pc_mg_cycle_type.clone(),
784 smoother: o.pc_mg_smoother.clone(),
785 smoother_steps: o.pc_mg_smoother_steps,
786 coarsen_type: o.pc_mg_coarsen_type.clone(),
787 interpolation_type: o.pc_mg_interpolation_type.clone(),
788 restriction_type: o.pc_mg_restriction_type.clone(),
789 coarse_pc_type: o.pc_mg_coarse_pc_type.clone(),
790 coarse_ksp_type: o.pc_mg_coarse_ksp_type.clone(),
791 coarse_ksp_maxits: o.pc_mg_coarse_ksp_maxits,
792 coarse_ksp_rtol: o.pc_mg_coarse_ksp_rtol,
793 level_policies: {
794 let mut merged: std::collections::BTreeMap<usize, MgLevelPolicy> =
795 std::collections::BTreeMap::new();
796 for policy in o
797 .pc_mg_level_policies
798 .as_ref()
799 .map(|entries| {
800 entries
801 .iter()
802 .filter_map(|entry| parse_mg_level_policy(entry).ok())
803 .collect::<Vec<_>>()
804 })
805 .unwrap_or_default()
806 {
807 let entry = merged.entry(policy.level).or_insert_with(|| MgLevelPolicy {
808 level: policy.level,
809 ..Default::default()
810 });
811 merge_mg_policy(entry, &policy);
812 }
813 for (level, scoped) in &o.pc_mg_level_scoped_options {
814 let scoped_policy = mg_policy_from_scoped_level(o, *level, scoped);
815 let entry = merged.entry(*level).or_insert_with(|| MgLevelPolicy {
816 level: *level,
817 ..Default::default()
818 });
819 merge_mg_policy(entry, &scoped_policy);
820 }
821 merged.into_values().collect()
822 },
823 },
824 Bddc => PcConfig::Bddc {
825 coarse_ksp_type: o.pc_bddc_coarse_ksp_type.clone(),
826 coarse_pc_type: o.pc_bddc_coarse_pc_type.clone(),
827 use_vertices: o.pc_bddc_use_vertices.unwrap_or(false),
828 constraint_selection: match o.pc_bddc_constraint_selection.as_deref() {
829 Some("vertices") => BddcConstraintSelection::Vertices,
830 Some("vertices_and_interface") | Some("all") => {
831 BddcConstraintSelection::VerticesAndInterface
832 }
833 _ => BddcConstraintSelection::Interface,
834 },
835 scaling: match o.pc_bddc_scaling.as_deref() {
836 Some("deluxe") | Some("deluxe_like") => BddcScaling::DeluxeLike,
837 _ => BddcScaling::Uniform,
838 },
839 },
840 Gamg => {
841 let cfg = GamgConfig::try_from_opts(o)?;
842 PcConfig::Gamg {
843 config: cfg,
844 conditioning: conditioning.clone(),
845 }
846 }
847
848 Lu => PcConfig::Lu,
849 Qr => PcConfig::Qr,
850 #[cfg(feature = "superlu_dist")]
851 SuperLuDist => PcConfig::SuperLuDist,
852 BlockJacobi => PcConfig::BlockJacobi {
853 block: o.jacobi_block_size.unwrap_or(1),
854 },
855 })
856 }
857}
858
859pub struct PcFactory;
878
879impl PcFactory {
880 fn composite_mode_from_opts(
881 opts: Option<&PcOptions>,
882 ) -> Result<crate::preconditioner::chain::PcCompositeMode, KError> {
883 match opts.and_then(|o| o.pc_composite_type.as_deref()) {
884 None | Some("multiplicative") | Some("mul") => {
885 Ok(crate::preconditioner::chain::PcCompositeMode::Multiplicative)
886 }
887 Some("additive") | Some("add") => {
888 Ok(crate::preconditioner::chain::PcCompositeMode::Additive)
889 }
890 Some("schur") => Ok(crate::preconditioner::chain::PcCompositeMode::Schur),
891 Some(other) => Err(KError::InvalidInput(format!(
892 "unknown pc_composite_type: {other}"
893 ))),
894 }
895 }
896
897 fn split_chain_tokens(chain: &str) -> Vec<String> {
898 chain
899 .replace("->", ",")
900 .replace('+', ",")
901 .split(',')
902 .map(|s| s.trim())
903 .filter(|s| !s.is_empty())
904 .map(|token| token.to_string())
905 .collect()
906 }
907
908 #[inline]
909 fn is_direct(pc: PcType) -> bool {
910 match pc {
911 PcType::Lu | PcType::Qr => true,
912 #[cfg(feature = "superlu_dist")]
913 PcType::SuperLuDist => true,
914 _ => false,
915 }
916 }
917
918 #[inline]
919 fn chain_strict() -> bool {
920 #[cfg(test)]
921 if let Some(val) = CHAIN_STRICT_OVERRIDE.with(|cell| cell.get()) {
922 return val;
923 }
924 std::env::var("KRYST_PC_CHAIN_STRICT")
927 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
928 .unwrap_or(false)
929 }
930
931 #[cfg(test)]
932 pub(crate) fn override_chain_strict(value: Option<bool>) -> ChainStrictGuard {
933 CHAIN_STRICT_OVERRIDE.with(|cell| {
934 let prev = cell.replace(value);
935 ChainStrictGuard(prev)
936 })
937 }
938
939 fn validate_chain_specs(specs: &[DeferredPcInfo]) -> Result<(), KError> {
943 if specs.is_empty() {
944 return Err(KError::InvalidInput("empty PC chain".into()));
945 }
946
947 let strict = Self::chain_strict();
948
949 let direct_positions: Vec<usize> = specs
951 .iter()
952 .enumerate()
953 .filter_map(|(i, s)| Self::is_direct(s.pc_type).then_some(i))
954 .collect();
955 if direct_positions.len() > 1 {
956 let msg = format!(
957 "PC chain contains multiple direct PCs at positions {direct_positions:?}. \
958 Stacking direct factorizations is usually unintended."
959 );
960 if strict {
961 return Err(KError::InvalidInput(msg));
962 } else {
963 log::warn!("{msg}");
964 }
965 }
966
967 if let Some((i, s)) = specs
969 .iter()
970 .enumerate()
971 .find(|(i, s)| Self::is_direct(s.pc_type) && *i + 1 != specs.len())
972 {
973 let msg = format!(
974 "Direct PC {:?} is not the last stage (index {}, chain len {}). \
975 Subsequent stages will likely be redundant or ignored.",
976 s.pc_type,
977 i,
978 specs.len()
979 );
980 if strict {
981 return Err(KError::InvalidInput(msg));
982 } else {
983 log::warn!("{msg}");
984 }
985 }
986
987 for w in specs.windows(2) {
991 if w[0].pc_type == w[1].pc_type {
992 let msg = format!(
993 "Consecutive duplicate PCs: {:?} -> {:?}. \
994 This is typically redundant unless options differ.",
995 w[0].pc_type, w[1].pc_type
996 );
997 log::warn!("{msg}");
998 }
999 }
1000
1001 for (i, spec) in specs.iter().enumerate() {
1003 if matches!(spec.pc_type, PcType::BlockJacobi)
1004 && let Some(ref o) = spec.options
1005 && o.jacobi_block_size.unwrap_or(1) <= 1
1006 {
1007 log::warn!(
1008 "PC chain stage {i}: BlockJacobi with block_size <= 1 behaves like Jacobi; \
1009 consider using 'jacobi' instead."
1010 );
1011 }
1012 }
1013
1014 Ok(())
1015 }
1016 pub fn create_preconditioner(
1017 pc_type: PcType,
1018 options: Option<&PcOptions>,
1019 ) -> Result<Box<dyn Preconditioner>, KError> {
1020 let cfg = PcConfig::from_type_and_options(pc_type, options)?;
1021 if let Some(pc) = crate::preconditioner::builders_none::try_build(&cfg)? {
1022 return Ok(pc);
1023 }
1024
1025 #[cfg(feature = "backend-faer")]
1026 if let Some(pc) = crate::preconditioner::builders_faer::try_build(&cfg)? {
1027 return Ok(pc);
1028 }
1029
1030 #[cfg(feature = "backend-nalgebra")]
1031 if let Some(pc) = crate::preconditioner::builders_nalgebra::try_build(&cfg)? {
1032 return Ok(pc);
1033 }
1034
1035 Err(KError::InvalidInput(format!(
1036 "Preconditioner {:?} requires a backend that is not enabled/supported for this build",
1037 pc_type
1038 )))
1039 }
1040
1041 pub fn create_from_options(opts: &PcOptions) -> Result<Box<dyn Preconditioner>, KError> {
1043 let pct = if let Some(ref s) = opts.pc_type {
1044 PcType::from_str(s)?
1045 } else {
1046 PcType::None
1047 };
1048 Self::create_preconditioner(pct, Some(opts))
1049 }
1050
1051 pub fn create_deferred_pc(
1052 pc_type: PcType,
1053 options: Option<PcOptions>,
1054 ) -> Result<DeferredPcInfo, KError> {
1055 Ok(DeferredPcInfo { pc_type, options })
1056 }
1057
1058 pub fn construct_deferred_preconditioner(
1059 info: DeferredPcInfo,
1060 _op: &dyn LinOp<S = S>,
1061 ) -> Result<Box<dyn Preconditioner>, KError> {
1062 Self::create_preconditioner(info.pc_type, info.options.as_ref())
1064 }
1065
1066 pub fn create_pc_chain_from_str(
1070 chain: &str,
1071 opts: Option<&PcOptions>,
1072 ) -> Result<Vec<DeferredPcInfo>, KError> {
1073 let mut specs = Vec::new();
1074 let prefixes = opts
1075 .and_then(|o| o.pc_composite_prefixes.clone())
1076 .unwrap_or_default();
1077 for (i, token) in Self::split_chain_tokens(chain).into_iter().enumerate() {
1078 let pct = PcType::from_str(&token)?;
1079 let mut stage_opts = opts.cloned();
1080 if let Some(prefix) = prefixes.get(i)
1081 && let Some(scoped) = opts.and_then(|o| o.scoped_child(prefix)).cloned()
1082 {
1083 let mut merged = stage_opts.unwrap_or_default();
1084 merged.overlay_from(scoped);
1085 stage_opts = Some(merged);
1086 }
1087 if token.eq_ignore_ascii_case("ras") {
1088 stage_opts.get_or_insert_with(PcOptions::default).asm_mode =
1089 Some("ras".to_string());
1090 }
1091 specs.push(DeferredPcInfo {
1092 pc_type: pct,
1093 options: stage_opts,
1094 });
1095 }
1096 if specs.is_empty() {
1097 return Err(KError::InvalidInput("empty PC chain".into()));
1098 }
1099 Self::validate_chain_specs(&specs)?;
1101 Ok(specs)
1102 }
1103
1104 pub fn create_pc_chain_candidates_from_str(
1108 chain: &str,
1109 opts: Option<&PcOptions>,
1110 ) -> Result<Vec<Vec<DeferredPcInfo>>, KError> {
1111 let mut candidates = Vec::new();
1112 for candidate in chain
1113 .split("||")
1114 .map(|s| s.trim())
1115 .filter(|s| !s.is_empty())
1116 {
1117 candidates.push(Self::create_pc_chain_from_str(candidate, opts)?);
1118 }
1119 if candidates.is_empty() {
1120 return Err(KError::InvalidInput("empty PC chain".into()));
1121 }
1122 Ok(candidates)
1123 }
1124
1125 pub fn construct_deferred_pc_chain(
1126 specs: Vec<DeferredPcInfo>,
1127 op: &dyn LinOp<S = S>,
1128 ) -> Result<Box<dyn Preconditioner>, KError> {
1129 Self::validate_chain_specs(&specs)?;
1131 use crate::preconditioner::chain::PcChain;
1132
1133 let mode = Self::composite_mode_from_opts(specs.first().and_then(|s| s.options.as_ref()))?;
1134 let mut stages: Vec<Box<dyn Preconditioner>> = Vec::with_capacity(specs.len());
1135 for (i, spec) in specs.into_iter().enumerate() {
1136 let pc_type = spec.pc_type;
1137 let stage = Self::construct_deferred_preconditioner(spec, op).map_err(|e| {
1138 KError::InvalidInput(format!("PC chain stage {i} ({pc_type:?}) failed: {e}",))
1139 })?;
1140 stages.push(stage);
1141 }
1142 Ok(Box::new(PcChain::with_mode(stages, mode)))
1143 }
1144
1145 pub fn create_pc_chain(
1146 chain: &str,
1147 op: &dyn LinOp<S = S>,
1148 opts: Option<PcOptions>,
1149 ) -> Result<Box<dyn Preconditioner>, KError> {
1150 let specs = Self::create_pc_chain_from_str(chain, opts.as_ref())?;
1151 Self::construct_deferred_pc_chain(specs, op)
1152 }
1153
1154 pub fn create_deferred_pc_chain_from_options(
1155 chain_opts: &[PcOptions],
1156 ) -> Result<Vec<DeferredPcInfo>, KError> {
1157 let mut specs = Vec::with_capacity(chain_opts.len());
1158 for co in chain_opts {
1159 let pct = if let Some(ref s) = co.pc_type {
1160 PcType::from_str(s)?
1161 } else {
1162 return Err(KError::InvalidInput(
1163 "PcOptions in chain missing pc_type".into(),
1164 ));
1165 };
1166 specs.push(DeferredPcInfo {
1167 pc_type: pct,
1168 options: Some(co.clone()),
1169 });
1170 }
1171 if specs.is_empty() {
1172 return Err(KError::InvalidInput("empty PcOptions.chain".into()));
1173 }
1174 Self::validate_chain_specs(&specs)?;
1176 Ok(specs)
1177 }
1178}
1179
1180#[derive(Clone, Debug)]
1182pub enum SparsityPattern {
1183 Manual(Vec<Vec<usize>>),
1184 Auto,
1185}
1186
1187#[cfg(test)]
1188mod tests {
1189 use super::*;
1190 use crate::preconditioner::Preconditioner;
1191
1192 #[cfg(feature = "dense-direct")]
1193 #[test]
1194 fn factory_builds_lu_qr() {
1195 let lu = PcFactory::create_preconditioner(PcType::from_str("lu").unwrap(), None).unwrap();
1196 let qr = PcFactory::create_preconditioner(PcType::from_str("qr").unwrap(), None).unwrap();
1197
1198 fn _is_pc(_p: &Box<dyn Preconditioner>) {}
1199 _is_pc(&lu);
1200 _is_pc(&qr);
1201 }
1202
1203 #[cfg(feature = "legacy-pc-bridge")]
1204 #[test]
1205 fn factory_uses_options_for_ilut() {
1206 let opts = PcOptions {
1207 pc_type: Some("ilut".into()),
1208 ilut_drop_tol: Some(1e-6),
1209 ilut_max_fill: Some(50),
1210 ..Default::default()
1211 };
1212 let pc = PcFactory::create_from_options(&opts).unwrap();
1213 fn _is_pc(_: &Box<dyn Preconditioner>) {}
1214 _is_pc(&pc);
1215 }
1216
1217 #[cfg(feature = "legacy-pc-bridge")]
1218 #[test]
1219 fn factory_builds_sor_from_options() {
1220 let opts = PcOptions {
1221 pc_type: Some("sor".into()),
1222 sor_omega: Some(1.5),
1223 sor_sweeps: Some(2),
1224 sor_mat_side: Some("lower".into()),
1225 ..Default::default()
1226 };
1227 let pc = PcFactory::create_from_options(&opts).unwrap();
1228 fn _is_pc(_: &Box<dyn Preconditioner>) {}
1229 _is_pc(&pc);
1230 }
1231
1232 #[cfg(feature = "backend-faer")]
1233 #[test]
1234 fn chebyshev_validates_bounds() {
1235 let bad = PcOptions {
1236 pc_type: Some("chebyshev".into()),
1237 cheb_degree: Some(0),
1238 cheb_eig_lo: Some(2.0),
1239 cheb_eig_hi: Some(1.0),
1240 ..Default::default()
1241 };
1242 let err = PcFactory::create_from_options(&bad).err().unwrap();
1243 assert!(matches!(err, KError::InvalidInput(_)));
1244 }
1245
1246 #[cfg(feature = "backend-faer")]
1247 #[test]
1248 fn factory_builds_asm_from_options() {
1249 let opts = crate::config::options::PcOptions {
1250 pc_type: Some("asm".into()),
1251 asm_block_solver: Some("ludense".into()),
1252 ..Default::default()
1253 };
1254 let pc = PcFactory::create_from_options(&opts).unwrap_or_else(|_| {
1255 PcFactory::create_from_options(&crate::config::options::PcOptions {
1257 pc_type: Some("asm".into()),
1258 asm_block_solver: Some("csr".into()),
1259 ..Default::default()
1260 })
1261 .unwrap()
1262 });
1263 fn _is_pc(_: &Box<dyn Preconditioner>) {}
1264 _is_pc(&pc);
1265 }
1266
1267 #[test]
1268 fn chain_direct_not_last_is_error_in_strict_mode() {
1269 let _guard = PcFactory::override_chain_strict(Some(true));
1271 let opts = crate::config::options::PcOptions::default();
1272
1273 let specs = PcFactory::create_pc_chain_from_str("lu->jacobi", Some(&opts));
1275 assert!(specs.is_err(), "expected validation error in strict mode");
1276 }
1277
1278 #[test]
1279 fn chain_duplicate_consecutive_warns_but_allows_by_default() {
1280 let opts = crate::config::options::PcOptions::default();
1282 let specs = PcFactory::create_pc_chain_from_str("ilu->ilu", Some(&opts))
1283 .expect("duplicates allowed with warning by default");
1284 assert!(!specs.is_empty());
1285 }
1286
1287 #[test]
1288 fn chain_fallback_parses_candidates_and_aliases() {
1289 let opts = crate::config::options::PcOptions::default();
1290 let candidates =
1291 PcFactory::create_pc_chain_candidates_from_str("amg||ras+ilutp", Some(&opts))
1292 .expect("fallback parse");
1293 assert_eq!(candidates.len(), 2);
1294 assert_eq!(candidates[0][0].pc_type, PcType::Amg);
1295 assert_eq!(candidates[1][0].pc_type, PcType::Asm);
1296 assert_eq!(candidates[1][1].pc_type, PcType::Ilutp);
1297 assert_eq!(
1298 candidates[1][0]
1299 .options
1300 .as_ref()
1301 .and_then(|o| o.asm_mode.as_deref()),
1302 Some("ras")
1303 );
1304 }
1305 #[test]
1306 fn chain_prefix_scoped_stage_options_are_merged() {
1307 let args = vec![
1308 "-pc_chain",
1309 "jacobi->ilu",
1310 "-pc_composite_prefixes",
1311 "s0_,s1_",
1312 "-s1_pc_ilu_levels",
1313 "4",
1314 ];
1315 let opts = crate::config::options::PcOptions::from_args(&args).unwrap();
1316 let specs =
1317 PcFactory::create_pc_chain_from_str(opts.pc_chain.as_deref().unwrap(), Some(&opts))
1318 .unwrap();
1319 assert_eq!(specs.len(), 2);
1320 assert_eq!(specs[1].options.as_ref().and_then(|o| o.ilu_level), Some(4));
1321 }
1322
1323 #[test]
1324 fn chain_ilu0_to_ilut_to_ilutp_promotion_path_parses_in_order() {
1325 let opts = crate::config::options::PcOptions::default();
1326 let specs = PcFactory::create_pc_chain_from_str("ilu0->ilut->ilutp", Some(&opts)).unwrap();
1327 let labels: Vec<PcType> = specs.iter().map(|s| s.pc_type.clone()).collect();
1328 assert_eq!(labels, vec![PcType::Ilu0, PcType::Ilut, PcType::Ilutp]);
1329 }
1330}