#[allow(unused_imports)]
use crate::algebra::prelude::*;
use crate::error::KError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReasonCategory {
Breakdown,
Nan,
Inf,
PcSetup,
PcApply,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FailureReasonKind {
Breakdown,
BreakdownBiCG,
IndefiniteMatrix,
IndefinitePc,
Nan,
Inf,
PcSetup,
PcApply,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FailureStage {
Setup,
Solve,
}
pub struct Convergence {
pub rtol: R,
pub atol: R,
pub dtol: R,
pub max_iters: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConvergedReason {
ConvergedRtol,
ConvergedAtol,
ConvergedTrustRegion,
ConvergedHappyBreakdown,
DivergedNan,
DivergedInf,
DivergedDtol,
DivergedMaxIts,
DivergedBreakdown,
DivergedArnoldiRankLoss,
DivergedBreakdownBiCG,
DivergedReducedSystemSingular,
DivergedIndefiniteMatrix,
DivergedIndefinitePC,
DivergedPcSetupFailed,
DivergedPcFailed,
StoppedByMonitor,
Continued,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AcceptanceStatus {
Ok,
OkWithWarning,
ContractMismatch,
Breakdown,
Stagnated,
Failed,
}
impl AcceptanceStatus {
pub fn as_str(self) -> &'static str {
match self {
AcceptanceStatus::Ok => "ok",
AcceptanceStatus::OkWithWarning => "ok_with_warning",
AcceptanceStatus::ContractMismatch => "contract_mismatch",
AcceptanceStatus::Breakdown => "breakdown",
AcceptanceStatus::Stagnated => "stagnated",
AcceptanceStatus::Failed => "failed",
}
}
pub fn is_accepted(self) -> bool {
matches!(self, AcceptanceStatus::Ok | AcceptanceStatus::OkWithWarning)
}
}
pub fn classify_acceptance_status(
reason: ConvergedReason,
true_residual: f64,
tol: f64,
) -> AcceptanceStatus {
let meets_true_residual = true_residual.is_finite() && true_residual < tol;
if meets_true_residual {
if reason.is_converged() {
return AcceptanceStatus::Ok;
}
return AcceptanceStatus::OkWithWarning;
}
match reason {
ConvergedReason::ConvergedRtol
| ConvergedReason::ConvergedAtol
| ConvergedReason::ConvergedTrustRegion
| ConvergedReason::ConvergedHappyBreakdown
| ConvergedReason::DivergedIndefiniteMatrix
| ConvergedReason::DivergedIndefinitePC => AcceptanceStatus::ContractMismatch,
ConvergedReason::DivergedBreakdown
| ConvergedReason::DivergedArnoldiRankLoss
| ConvergedReason::DivergedBreakdownBiCG
| ConvergedReason::DivergedReducedSystemSingular
| ConvergedReason::DivergedNan
| ConvergedReason::DivergedInf => AcceptanceStatus::Breakdown,
ConvergedReason::DivergedDtol
| ConvergedReason::DivergedMaxIts
| ConvergedReason::StoppedByMonitor => AcceptanceStatus::Stagnated,
ConvergedReason::DivergedPcSetupFailed
| ConvergedReason::DivergedPcFailed
| ConvergedReason::Continued => AcceptanceStatus::Failed,
}
}
impl ConvergedReason {
pub fn from_failure_kind(kind: FailureReasonKind) -> Self {
match kind {
FailureReasonKind::Breakdown => ConvergedReason::DivergedBreakdown,
FailureReasonKind::BreakdownBiCG => ConvergedReason::DivergedBreakdownBiCG,
FailureReasonKind::IndefiniteMatrix => ConvergedReason::DivergedIndefiniteMatrix,
FailureReasonKind::IndefinitePc => ConvergedReason::DivergedIndefinitePC,
FailureReasonKind::Nan => ConvergedReason::DivergedNan,
FailureReasonKind::Inf => ConvergedReason::DivergedInf,
FailureReasonKind::PcSetup => ConvergedReason::DivergedPcSetupFailed,
FailureReasonKind::PcApply => ConvergedReason::DivergedPcFailed,
}
}
pub fn category(self) -> Option<ReasonCategory> {
match self {
ConvergedReason::DivergedBreakdown
| ConvergedReason::DivergedArnoldiRankLoss
| ConvergedReason::DivergedBreakdownBiCG
| ConvergedReason::DivergedReducedSystemSingular => Some(ReasonCategory::Breakdown),
ConvergedReason::DivergedNan => Some(ReasonCategory::Nan),
ConvergedReason::DivergedInf => Some(ReasonCategory::Inf),
ConvergedReason::DivergedPcSetupFailed => Some(ReasonCategory::PcSetup),
ConvergedReason::DivergedPcFailed => Some(ReasonCategory::PcApply),
_ => None,
}
}
pub fn petsc_reason(self) -> &'static str {
match self {
ConvergedReason::ConvergedRtol => "KSP_CONVERGED_RTOL",
ConvergedReason::ConvergedAtol => "KSP_CONVERGED_ATOL",
ConvergedReason::ConvergedTrustRegion => "KSP_CONVERGED_TRUST_REGION",
ConvergedReason::ConvergedHappyBreakdown => "KSP_CONVERGED_HAPPY_BREAKDOWN",
ConvergedReason::DivergedNan => "KSP_DIVERGED_NANORINF",
ConvergedReason::DivergedInf => "KSP_DIVERGED_NANORINF",
ConvergedReason::DivergedDtol => "KSP_DIVERGED_DTOL",
ConvergedReason::DivergedMaxIts => "KSP_DIVERGED_ITS",
ConvergedReason::DivergedBreakdown => "KSP_DIVERGED_BREAKDOWN",
ConvergedReason::DivergedArnoldiRankLoss => "KSP_DIVERGED_BREAKDOWN",
ConvergedReason::DivergedBreakdownBiCG => "KSP_DIVERGED_BREAKDOWN_BICG",
ConvergedReason::DivergedReducedSystemSingular => "KSP_DIVERGED_BREAKDOWN",
ConvergedReason::DivergedIndefiniteMatrix => "KSP_DIVERGED_INDEFINITE_MAT",
ConvergedReason::DivergedIndefinitePC => "KSP_DIVERGED_INDEFINITE_PC",
ConvergedReason::DivergedPcSetupFailed => "KSP_DIVERGED_PCSETUP_FAILED",
ConvergedReason::DivergedPcFailed => "KSP_DIVERGED_PC_FAILED",
ConvergedReason::StoppedByMonitor => "KSP_DIVERGED_USER",
ConvergedReason::Continued => "KSP_CONVERGED_ITERATING",
}
}
pub fn from_non_finite(value: R) -> Option<Self> {
if value.is_nan() {
Some(ConvergedReason::from_failure_kind(FailureReasonKind::Nan))
} else if value.is_infinite() {
Some(ConvergedReason::from_failure_kind(FailureReasonKind::Inf))
} else {
None
}
}
pub fn is_converged(self) -> bool {
matches!(
self,
ConvergedReason::ConvergedRtol
| ConvergedReason::ConvergedAtol
| ConvergedReason::ConvergedTrustRegion
| ConvergedReason::ConvergedHappyBreakdown
)
}
pub fn is_diverged(self) -> bool {
!matches!(self, ConvergedReason::Continued) && !self.is_converged()
}
}
pub struct ReasonEmitter;
impl ReasonEmitter {
#[inline(always)]
pub fn non_finite(value: R) -> Option<ConvergedReason> {
ConvergedReason::from_non_finite(value)
}
#[inline(always)]
pub fn breakdown() -> ConvergedReason {
ConvergedReason::from_failure_kind(FailureReasonKind::Breakdown)
}
#[inline(always)]
pub fn breakdown_bicg() -> ConvergedReason {
ConvergedReason::from_failure_kind(FailureReasonKind::BreakdownBiCG)
}
#[inline(always)]
pub fn indefinite_matrix() -> ConvergedReason {
ConvergedReason::from_failure_kind(FailureReasonKind::IndefiniteMatrix)
}
#[inline(always)]
pub fn indefinite_pc() -> ConvergedReason {
ConvergedReason::from_failure_kind(FailureReasonKind::IndefinitePc)
}
#[inline]
pub fn from_error(err: &KError, stage: FailureStage) -> Option<ConvergedReason> {
map_kerror_to_reason(err, stage)
}
#[inline]
pub fn nested_pc_failure(err: &KError, stage: FailureStage) -> Option<NestedPcFailure> {
match err {
KError::PcFailed(msg) => {
let reason = match stage {
FailureStage::Setup => {
ConvergedReason::from_failure_kind(FailureReasonKind::PcSetup)
}
FailureStage::Solve => {
ConvergedReason::from_failure_kind(FailureReasonKind::PcApply)
}
};
Some(NestedPcFailure {
component: "pc",
reason,
iterations: 0,
final_norm: None,
residual_history_summary: None,
detail: format!("stage={stage:?} detail={msg}"),
})
}
KError::FactorError(msg) => {
let reason = match stage {
FailureStage::Setup => {
ConvergedReason::from_failure_kind(FailureReasonKind::PcSetup)
}
FailureStage::Solve => {
ConvergedReason::from_failure_kind(FailureReasonKind::PcApply)
}
};
Some(NestedPcFailure {
component: "factorization",
reason,
iterations: 0,
final_norm: None,
residual_history_summary: None,
detail: format!("stage={stage:?} detail={msg}"),
})
}
KError::ZeroPivot(row) => {
let reason = match stage {
FailureStage::Setup => {
ConvergedReason::from_failure_kind(FailureReasonKind::PcSetup)
}
FailureStage::Solve => {
ConvergedReason::from_failure_kind(FailureReasonKind::PcApply)
}
};
Some(NestedPcFailure {
component: "factorization",
reason,
iterations: 0,
final_norm: None,
residual_history_summary: None,
detail: format!("stage={stage:?} zero_pivot_row={row}"),
})
}
KError::NestedPcFailed(failure) => Some(failure.clone()),
_ => None,
}
}
}
pub fn map_kerror_to_reason(err: &KError, stage: FailureStage) -> Option<ConvergedReason> {
match stage {
FailureStage::Setup => match err {
KError::NestedPcFailed(failure) => Some(failure.reason),
KError::PcFailed(_) | KError::FactorError(_) | KError::ZeroPivot(_) => Some(
ConvergedReason::from_failure_kind(FailureReasonKind::PcSetup),
),
KError::IndefinitePreconditioner | KError::DivergedIndefinitePC => Some(
ConvergedReason::from_failure_kind(FailureReasonKind::IndefinitePc),
),
KError::BreakdownOrIndefinite => Some(ConvergedReason::from_failure_kind(
FailureReasonKind::Breakdown,
)),
KError::IndefiniteMatrix => Some(ConvergedReason::from_failure_kind(
FailureReasonKind::IndefiniteMatrix,
)),
_ => None,
},
FailureStage::Solve => match err {
KError::NestedPcFailed(failure) => Some(failure.reason),
KError::PcFailed(_) | KError::FactorError(_) | KError::ZeroPivot(_) => Some(
ConvergedReason::from_failure_kind(FailureReasonKind::PcApply),
),
KError::BreakdownOrIndefinite => Some(ConvergedReason::from_failure_kind(
FailureReasonKind::Breakdown,
)),
KError::IndefiniteMatrix => Some(ConvergedReason::from_failure_kind(
FailureReasonKind::IndefiniteMatrix,
)),
KError::IndefinitePreconditioner | KError::DivergedIndefinitePC => Some(
ConvergedReason::from_failure_kind(FailureReasonKind::IndefinitePc),
),
_ => None,
},
}
}
#[derive(Clone, Debug, Default)]
pub struct ReasonDiagnosticsCounters {
pub breakdown: usize,
pub nan: usize,
pub inf: usize,
pub pc_setup: usize,
pub pc_apply: usize,
}
impl ReasonDiagnosticsCounters {
pub fn record_reason(&mut self, reason: ConvergedReason) {
match reason.category() {
Some(ReasonCategory::Breakdown) => self.breakdown += 1,
Some(ReasonCategory::Nan) => self.nan += 1,
Some(ReasonCategory::Inf) => self.inf += 1,
Some(ReasonCategory::PcSetup) => self.pc_setup += 1,
Some(ReasonCategory::PcApply) => self.pc_apply += 1,
None => {}
}
}
}
impl std::fmt::Display for ConvergedReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.petsc_reason())
}
}
#[derive(Clone, Debug, Default)]
pub struct SolverCounters {
pub num_global_reductions: usize,
pub overlap_global_reductions: usize,
pub residual_replacements: usize,
}
#[derive(Clone, Debug, Default)]
pub struct FgmresCounters {
pub restart_count: usize,
pub inner_iterations_last_cycle: usize,
pub orthog_passes: usize,
pub happy_breakdowns: usize,
pub explicit_residual_checks: usize,
pub pipeline_fallbacks: usize,
pub modify_pc_calls: usize,
pub deferred_pipeline_waits: usize,
pub immediate_pipeline_completions: usize,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ReductionPhaseDiagnostics {
pub startup: usize,
pub iterative: usize,
pub tail: usize,
}
impl SolverCounters {
pub fn reduction_phase_diagnostics(
&self,
model: Option<&ReductionModel>,
iterations: usize,
) -> Option<ReductionPhaseDiagnostics> {
let model = model?;
let startup = model.startup;
let iterative = (model.per_iteration * iterations as f64).ceil() as usize;
let modeled_total = startup + iterative + model.tail;
let tail = if self.num_global_reductions >= startup + iterative {
self.num_global_reductions - startup - iterative
} else {
model.tail
};
let _ = modeled_total;
Some(ReductionPhaseDiagnostics {
startup,
iterative,
tail,
})
}
}
#[derive(Clone, Debug, Default)]
pub struct GcrCounters {
pub basis_updates: usize,
pub sync_count: usize,
pub restart_count: usize,
pub restarted: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NestedPcFailure {
pub component: &'static str,
pub reason: ConvergedReason,
pub iterations: usize,
pub final_norm: Option<String>,
pub residual_history_summary: Option<String>,
pub detail: String,
}
#[cfg(feature = "metrics")]
#[derive(Clone, Debug, Default)]
pub struct SolveMetrics {
pub reductions: usize,
pub reduction_wait_nanos: u64,
pub reduction_overlap_nanos: u64,
pub matvec_nanos: u64,
pub pc_apply_nanos: u64,
pub bytes_reduced: usize,
}
#[cfg(not(feature = "metrics"))]
#[derive(Clone, Debug, Default)]
pub struct SolveMetrics;
#[derive(Clone, Debug)]
pub struct ReductionModel {
pub variant: &'static str,
pub startup: usize,
pub per_iteration: f64,
pub tail: usize,
}
impl ReductionModel {
pub fn estimate_total(&self, iterations: usize) -> usize {
self.startup + (self.per_iteration * iterations as f64).ceil() as usize + self.tail
}
}
#[must_use]
#[derive(Clone, Debug)]
pub struct SolveStats<R> {
pub iterations: usize,
pub final_residual: R,
pub final_recurrence_residual: Option<R>,
pub final_true_residual: Option<R>,
pub last_preconditioned_residual: Option<R>,
pub reason: ConvergedReason,
pub counters: SolverCounters,
pub gcr_counters: Option<GcrCounters>,
pub fgmres_counters: Option<FgmresCounters>,
pub reduction_model: Option<ReductionModel>,
pub complex_drift_events: usize,
pub complex_drift_counts: [usize; 6],
pub complex_drift_max_rel: R,
pub metrics: SolveMetrics,
pub nested_pc_failure: Option<NestedPcFailure>,
pub reason_counters: ReasonDiagnosticsCounters,
pub gmres_classical_retry: bool,
pub acceptance_status: AcceptanceStatus,
pub breakdown_reason: Option<ConvergedReason>,
pub residual_override_note: Option<String>,
pub orthogonalization_passes: usize,
pub orthogonalization_rank_loss: bool,
pub max_orthogonality_loss_estimate: R,
pub effective_variant: Option<String>,
pub effective_restart: Option<usize>,
pub effective_residual_check_policy: Option<String>,
}
impl<R: Default> SolveStats<R> {
pub fn new(iterations: usize, final_residual: R, reason: ConvergedReason) -> Self {
Self {
iterations,
final_residual,
final_recurrence_residual: None,
final_true_residual: None,
last_preconditioned_residual: None,
reason,
counters: SolverCounters::default(),
gcr_counters: None,
fgmres_counters: None,
reduction_model: None,
complex_drift_events: 0,
complex_drift_counts: [0; 6],
complex_drift_max_rel: R::default(),
metrics: SolveMetrics::default(),
nested_pc_failure: None,
reason_counters: ReasonDiagnosticsCounters::default(),
gmres_classical_retry: false,
acceptance_status: if reason.is_converged() {
AcceptanceStatus::Ok
} else {
AcceptanceStatus::Failed
},
breakdown_reason: None,
residual_override_note: None,
orthogonalization_passes: 0,
orthogonalization_rank_loss: false,
max_orthogonality_loss_estimate: R::default(),
effective_variant: None,
effective_restart: None,
effective_residual_check_policy: None,
}
}
pub fn with_counters(mut self, counters: SolverCounters) -> Self {
self.counters = counters;
self
}
pub fn with_reduction_model(mut self, model: ReductionModel) -> Self {
self.reduction_model = Some(model);
self
}
pub fn with_gcr_counters(mut self, counters: GcrCounters) -> Self {
self.gcr_counters = Some(counters);
self
}
pub fn with_fgmres_counters(mut self, counters: FgmresCounters) -> Self {
self.fgmres_counters = Some(counters);
self
}
pub fn with_nested_pc_failure(mut self, failure: NestedPcFailure) -> Self {
self.nested_pc_failure = Some(failure);
self
}
pub fn with_gmres_classical_retry(mut self, used: bool) -> Self {
self.gmres_classical_retry = used;
self
}
pub fn finalize_reason_counters(mut self) -> Self {
self.reason_counters.record_reason(self.reason);
if let Some(inner) = self.nested_pc_failure.as_ref() {
self.reason_counters.record_reason(inner.reason);
}
self
}
pub fn reduction_phase_diagnostics(&self) -> Option<ReductionPhaseDiagnostics> {
self.counters
.reduction_phase_diagnostics(self.reduction_model.as_ref(), self.iterations)
}
pub fn with_orthogonalization_diagnostics(
mut self,
passes: usize,
rank_loss: bool,
max_loss_estimate: R,
) -> Self {
self.orthogonalization_passes = passes;
self.orthogonalization_rank_loss = rank_loss;
self.max_orthogonality_loss_estimate = max_loss_estimate;
self
}
pub fn with_effective_runtime_policy(
mut self,
variant: impl Into<String>,
restart: usize,
residual_check_policy: impl Into<String>,
) -> Self {
self.effective_variant = Some(variant.into());
self.effective_restart = Some(restart);
self.effective_residual_check_policy = Some(residual_check_policy.into());
self
}
}
impl Convergence {
pub fn new(rtol: R, atol: R, dtol: R, max_iters: usize) -> Self {
Self {
rtol,
atol,
dtol,
max_iters,
}
}
pub fn check(&self, rnorm: R, bnorm: R, iters: usize) -> (ConvergedReason, SolveStats<R>) {
if let Some(reason) = ConvergedReason::from_non_finite(rnorm)
.or_else(|| ConvergedReason::from_non_finite(bnorm))
{
let stats = SolveStats::new(iters, rnorm, reason);
return (reason, stats);
}
if rnorm <= self.atol {
let stats = SolveStats::new(iters, rnorm, ConvergedReason::ConvergedAtol);
return (ConvergedReason::ConvergedAtol, stats);
}
if rnorm <= self.rtol * bnorm {
let stats = SolveStats::new(iters, rnorm, ConvergedReason::ConvergedRtol);
return (ConvergedReason::ConvergedRtol, stats);
}
if rnorm >= self.dtol * bnorm {
let stats = SolveStats::new(iters, rnorm, ConvergedReason::DivergedDtol);
return (ConvergedReason::DivergedDtol, stats);
}
if iters >= self.max_iters {
let stats = SolveStats::new(iters, rnorm, ConvergedReason::DivergedMaxIts);
return (ConvergedReason::DivergedMaxIts, stats);
}
let stats = SolveStats::new(iters, rnorm, ConvergedReason::Continued);
(ConvergedReason::Continued, stats)
}
}
impl Convergence {
#[deprecated(since = "0.1.0", note = "use check() method instead")]
pub fn check_legacy(&self, res_norm: R, res0_norm: R, i: usize) -> (bool, SolveStats<R>) {
let (reason, stats) = self.check(res_norm, res0_norm, i);
let converged = matches!(
reason,
ConvergedReason::ConvergedRtol | ConvergedReason::ConvergedAtol
);
let mut legacy_stats =
SolveStats::new(stats.iterations, stats.final_residual, stats.reason);
legacy_stats.counters = stats.counters;
(
converged || reason != ConvergedReason::Continued,
legacy_stats,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_convergence_new() {
let conv = Convergence::new(1e-6, 1e-12, 1e3, 1000);
assert_eq!(conv.rtol, 1e-6);
assert_eq!(conv.atol, 1e-12);
assert_eq!(conv.dtol, 1e3);
assert_eq!(conv.max_iters, 1000);
}
#[test]
fn test_converged_absolute_tolerance() {
let conv = Convergence::new(1e-6, 1e-8, 1e3, 100);
let rnorm = 1e-9; let bnorm = 1.0;
let iters = 5;
let (reason, stats) = conv.check(rnorm, bnorm, iters);
assert_eq!(reason, ConvergedReason::ConvergedAtol);
assert_eq!(stats.reason, ConvergedReason::ConvergedAtol);
assert_eq!(stats.iterations, 5);
assert_eq!(stats.final_residual, 1e-9);
}
#[test]
fn test_converged_relative_tolerance() {
let conv = Convergence::new(1e-6, 1e-12, 1e3, 100);
let rnorm = 1e-7; let bnorm = 1.0;
let iters = 10;
let (reason, stats) = conv.check(rnorm, bnorm, iters);
assert_eq!(reason, ConvergedReason::ConvergedRtol);
assert_eq!(stats.reason, ConvergedReason::ConvergedRtol);
assert_eq!(stats.iterations, 10);
assert_eq!(stats.final_residual, 1e-7);
}
#[test]
fn test_diverged_tolerance() {
let conv = Convergence::new(1e-6, 1e-12, 2.0, 100);
let rnorm = 3.0; let bnorm = 1.0;
let iters = 5;
let (reason, stats) = conv.check(rnorm, bnorm, iters);
assert_eq!(reason, ConvergedReason::DivergedDtol);
assert_eq!(stats.reason, ConvergedReason::DivergedDtol);
assert_eq!(stats.iterations, 5);
assert_eq!(stats.final_residual, 3.0);
}
#[test]
fn test_diverged_max_iterations() {
let conv = Convergence::new(1e-6, 1e-12, 1e3, 10);
let rnorm = 1e-3; let bnorm = 1.0;
let iters = 10;
let (reason, stats) = conv.check(rnorm, bnorm, iters);
assert_eq!(reason, ConvergedReason::DivergedMaxIts);
assert_eq!(stats.reason, ConvergedReason::DivergedMaxIts);
assert_eq!(stats.iterations, 10);
assert_eq!(stats.final_residual, 1e-3);
}
#[test]
fn test_continued() {
let conv = Convergence::new(1e-6, 1e-12, 1e3, 100);
let rnorm = 1e-3; let bnorm = 1.0;
let iters = 5;
let (reason, stats) = conv.check(rnorm, bnorm, iters);
assert_eq!(reason, ConvergedReason::Continued);
assert_eq!(stats.reason, ConvergedReason::Continued);
assert_eq!(stats.iterations, 5);
assert_eq!(stats.final_residual, 1e-3);
}
#[test]
fn test_convergence_precedence() {
let conv = Convergence::new(1e-6, 1e-8, 1e3, 100);
let rnorm = 1e-9; let bnorm = 1.0;
let iters = 5;
let (reason, _) = conv.check(rnorm, bnorm, iters);
assert_eq!(reason, ConvergedReason::ConvergedAtol);
}
#[test]
fn test_converged_reason_equality() {
assert_eq!(
ConvergedReason::ConvergedRtol,
ConvergedReason::ConvergedRtol
);
assert_eq!(
ConvergedReason::ConvergedAtol,
ConvergedReason::ConvergedAtol
);
assert_eq!(ConvergedReason::DivergedDtol, ConvergedReason::DivergedDtol);
assert_eq!(
ConvergedReason::DivergedMaxIts,
ConvergedReason::DivergedMaxIts
);
assert_eq!(ConvergedReason::Continued, ConvergedReason::Continued);
assert_ne!(
ConvergedReason::ConvergedRtol,
ConvergedReason::ConvergedAtol
);
assert_ne!(
ConvergedReason::DivergedDtol,
ConvergedReason::DivergedMaxIts
);
}
#[test]
fn test_converged_reason_debug() {
let reason = ConvergedReason::ConvergedRtol;
let debug_str = format!("{:?}", reason);
assert!(debug_str.contains("ConvergedRtol"));
}
#[test]
fn test_convergence_nan_or_inf() {
let conv = Convergence::new(1e-6, 1e-12, 1e3, 10);
let (reason_nan, _) = conv.check(f64::NAN, 1.0, 1);
assert_eq!(reason_nan, ConvergedReason::DivergedNan);
let (reason_inf, _) = conv.check(f64::INFINITY, 1.0, 2);
assert_eq!(reason_inf, ConvergedReason::DivergedInf);
let (reason_bnorm_inf, _) = conv.check(1.0, f64::INFINITY, 3);
assert_eq!(reason_bnorm_inf, ConvergedReason::DivergedInf);
}
#[test]
fn test_solve_stats_clone() {
let stats = SolveStats::new(42, 1e-8, ConvergedReason::ConvergedRtol);
let cloned = stats.clone();
assert_eq!(cloned.iterations, 42);
assert_eq!(cloned.final_residual, 1e-8);
assert_eq!(cloned.final_recurrence_residual, None);
assert_eq!(cloned.final_true_residual, None);
assert_eq!(cloned.last_preconditioned_residual, None);
assert_eq!(cloned.reason, ConvergedReason::ConvergedRtol);
}
#[test]
fn test_solve_stats_debug() {
let stats = SolveStats::new(10, 1e-6, ConvergedReason::ConvergedAtol);
let debug_str = format!("{:?}", stats);
assert!(debug_str.contains("10"));
assert!(debug_str.contains("ConvergedAtol"));
}
#[test]
#[allow(deprecated)]
fn test_legacy_check_convergence() {
let conv = Convergence::new(1e-6, 1e-12, 1e3, 100);
let res_norm = 1e-8;
let res0_norm = 1.0;
let iters = 5;
let (should_stop, stats) = conv.check_legacy(res_norm, res0_norm, iters);
assert!(should_stop);
assert_eq!(stats.iterations, 5);
assert_eq!(stats.final_residual, 1e-8);
}
#[test]
#[allow(deprecated)]
fn test_legacy_check_continue() {
let conv = Convergence::new(1e-6, 1e-12, 1e3, 100);
let res_norm = 1e-3;
let res0_norm = 1.0;
let iters = 5;
let (should_stop, stats) = conv.check_legacy(res_norm, res0_norm, iters);
assert!(!should_stop);
assert_eq!(stats.iterations, 5);
assert_eq!(stats.final_residual, 1e-3);
assert_eq!(stats.reason, ConvergedReason::Continued);
}
#[test]
fn nested_pc_failure_mapping_preserves_structured_inner_reason() {
let err = KError::NestedPcFailed(NestedPcFailure {
component: "pc_ksp",
reason: ConvergedReason::DivergedBreakdown,
iterations: 4,
final_norm: Some("true_residual_l2=1.0e+00".into()),
residual_history_summary: Some("history_len=4".into()),
detail: "inner failure".into(),
});
let reason = map_kerror_to_reason(&err, FailureStage::Solve).expect("reason");
assert_eq!(reason, ConvergedReason::DivergedBreakdown);
let nested =
ReasonEmitter::nested_pc_failure(&err, FailureStage::Solve).expect("nested metadata");
assert_eq!(nested.component, "pc_ksp");
assert_eq!(nested.reason, ConvergedReason::DivergedBreakdown);
assert_eq!(nested.iterations, 4);
}
#[test]
fn test_different_numeric_types() {
let conv_f64 = Convergence::new(1e-6f64, 1e-12f64, 1e3f64, 100);
let (reason, _) = conv_f64.check(1e-8f64, 1.0f64, 5);
assert_eq!(reason, ConvergedReason::ConvergedRtol);
let conv2 = Convergence::new(1e-8, 1e-16, 1e6, 50);
let (reason2, _) = conv2.check(1e-10, 1.0, 10);
assert_eq!(reason2, ConvergedReason::ConvergedRtol);
}
}