Skip to main content

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;