1pub type Result<T> = std::result::Result<T, CopulaError>;
9
10fn 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#[derive(Debug, thiserror::Error)]
34#[non_exhaustive]
35pub enum CopulaError {
36 #[error("Invalid parameter: {message}{}", .suggestion.as_ref().map(|s| format!("\nSuggestion: {}", s)).unwrap_or_default())]
42 InvalidParameter {
43 message: String,
45 suggestion: Option<String>,
47 },
48
49 #[error("Dimension mismatch{}: expected {expected}, got {actual}", .context.as_ref().map(|c| format!(" in {}", c)).unwrap_or_default())]
54 DimensionMismatch {
55 expected: usize,
57 actual: usize,
59 context: Option<String>,
61 },
62
63 #[error("Input values must be in [0,1]: found {} invalid value(s) - {}", .values.len(), format_invalid_values(.values))]
68 InvalidRange {
69 values: Vec<f64>,
71 },
72
73 #[error("Numerical error: {message}")]
78 NumericalError {
79 message: String,
81 },
82
83 #[error("Matrix operation failed: {operation} - {reason}")]
88 MatrixError {
89 operation: String,
91 reason: String,
93 },
94
95 #[cfg(feature = "estimation")]
100 #[cfg_attr(docsrs, doc(cfg(feature = "estimation")))]
101 #[error("Optimization failed: {reason}")]
102 OptimizationError {
103 reason: String,
105 },
106
107 #[error("Statistical error: {message}")]
112 StatisticalError {
113 message: String,
115 },
116
117 #[error("Data validation error: {message}")]
123 DataError {
124 message: String,
126 },
127
128 #[error("Not implemented: {feature}")]
133 NotImplemented {
134 feature: String,
136 },
137
138 #[error("Computation error: {message}")]
143 ComputationError {
144 message: String,
146 },
147
148 #[cfg(feature = "serde")]
152 #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
153 #[error("Serialization error: {message}")]
154 SerializationError {
155 message: String,
157 },
158}
159
160impl CopulaError {
161 pub fn invalid_parameter<S: Into<String>>(message: S) -> Self {
163 Self::InvalidParameter {
164 message: message.into(),
165 suggestion: None,
166 }
167 }
168
169 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 pub fn dimension_mismatch(expected: usize, actual: usize) -> Self {
179 Self::DimensionMismatch {
180 expected,
181 actual,
182 context: None,
183 }
184 }
185
186 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 pub fn invalid_range(values: Vec<f64>) -> Self {
201 Self::InvalidRange { values }
202 }
203
204 pub fn numerical<S: Into<String>>(message: S) -> Self {
206 Self::NumericalError {
207 message: message.into(),
208 }
209 }
210
211 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 #[cfg(feature = "estimation")]
221 pub fn optimization<S: Into<String>>(reason: S) -> Self {
222 Self::OptimizationError {
223 reason: reason.into(),
224 }
225 }
226
227 pub fn statistical<S: Into<String>>(message: S) -> Self {
229 Self::StatisticalError {
230 message: message.into(),
231 }
232 }
233
234 pub fn data_error<S: Into<String>>(message: S) -> Self {
236 Self::DataError {
237 message: message.into(),
238 }
239 }
240
241 pub fn not_implemented<S: Into<String>>(feature: S) -> Self {
243 Self::NotImplemented {
244 feature: feature.into(),
245 }
246 }
247
248 pub fn computation<S: Into<String>>(message: S) -> Self {
250 Self::ComputationError {
251 message: message.into(),
252 }
253 }
254
255 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 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
291pub 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
308pub 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
320pub 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
332pub 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
344pub 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
353pub 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 assert!(validate_unit_range(&[0.0, 0.5, 1.0]).is_ok());
373
374 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}