sparse_ir_core/error.rs
1//! Error type of the public API
2//!
3//! The public functions of this crate that return a `Result` use [`Error`]
4//! as the error type. Each variant carries the values that caused it, so
5//! that its message locates the problem. [`Error::kind`] sorts the variants
6//! into the categories that the C API reports as status codes.
7
8use crate::gemm::GemmError;
9use crate::traits::Statistics;
10
11/// Which array of an operation has the wrong shape, in
12/// [`Error::ShapeMismatch`]
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum ArrayRole {
15 /// An array that the operation reads, e.g. the coefficients of an
16 /// evaluation or a given sampling matrix
17 Input,
18 /// The array that the operation writes to, e.g. `out` of `evaluate_nd_to`
19 Output,
20}
21
22impl std::fmt::Display for ArrayRole {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 f.write_str(match self {
25 ArrayRole::Input => "input",
26 ArrayRole::Output => "output",
27 })
28 }
29}
30
31/// Error returned by the fallible public functions of this crate
32///
33/// More variants may be added without a major version bump. To handle a category of
34/// errors rather than one variant, match on [`Error::kind`].
35///
36/// Errors compare equal when all their fields do. An error that holds a NaN
37/// value is therefore not equal to itself; match on the variant instead.
38#[derive(Debug, Clone, PartialEq, thiserror::Error)]
39#[non_exhaustive]
40pub enum Error {
41 /// A parameter is outside its valid range.
42 #[error("invalid {name} = {value}: {reason}")]
43 InvalidParameter {
44 /// Name of the parameter, as in the documentation of the function
45 name: &'static str,
46 /// The rejected value, formatted for the message
47 value: String,
48 /// The condition that the value violates, e.g. "must be in (0, 1)"
49 reason: String,
50 },
51 /// A point lies outside the domain of a function, e.g. τ outside [-β, β]
52 /// or x outside the knots of a polynomial. NaN lies outside every domain.
53 #[error("{name} = {value:?} is outside the domain [{:?}, {:?}]", .domain.0, .domain.1)]
54 OutOfDomain {
55 /// Name of the argument, as in the documentation of the function
56 name: &'static str,
57 /// The rejected point
58 value: f64,
59 /// The closed interval `(lower, upper)` of valid points
60 domain: (f64, f64),
61 },
62 /// A Matsubara frequency index is not allowed: `n` must be odd for
63 /// fermionic and even for bosonic statistics, and non-negative for a
64 /// positive-only sampling. The message says which: a wrong parity, or a
65 /// negative `n` of the right parity.
66 #[error("{}", invalid_matsubara_index_message(.n, .statistics))]
67 InvalidMatsubaraIndex {
68 /// The rejected index
69 n: i64,
70 /// The statistics that `n` was checked against
71 statistics: Statistics,
72 },
73 /// An input that must not be empty is empty.
74 #[error("{name} must not be empty")]
75 EmptyInput {
76 /// Name of the input, as in the documentation of the function
77 name: &'static str,
78 },
79 /// An input contains NaN or an infinity.
80 #[error("{name} has the non-finite entry {value} at index {index:?}")]
81 NonFiniteInput {
82 /// Name of the input, as in the documentation of the function
83 name: &'static str,
84 /// Index of the first non-finite entry (`[row, column]` for a matrix)
85 index: Vec<usize>,
86 /// That entry, converted to `f64`
87 value: f64,
88 },
89 /// The kernel does not support the requested statistics, e.g.
90 /// `RegularizedBoseKernel` with fermionic statistics.
91 #[error(
92 "kernel does not support the requested statistics: kernels with ypower = 1 \
93 (e.g. RegularizedBoseKernel) require bosonic statistics"
94 )]
95 KernelStatisticsMismatch,
96 /// The basis has fewer default poles than functions, so its default poles
97 /// cannot define a DLR. This can happen with certain kernels (e.g.
98 /// `RegularizedBoseKernel`) because of the limited precision of the root
99 /// finding.
100 #[error("number of default poles ({n_poles}) is less than the basis size ({basis_size})")]
101 InsufficientDefaultPoles {
102 /// Basis size
103 basis_size: usize,
104 /// Number of default poles found
105 n_poles: usize,
106 },
107 /// An axis argument is not an axis of the array, e.g. `dim` of
108 /// `evaluate_nd` for an array of rank `dim` or less.
109 #[error("axis {axis} is out of range for an array of rank {rank}")]
110 AxisOutOfRange {
111 /// The rejected axis
112 axis: usize,
113 /// The rank of the array
114 rank: usize,
115 },
116 /// An array has the wrong shape. For a slice, the shapes have one entry,
117 /// its length.
118 #[error("{which} has the shape {actual:?}, expected {expected:?}")]
119 ShapeMismatch {
120 /// Which array: an input, or the output
121 which: ArrayRole,
122 /// The shape the operation needs
123 expected: Vec<usize>,
124 /// The shape of the array
125 actual: Vec<usize>,
126 },
127 /// The operation is not defined for this input, e.g. default Matsubara
128 /// sampling points for basis functions without a definite parity (#183).
129 #[error("not supported: {what}")]
130 NotSupported {
131 /// What is not supported, and why
132 what: String,
133 },
134 /// A matrix decomposition failed, e.g. the SVD iteration did not converge.
135 #[error("decomposition failed: {reason}")]
136 DecompositionFailed {
137 /// What failed, with the values involved
138 reason: String,
139 },
140 /// The GEMM backend rejected a call, e.g. a dimension exceeds the integer
141 /// range of an injected BLAS.
142 #[error(transparent)]
143 Gemm(#[from] GemmError),
144 /// A tenferro tensor operation failed.
145 #[error("tensor operation failed: {reason}")]
146 Tensor {
147 /// The message of the tenferro error
148 reason: String,
149 },
150}
151
152impl From<tenferro_tensor::Error> for Error {
153 fn from(e: tenferro_tensor::Error) -> Self {
154 Error::Tensor {
155 reason: e.to_string(),
156 }
157 }
158}
159
160/// Category of an [`Error`]
161///
162/// Each category corresponds to one failure status code of the C API, shown
163/// in parentheses.
164///
165/// Unlike [`Error`], this enum is exhaustive: a new category would need a new
166/// C status code, so adding one is a breaking change.
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
168pub enum ErrorKind {
169 /// An argument has an invalid value (`SPIR_INVALID_ARGUMENT`).
170 InvalidArgument,
171 /// An axis or a rank is invalid (`SPIR_INVALID_DIMENSION`).
172 InvalidDimension,
173 /// An input array has the wrong shape (`SPIR_INPUT_DIMENSION_MISMATCH`).
174 InputDimensionMismatch,
175 /// An output array has the wrong shape (`SPIR_OUTPUT_DIMENSION_MISMATCH`).
176 OutputDimensionMismatch,
177 /// The operation is not supported for these arguments
178 /// (`SPIR_NOT_SUPPORTED`).
179 NotSupported,
180 /// A failure that invalid input does not explain
181 /// (`SPIR_INTERNAL_ERROR`).
182 Internal,
183}
184
185impl Error {
186 /// Category of the error
187 pub fn kind(&self) -> ErrorKind {
188 match self {
189 Error::InvalidParameter { .. }
190 | Error::OutOfDomain { .. }
191 | Error::InvalidMatsubaraIndex { .. }
192 | Error::EmptyInput { .. }
193 | Error::NonFiniteInput { .. }
194 | Error::InsufficientDefaultPoles { .. } => ErrorKind::InvalidArgument,
195 Error::KernelStatisticsMismatch | Error::NotSupported { .. } => ErrorKind::NotSupported,
196 Error::AxisOutOfRange { .. } => ErrorKind::InvalidDimension,
197 Error::ShapeMismatch {
198 which: ArrayRole::Input,
199 ..
200 } => ErrorKind::InputDimensionMismatch,
201 Error::ShapeMismatch {
202 which: ArrayRole::Output,
203 ..
204 } => ErrorKind::OutputDimensionMismatch,
205 Error::Gemm(GemmError::DimensionOverflow { .. }) => ErrorKind::InvalidArgument,
206 Error::DecompositionFailed { .. }
207 | Error::Gemm(GemmError::InvalidArgument(_))
208 | Error::Tensor { .. } => ErrorKind::Internal,
209 }
210 }
211}
212
213/// Message of [`Error::InvalidMatsubaraIndex`]: an `n` of the parity of
214/// `statistics` can only have been rejected for being negative (in a
215/// positive-only sampling)
216fn invalid_matsubara_index_message(n: &i64, statistics: &Statistics) -> String {
217 let parity_ok = match statistics {
218 Statistics::Fermionic => n.rem_euclid(2) == 1,
219 Statistics::Bosonic => n.rem_euclid(2) == 0,
220 };
221 if parity_ok && *n < 0 {
222 format!(
223 "Matsubara frequency n = {n} is negative, but only non-negative frequencies are \
224 allowed here (positive-only sampling)"
225 )
226 } else {
227 format!(
228 "Matsubara frequency n = {n} is not allowed for {} statistics",
229 statistics.as_str()
230 )
231 }
232}
233
234/// `Ok` if `value` is positive and finite
235#[doc(hidden)]
236pub fn require_positive_finite(name: &'static str, value: f64) -> Result<(), Error> {
237 if value > 0.0 && value.is_finite() {
238 Ok(())
239 } else {
240 Err(Error::InvalidParameter {
241 name,
242 value: format!("{value:?}"),
243 reason: "must be positive and finite".to_string(),
244 })
245 }
246}
247
248/// `Ok` if `value` is finite
249#[doc(hidden)]
250pub fn require_finite(name: &'static str, value: f64) -> Result<(), Error> {
251 if value.is_finite() {
252 Ok(())
253 } else {
254 Err(Error::InvalidParameter {
255 name,
256 value: format!("{value:?}"),
257 reason: "must be finite".to_string(),
258 })
259 }
260}
261
262/// `Ok` for an accuracy that selects the SVE: `None` (automatic) or a value
263/// in (0, 1)
264#[doc(hidden)]
265pub fn require_accuracy(name: &'static str, epsilon: Option<f64>) -> Result<(), Error> {
266 match epsilon {
267 Some(eps) if !(eps > 0.0 && eps < 1.0) => Err(Error::InvalidParameter {
268 name,
269 value: format!("{eps:?}"),
270 reason: "must be in (0, 1)".to_string(),
271 }),
272 _ => Ok(()),
273 }
274}
275
276/// `Ok` for a relative truncation threshold: `None` or a value in [0, 1),
277/// where 0 keeps every singular value
278#[doc(hidden)]
279pub fn require_threshold(name: &'static str, epsilon: Option<f64>) -> Result<(), Error> {
280 match epsilon {
281 Some(eps) if !(eps >= 0.0 && eps < 1.0) => Err(Error::InvalidParameter {
282 name,
283 value: format!("{eps:?}"),
284 reason: "must be in [0, 1)".to_string(),
285 }),
286 _ => Ok(()),
287 }
288}
289
290/// `Ok` unless `size` is `Some(0)`
291#[doc(hidden)]
292pub fn require_nonzero_size(name: &'static str, size: Option<usize>) -> Result<(), Error> {
293 match size {
294 Some(0) => Err(Error::InvalidParameter {
295 name,
296 value: "0".to_string(),
297 reason: "must be positive".to_string(),
298 }),
299 _ => Ok(()),
300 }
301}
302
303/// Result type of the fallible public functions of this crate
304pub type Result<T, E = Error> = std::result::Result<T, E>;
305
306#[cfg(test)]
307#[path = "error_tests.rs"]
308mod error_tests;