1use std::error::Error;
2use std::fmt::{Display, Formatter};
3
4#[derive(Clone, Debug, PartialEq)]
5pub enum HybitError {
6 InvalidMatrix(&'static str),
7 InvalidArgument(&'static str),
8 DimensionMismatch { expected: usize, actual: usize },
9 MissingDiagonal { row: usize },
10 ZeroDiagonal { row: usize },
11 SizeOverflow,
12 NumericalBreakdown(&'static str),
13 NotConverged { iterations: usize, residual: f64 },
14}
15
16impl Display for HybitError {
17 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
18 match self {
19 Self::InvalidMatrix(msg) => write!(f, "invalid matrix: {msg}"),
20 Self::InvalidArgument(msg) => write!(f, "invalid argument: {msg}"),
21 Self::DimensionMismatch { expected, actual } => {
22 write!(f, "dimension mismatch: expected {expected}, got {actual}")
23 }
24 Self::MissingDiagonal { row } => write!(f, "missing diagonal entry at row {row}"),
25 Self::ZeroDiagonal { row } => write!(f, "zero diagonal entry at row {row}"),
26 Self::SizeOverflow => write!(
27 f,
28 "matrix or index size exceeds the selected representation"
29 ),
30 Self::NumericalBreakdown(msg) => write!(f, "numerical breakdown: {msg}"),
31 Self::NotConverged {
32 iterations,
33 residual,
34 } => {
35 write!(
36 f,
37 "solver did not converge after {iterations} iterations; residual={residual:e}"
38 )
39 }
40 }
41 }
42}
43
44impl Error for HybitError {}
45
46pub trait LinearOperator {
47 fn rows(&self) -> usize;
48 fn cols(&self) -> usize;
49 fn apply(&self, x: &[f64], y: &mut [f64]) -> Result<(), HybitError>;
50}
51
52pub trait Preconditioner {
53 fn len(&self) -> usize;
54
55 fn is_empty(&self) -> bool {
56 self.len() == 0
57 }
58
59 fn apply(&self, r: &[f64], z: &mut [f64]) -> Result<(), HybitError>;
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
63pub enum MatrixBackend {
64 Csr32,
65 Abtm,
66 MatrixFree,
67}
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub enum SolverKind {
71 Pcg,
72 Minres,
73 Gmres,
74 Bicgstab,
75 Hybrid,
76}
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub enum PreconditionerKind {
80 None,
81 Jacobi,
82 BlockJacobi,
83 LocalDirect,
84 Hybrid,
85 RigidBodyTwoLevel,
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum SolveStatus {
90 Converged,
91 MaxIterations,
92 Breakdown,
93}
94
95#[derive(Clone, Copy, Debug)]
96pub struct SolverOptions {
97 pub relative_tolerance: f64,
98 pub absolute_tolerance: f64,
99 pub max_iterations: usize,
100}
101
102impl Default for SolverOptions {
103 fn default() -> Self {
104 Self {
105 relative_tolerance: 1.0e-8,
106 absolute_tolerance: 0.0,
107 max_iterations: 1000,
108 }
109 }
110}
111
112impl SolverOptions {
113 pub fn validate(&self) -> Result<(), HybitError> {
114 if !self.relative_tolerance.is_finite() || self.relative_tolerance < 0.0 {
115 return Err(HybitError::InvalidArgument(
116 "relative_tolerance must be finite and >= 0",
117 ));
118 }
119 if !self.absolute_tolerance.is_finite() || self.absolute_tolerance < 0.0 {
120 return Err(HybitError::InvalidArgument(
121 "absolute_tolerance must be finite and >= 0",
122 ));
123 }
124 if self.relative_tolerance == 0.0 && self.absolute_tolerance == 0.0 {
125 return Err(HybitError::InvalidArgument(
126 "at least one tolerance must be > 0",
127 ));
128 }
129 if self.max_iterations == 0 {
130 return Err(HybitError::InvalidArgument("max_iterations must be > 0"));
131 }
132 Ok(())
133 }
134}
135
136#[derive(Clone, Debug)]
137pub struct SolveReport {
138 pub status: SolveStatus,
139 pub solver: SolverKind,
140 pub preconditioner: PreconditionerKind,
141 pub backend: MatrixBackend,
142 pub iterations: usize,
143 pub initial_residual: f64,
144 pub final_residual: f64,
145 pub relative_residual: f64,
146 pub setup_seconds: f64,
147 pub solve_seconds: f64,
148 pub analysis_seconds: f64,
150 pub prepare_seconds: f64,
152 pub probe_seconds: f64,
154 pub diagnostics_seconds: f64,
156 pub local_factor_seconds: f64,
158 pub restart_seconds: f64,
160 pub escalations: usize,
161 pub probe_iterations: usize,
162 pub probe_final_residual: f64,
163 pub hard_dofs: usize,
165 pub local_direct_regions: usize,
167 pub largest_local_region: usize,
169 pub local_factor_dofs: usize,
171 pub unique_local_factor_dofs: usize,
173 pub local_factor_bytes: usize,
175 pub overlap_layers: usize,
177 pub preconditioner_reused: bool,
179 pub solve_sequence: usize,
181 pub krylov_workspace_bytes: usize,
183}
184
185impl SolveReport {
186 pub fn converged(&self) -> bool {
187 self.status == SolveStatus::Converged
188 }
189}
190
191#[inline]
192pub fn dot(a: &[f64], b: &[f64]) -> f64 {
193 debug_assert_eq!(a.len(), b.len());
194 a.iter().zip(b).map(|(x, y)| x * y).sum()
195}
196
197#[inline]
198pub fn l2_norm(x: &[f64]) -> f64 {
199 dot(x, x).sqrt()
200}