Skip to main content

hybit_core/
lib.rs

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    /// Structural matrix analysis cost charged to this solve.
149    pub analysis_seconds: f64,
150    /// Reusable baseline preparation cost (Jacobi, ABTM topology and Krylov workspace).
151    pub prepare_seconds: f64,
152    /// Initial Jacobi-PCG probe time.
153    pub probe_seconds: f64,
154    /// Residual/risk analysis and ABTM topology-region construction time.
155    pub diagnostics_seconds: f64,
156    /// Dense local Cholesky construction time.
157    pub local_factor_seconds: f64,
158    /// Restarted PCG time after escalation.
159    pub restart_seconds: f64,
160    pub escalations: usize,
161    pub probe_iterations: usize,
162    pub probe_final_residual: f64,
163    /// Core DOFs identified as numerically difficult before halo expansion.
164    pub hard_dofs: usize,
165    /// Number of local direct subdomains.
166    pub local_direct_regions: usize,
167    /// Largest local factor order after overlap expansion.
168    pub largest_local_region: usize,
169    /// Sum of local factor orders. Overlapped DOFs are counted once per factor.
170    pub local_factor_dofs: usize,
171    /// Unique global DOFs covered by at least one local factor.
172    pub unique_local_factor_dofs: usize,
173    /// Bytes owned by local Cholesky factors, indices and symmetric weights.
174    pub local_factor_bytes: usize,
175    /// Number of topology halo layers requested for each hard region.
176    pub overlap_layers: usize,
177    /// True when an already-built local direct preconditioner was reused.
178    pub preconditioner_reused: bool,
179    /// 1-based solve number within a prepared context.
180    pub solve_sequence: usize,
181    /// Bytes reserved for reusable PCG work vectors.
182    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}