Skip to main content

copula_core/
error.rs

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