1pub type Result<T> = std::result::Result<T, CopulaError>;
12
13fn 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#[derive(Debug, thiserror::Error)]
33pub enum CopulaError {
34 #[error("Invalid parameter: {message}{}", .suggestion.as_ref().map(|s| format!("\nSuggestion: {}", s)).unwrap_or_default())]
40 InvalidParameter {
41 message: String,
43 suggestion: Option<String>,
45 },
46
47 #[error("Dimension mismatch{}: expected {expected}, got {actual}", .context.as_ref().map(|c| format!(" in {}", c)).unwrap_or_default())]
52 DimensionMismatch {
53 expected: usize,
55 actual: usize,
57 context: Option<String>,
59 },
60
61 #[error("Input values must be in [0,1]: found {} invalid value(s) - {}", .values.len(), format_invalid_values(.values))]
66 InvalidRange {
67 values: Vec<f64>,
69 },
70
71 #[error("Numerical error: {message}")]
76 NumericalError {
77 message: String,
79 },
80
81 #[error("Matrix operation failed: {operation} - {reason}")]
86 MatrixError {
87 operation: String,
89 reason: String,
91 },
92
93 #[cfg(feature = "estimation")]
98 #[cfg_attr(docsrs, doc(cfg(feature = "estimation")))]
99 #[error("Optimization failed: {reason}")]
100 OptimizationError {
101 reason: String,
103 },
104
105 #[error("Statistical error: {message}")]
110 StatisticalError {
111 message: String,
113 },
114
115 #[error("Data validation error: {message}")]
121 DataError {
122 message: String,
124 },
125
126 #[error("Not implemented: {feature}")]
131 NotImplemented {
132 feature: String,
134 },
135
136 #[error("Computation error: {message}")]
141 ComputationError {
142 message: String,
144 },
145
146 #[cfg(feature = "serde")]
150 #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
151 #[error("Serialization error: {message}")]
152 SerializationError {
153 message: String,
155 },
156}
157
158impl CopulaError {
159 pub fn invalid_parameter<S: Into<String>>(message: S) -> Self {
161 Self::InvalidParameter {
162 message: message.into(),
163 suggestion: None,
164 }
165 }
166
167 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 pub fn dimension_mismatch(expected: usize, actual: usize) -> Self {
177 Self::DimensionMismatch {
178 expected,
179 actual,
180 context: None,
181 }
182 }
183
184 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 pub fn invalid_range(values: Vec<f64>) -> Self {
199 Self::InvalidRange { values }
200 }
201
202 pub fn numerical<S: Into<String>>(message: S) -> Self {
204 Self::NumericalError {
205 message: message.into(),
206 }
207 }
208
209 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 #[cfg(feature = "estimation")]
219 pub fn optimization<S: Into<String>>(reason: S) -> Self {
220 Self::OptimizationError {
221 reason: reason.into(),
222 }
223 }
224
225 pub fn statistical<S: Into<String>>(message: S) -> Self {
227 Self::StatisticalError {
228 message: message.into(),
229 }
230 }
231
232 pub fn data_error<S: Into<String>>(message: S) -> Self {
234 Self::DataError {
235 message: message.into(),
236 }
237 }
238
239 pub fn not_implemented<S: Into<String>>(feature: S) -> Self {
241 Self::NotImplemented {
242 feature: feature.into(),
243 }
244 }
245
246 pub fn computation<S: Into<String>>(message: S) -> Self {
248 Self::ComputationError {
249 message: message.into(),
250 }
251 }
252
253 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 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
289pub 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
306pub 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
318pub 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
330pub 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
342pub 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
351pub 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 assert!(validate_unit_range(&[0.0, 0.5, 1.0]).is_ok());
371
372 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}