pub type Result<T> = std::result::Result<T, CopulaError>;
fn format_invalid_values(values: &[f64]) -> String {
if values.len() <= 3 {
format!("{:?}", values)
} else {
format!(
"[{}, {}, {} ... and {} more]",
values[0],
values[1],
values[2],
values.len() - 3
)
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CopulaError {
#[error("Invalid parameter: {message}{}", .suggestion.as_ref().map(|s| format!("\nSuggestion: {}", s)).unwrap_or_default())]
InvalidParameter {
message: String,
suggestion: Option<String>,
},
#[error("Dimension mismatch{}: expected {expected}, got {actual}", .context.as_ref().map(|c| format!(" in {}", c)).unwrap_or_default())]
DimensionMismatch {
expected: usize,
actual: usize,
context: Option<String>,
},
#[error("Input values must be in [0,1]: found {} invalid value(s) - {}", .values.len(), format_invalid_values(.values))]
InvalidRange {
values: Vec<f64>,
},
#[error("Numerical error: {message}")]
NumericalError {
message: String,
},
#[error("Matrix operation failed: {operation} - {reason}")]
MatrixError {
operation: String,
reason: String,
},
#[cfg(feature = "estimation")]
#[cfg_attr(docsrs, doc(cfg(feature = "estimation")))]
#[error("Optimization failed: {reason}")]
OptimizationError {
reason: String,
},
#[error("Statistical error: {message}")]
StatisticalError {
message: String,
},
#[error("Data validation error: {message}")]
DataError {
message: String,
},
#[error("Not implemented: {feature}")]
NotImplemented {
feature: String,
},
#[error("Computation error: {message}")]
ComputationError {
message: String,
},
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
#[error("Serialization error: {message}")]
SerializationError {
message: String,
},
}
impl CopulaError {
pub fn invalid_parameter<S: Into<String>>(message: S) -> Self {
Self::InvalidParameter {
message: message.into(),
suggestion: None,
}
}
pub fn invalid_parameter_with_suggestion<S: Into<String>>(message: S, suggestion: S) -> Self {
Self::InvalidParameter {
message: message.into(),
suggestion: Some(suggestion.into()),
}
}
pub fn dimension_mismatch(expected: usize, actual: usize) -> Self {
Self::DimensionMismatch {
expected,
actual,
context: None,
}
}
pub fn dimension_mismatch_with_context<S: Into<String>>(
expected: usize,
actual: usize,
context: S,
) -> Self {
Self::DimensionMismatch {
expected,
actual,
context: Some(context.into()),
}
}
pub fn invalid_range(values: Vec<f64>) -> Self {
Self::InvalidRange { values }
}
pub fn numerical<S: Into<String>>(message: S) -> Self {
Self::NumericalError {
message: message.into(),
}
}
pub fn matrix_error<S: Into<String>>(operation: S, reason: S) -> Self {
Self::MatrixError {
operation: operation.into(),
reason: reason.into(),
}
}
#[cfg(feature = "estimation")]
pub fn optimization<S: Into<String>>(reason: S) -> Self {
Self::OptimizationError {
reason: reason.into(),
}
}
pub fn statistical<S: Into<String>>(message: S) -> Self {
Self::StatisticalError {
message: message.into(),
}
}
pub fn data_error<S: Into<String>>(message: S) -> Self {
Self::DataError {
message: message.into(),
}
}
pub fn not_implemented<S: Into<String>>(feature: S) -> Self {
Self::NotImplemented {
feature: feature.into(),
}
}
pub fn computation<S: Into<String>>(message: S) -> Self {
Self::ComputationError {
message: message.into(),
}
}
pub fn is_recoverable(&self) -> bool {
match self {
CopulaError::NumericalError { .. } => true,
#[cfg(feature = "estimation")]
CopulaError::OptimizationError { .. } => true,
CopulaError::StatisticalError { .. } => true,
CopulaError::ComputationError { .. } => true,
_ => false,
}
}
pub fn category(&self) -> &'static str {
match self {
CopulaError::InvalidParameter { .. } => "parameter",
CopulaError::DimensionMismatch { .. } => "dimension",
CopulaError::InvalidRange { .. } => "range",
CopulaError::NumericalError { .. } => "numerical",
CopulaError::MatrixError { .. } => "matrix",
#[cfg(feature = "estimation")]
CopulaError::OptimizationError { .. } => "optimization",
CopulaError::StatisticalError { .. } => "statistical",
CopulaError::DataError { .. } => "data",
CopulaError::NotImplemented { .. } => "implementation",
CopulaError::ComputationError { .. } => "computation",
#[cfg(feature = "serde")]
CopulaError::SerializationError { .. } => "serialization",
}
}
}
pub fn validate_unit_range(values: &[f64]) -> Result<()> {
let invalid_values: Vec<f64> = values
.iter()
.copied()
.filter(|&x| !(0.0..=1.0).contains(&x))
.collect();
if invalid_values.is_empty() {
Ok(())
} else {
Err(CopulaError::invalid_range(invalid_values))
}
}
pub fn validate_positive(value: f64, name: &str) -> Result<()> {
if value > 0.0 && value.is_finite() {
Ok(())
} else {
Err(CopulaError::invalid_parameter(format!(
"{} must be positive and finite, got {}",
name, value
)))
}
}
pub fn validate_non_negative(value: f64, name: &str) -> Result<()> {
if value >= 0.0 && value.is_finite() {
Ok(())
} else {
Err(CopulaError::invalid_parameter(format!(
"{} must be non-negative and finite, got {}",
name, value
)))
}
}
pub fn validate_range(value: f64, min: f64, max: f64, name: &str) -> Result<()> {
if value >= min && value <= max && value.is_finite() {
Ok(())
} else {
Err(CopulaError::invalid_parameter(format!(
"{} must be in [{}, {}], got {}",
name, min, max, value
)))
}
}
pub fn validate_dimensions(expected: usize, actual: usize, _context: &str) -> Result<()> {
if expected == actual {
Ok(())
} else {
Err(CopulaError::dimension_mismatch(expected, actual))
}
}
pub fn validate_finite_data(data: &[f64], name: &str) -> Result<()> {
if data.iter().all(|x| x.is_finite()) {
Ok(())
} else {
Err(CopulaError::data_error(format!(
"{} contains non-finite values (NaN or infinite)",
name
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_unit_range() {
assert!(validate_unit_range(&[0.0, 0.5, 1.0]).is_ok());
assert!(validate_unit_range(&[-0.1, 0.5]).is_err());
assert!(validate_unit_range(&[0.5, 1.1]).is_err());
assert!(validate_unit_range(&[f64::NAN]).is_err());
}
#[test]
fn test_validate_positive() {
assert!(validate_positive(1.0, "theta").is_ok());
assert!(validate_positive(0.0, "theta").is_err());
assert!(validate_positive(-1.0, "theta").is_err());
assert!(validate_positive(f64::NAN, "theta").is_err());
assert!(validate_positive(f64::INFINITY, "theta").is_err());
}
#[test]
fn test_validate_range() {
assert!(validate_range(0.5, 0.0, 1.0, "param").is_ok());
assert!(validate_range(0.0, 0.0, 1.0, "param").is_ok());
assert!(validate_range(1.0, 0.0, 1.0, "param").is_ok());
assert!(validate_range(-0.1, 0.0, 1.0, "param").is_err());
assert!(validate_range(1.1, 0.0, 1.0, "param").is_err());
}
#[test]
fn test_error_categories() {
let err = CopulaError::invalid_parameter("test");
assert_eq!(err.category(), "parameter");
assert!(!err.is_recoverable());
let err = CopulaError::numerical("test");
assert_eq!(err.category(), "numerical");
assert!(err.is_recoverable());
}
}