Skip to main content

copula_core/
error.rs

1//! Error types and handling for `copula-core`.
2//!
3//! This module defines the main error type [`CopulaError`] and result type [`Result`]
4//! used throughout the library. All copula operations that can fail return a
5//! [`Result<T>`] where the error type is [`CopulaError`].
6
7/// Result type used throughout the crate.
8pub type Result<T> = std::result::Result<T, CopulaError>;
9
10/// Helper function to format invalid values for error messages.
11fn format_invalid_values(values: &[f64]) -> String {
12    if values.len() <= 3 {
13        format!("{:?}", values)
14    } else {
15        format!(
16            "[{}, {}, {} ... and {} more]",
17            values[0],
18            values[1],
19            values[2],
20            values.len() - 3
21        )
22    }
23}
24
25/// Errors that can occur in copula operations.
26///
27/// This enum covers all possible error conditions that can arise when working
28/// with copulas, from parameter validation to numerical computation issues.
29///
30/// The enum is `#[non_exhaustive]`: some variants only exist when the
31/// corresponding Cargo feature is enabled, and new variants may be added in
32/// minor releases. Match with a wildcard arm.
33#[derive(Debug, thiserror::Error)]
34#[non_exhaustive]
35pub enum CopulaError {
36    /// Invalid parameter value provided to a copula constructor or method.
37    ///
38    /// This occurs when parameters are outside their valid domain, such as
39    /// negative values for parameters that must be positive, or correlation
40    /// matrices that are not positive definite.
41    #[error("Invalid parameter: {message}{}", .suggestion.as_ref().map(|s| format!("\nSuggestion: {}", s)).unwrap_or_default())]
42    InvalidParameter {
43        /// Description of what makes the parameter invalid
44        message: String,
45        /// Optional suggestion for fixing the issue
46        suggestion: Option<String>,
47    },
48
49    /// Dimension mismatch between expected and actual dimensions.
50    ///
51    /// This occurs when the number of variables doesn't match the copula's
52    /// expected dimension, or when matrix dimensions are incompatible.
53    #[error("Dimension mismatch{}: expected {expected}, got {actual}", .context.as_ref().map(|c| format!(" in {}", c)).unwrap_or_default())]
54    DimensionMismatch {
55        /// Expected dimension
56        expected: usize,
57        /// Actual dimension provided
58        actual: usize,
59        /// Optional context about where the mismatch occurred
60        context: Option<String>,
61    },
62
63    /// Input values are outside the valid range [0, 1] for copula evaluation.
64    ///
65    /// Copula functions are defined on the unit hypercube [0, 1]ⁿ, so all
66    /// input values must be in this range.
67    #[error("Input values must be in [0,1]: found {} invalid value(s) - {}", .values.len(), format_invalid_values(.values))]
68    InvalidRange {
69        /// The problematic values
70        values: Vec<f64>,
71    },
72
73    /// Numerical computation error.
74    ///
75    /// This occurs when numerical methods fail due to numerical instability,
76    /// convergence issues, or other computational problems.
77    #[error("Numerical error: {message}")]
78    NumericalError {
79        /// Description of the numerical issue
80        message: String,
81    },
82
83    /// Matrix operation failed.
84    ///
85    /// This occurs when matrix operations like inversion or decomposition fail,
86    /// typically due to singular or ill-conditioned matrices.
87    #[error("Matrix operation failed: {operation} - {reason}")]
88    MatrixError {
89        /// The operation that failed
90        operation: String,
91        /// Reason for failure
92        reason: String,
93    },
94
95    /// Optimization algorithm failed to converge.
96    ///
97    /// This occurs during parameter estimation when the optimization algorithm
98    /// cannot find a solution within the specified tolerances or iterations.
99    #[cfg(feature = "estimation")]
100    #[cfg_attr(docsrs, doc(cfg(feature = "estimation")))]
101    #[error("Optimization failed: {reason}")]
102    OptimizationError {
103        /// Reason for optimization failure
104        reason: String,
105    },
106
107    /// Statistical test or computation error.
108    ///
109    /// This occurs when statistical procedures fail, such as when there's
110    /// insufficient data or when test assumptions are violated.
111    #[error("Statistical error: {message}")]
112    StatisticalError {
113        /// Description of the statistical issue
114        message: String,
115    },
116
117    /// Data validation error.
118    ///
119    /// This occurs when input data doesn't meet requirements, such as
120    /// containing NaN values, having insufficient sample size, or
121    /// violating distributional assumptions.
122    #[error("Data validation error: {message}")]
123    DataError {
124        /// Description of the data issue
125        message: String,
126    },
127
128    /// Feature not yet implemented.
129    ///
130    /// This is used for methods that are planned but not yet implemented.
131    /// Users should check the library roadmap for implementation timeline.
132    #[error("Not implemented: {feature}")]
133    NotImplemented {
134        /// The feature that is not implemented
135        feature: String,
136    },
137
138    /// Generic computation error with custom message.
139    ///
140    /// This is a catch-all for other computational errors that don't fit
141    /// into the more specific categories above.
142    #[error("Computation error: {message}")]
143    ComputationError {
144        /// Description of the computation error
145        message: String,
146    },
147
148    /// Serialization or deserialization error.
149    ///
150    /// This occurs when converting copulas to/from serialized formats.
151    #[cfg(feature = "serde")]
152    #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
153    #[error("Serialization error: {message}")]
154    SerializationError {
155        /// Description of the serialization issue
156        message: String,
157    },
158}
159
160impl CopulaError {
161    /// Create an invalid parameter error with a custom message.
162    pub fn invalid_parameter<S: Into<String>>(message: S) -> Self {
163        Self::InvalidParameter {
164            message: message.into(),
165            suggestion: None,
166        }
167    }
168
169    /// Create an invalid parameter error with a custom message and suggestion.
170    pub fn invalid_parameter_with_suggestion<S: Into<String>>(message: S, suggestion: S) -> Self {
171        Self::InvalidParameter {
172            message: message.into(),
173            suggestion: Some(suggestion.into()),
174        }
175    }
176
177    /// Create a dimension mismatch error.
178    pub fn dimension_mismatch(expected: usize, actual: usize) -> Self {
179        Self::DimensionMismatch {
180            expected,
181            actual,
182            context: None,
183        }
184    }
185
186    /// Create a dimension mismatch error with context.
187    pub fn dimension_mismatch_with_context<S: Into<String>>(
188        expected: usize,
189        actual: usize,
190        context: S,
191    ) -> Self {
192        Self::DimensionMismatch {
193            expected,
194            actual,
195            context: Some(context.into()),
196        }
197    }
198
199    /// Create an invalid range error for values outside [0, 1].
200    pub fn invalid_range(values: Vec<f64>) -> Self {
201        Self::InvalidRange { values }
202    }
203
204    /// Create a numerical error with a custom message.
205    pub fn numerical<S: Into<String>>(message: S) -> Self {
206        Self::NumericalError {
207            message: message.into(),
208        }
209    }
210
211    /// Create a matrix operation error.
212    pub fn matrix_error<S: Into<String>>(operation: S, reason: S) -> Self {
213        Self::MatrixError {
214            operation: operation.into(),
215            reason: reason.into(),
216        }
217    }
218
219    /// Create an optimization error.
220    #[cfg(feature = "estimation")]
221    pub fn optimization<S: Into<String>>(reason: S) -> Self {
222        Self::OptimizationError {
223            reason: reason.into(),
224        }
225    }
226
227    /// Create a statistical error with a custom message.
228    pub fn statistical<S: Into<String>>(message: S) -> Self {
229        Self::StatisticalError {
230            message: message.into(),
231        }
232    }
233
234    /// Create a data validation error.
235    pub fn data_error<S: Into<String>>(message: S) -> Self {
236        Self::DataError {
237            message: message.into(),
238        }
239    }
240
241    /// Create a not implemented error.
242    pub fn not_implemented<S: Into<String>>(feature: S) -> Self {
243        Self::NotImplemented {
244            feature: feature.into(),
245        }
246    }
247
248    /// Create a generic computation error.
249    pub fn computation<S: Into<String>>(message: S) -> Self {
250        Self::ComputationError {
251            message: message.into(),
252        }
253    }
254
255    /// Check if the error is recoverable.
256    ///
257    /// Some errors (like numerical instability) might be recoverable by
258    /// adjusting parameters or using different algorithms, while others
259    /// (like invalid parameters) are not.
260    pub fn is_recoverable(&self) -> bool {
261        match self {
262            CopulaError::NumericalError { .. } => true,
263            #[cfg(feature = "estimation")]
264            CopulaError::OptimizationError { .. } => true,
265            CopulaError::StatisticalError { .. } => true,
266            CopulaError::ComputationError { .. } => true,
267            _ => false,
268        }
269    }
270
271    /// Get the error category as a string.
272    pub fn category(&self) -> &'static str {
273        match self {
274            CopulaError::InvalidParameter { .. } => "parameter",
275            CopulaError::DimensionMismatch { .. } => "dimension",
276            CopulaError::InvalidRange { .. } => "range",
277            CopulaError::NumericalError { .. } => "numerical",
278            CopulaError::MatrixError { .. } => "matrix",
279            #[cfg(feature = "estimation")]
280            CopulaError::OptimizationError { .. } => "optimization",
281            CopulaError::StatisticalError { .. } => "statistical",
282            CopulaError::DataError { .. } => "data",
283            CopulaError::NotImplemented { .. } => "implementation",
284            CopulaError::ComputationError { .. } => "computation",
285            #[cfg(feature = "serde")]
286            CopulaError::SerializationError { .. } => "serialization",
287        }
288    }
289}
290
291/// Validate that all values are in the range [0, 1].
292///
293/// This is a common validation step for copula inputs.
294pub fn validate_unit_range(values: &[f64]) -> Result<()> {
295    let invalid_values: Vec<f64> = values
296        .iter()
297        .copied()
298        .filter(|&x| !(0.0..=1.0).contains(&x))
299        .collect();
300
301    if invalid_values.is_empty() {
302        Ok(())
303    } else {
304        Err(CopulaError::invalid_range(invalid_values))
305    }
306}
307
308/// Validate that a parameter is positive.
309pub fn validate_positive(value: f64, name: &str) -> Result<()> {
310    if value > 0.0 && value.is_finite() {
311        Ok(())
312    } else {
313        Err(CopulaError::invalid_parameter(format!(
314            "{} must be positive and finite, got {}",
315            name, value
316        )))
317    }
318}
319
320/// Validate that a parameter is non-negative.
321pub fn validate_non_negative(value: f64, name: &str) -> Result<()> {
322    if value >= 0.0 && value.is_finite() {
323        Ok(())
324    } else {
325        Err(CopulaError::invalid_parameter(format!(
326            "{} must be non-negative and finite, got {}",
327            name, value
328        )))
329    }
330}
331
332/// Validate that a parameter is in a specified range.
333pub fn validate_range(value: f64, min: f64, max: f64, name: &str) -> Result<()> {
334    if value >= min && value <= max && value.is_finite() {
335        Ok(())
336    } else {
337        Err(CopulaError::invalid_parameter(format!(
338            "{} must be in [{}, {}], got {}",
339            name, min, max, value
340        )))
341    }
342}
343
344/// Validate matrix dimensions.
345pub fn validate_dimensions(expected: usize, actual: usize, _context: &str) -> Result<()> {
346    if expected == actual {
347        Ok(())
348    } else {
349        Err(CopulaError::dimension_mismatch(expected, actual))
350    }
351}
352
353/// Validate that data contains no NaN or infinite values.
354pub fn validate_finite_data(data: &[f64], name: &str) -> Result<()> {
355    if data.iter().all(|x| x.is_finite()) {
356        Ok(())
357    } else {
358        Err(CopulaError::data_error(format!(
359            "{} contains non-finite values (NaN or infinite)",
360            name
361        )))
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    #[test]
370    fn test_validate_unit_range() {
371        // Valid range
372        assert!(validate_unit_range(&[0.0, 0.5, 1.0]).is_ok());
373
374        // Invalid range
375        assert!(validate_unit_range(&[-0.1, 0.5]).is_err());
376        assert!(validate_unit_range(&[0.5, 1.1]).is_err());
377        assert!(validate_unit_range(&[f64::NAN]).is_err());
378    }
379
380    #[test]
381    fn test_validate_positive() {
382        assert!(validate_positive(1.0, "theta").is_ok());
383        assert!(validate_positive(0.0, "theta").is_err());
384        assert!(validate_positive(-1.0, "theta").is_err());
385        assert!(validate_positive(f64::NAN, "theta").is_err());
386        assert!(validate_positive(f64::INFINITY, "theta").is_err());
387    }
388
389    #[test]
390    fn test_validate_range() {
391        assert!(validate_range(0.5, 0.0, 1.0, "param").is_ok());
392        assert!(validate_range(0.0, 0.0, 1.0, "param").is_ok());
393        assert!(validate_range(1.0, 0.0, 1.0, "param").is_ok());
394        assert!(validate_range(-0.1, 0.0, 1.0, "param").is_err());
395        assert!(validate_range(1.1, 0.0, 1.0, "param").is_err());
396    }
397
398    #[test]
399    fn test_error_categories() {
400        let err = CopulaError::invalid_parameter("test");
401        assert_eq!(err.category(), "parameter");
402        assert!(!err.is_recoverable());
403
404        let err = CopulaError::numerical("test");
405        assert_eq!(err.category(), "numerical");
406        assert!(err.is_recoverable());
407    }
408}