autoeq_de/
error.rs

1//! Error types for the Differential Evolution optimizer.
2//!
3//! This module provides structured error handling for DE optimization,
4//! following the Microsoft Rust Guidelines pattern of using `thiserror`
5//! for library error types with helper methods for error categorization.
6
7use thiserror::Error;
8
9/// Errors that can occur during Differential Evolution optimization.
10#[derive(Debug, Error)]
11pub enum DEError {
12    /// Lower and upper bounds have different lengths.
13    #[error("bounds mismatch: lower has {lower_len} elements, upper has {upper_len}")]
14    BoundsMismatch {
15        /// Length of the lower bounds array
16        lower_len: usize,
17        /// Length of the upper bounds array
18        upper_len: usize,
19    },
20
21    /// A lower bound exceeds its corresponding upper bound.
22    #[error("invalid bounds at index {index}: lower ({lower}) > upper ({upper})")]
23    InvalidBounds {
24        /// Index of the invalid bound pair
25        index: usize,
26        /// The lower bound value
27        lower: f64,
28        /// The upper bound value
29        upper: f64,
30    },
31
32    /// Population size is too small (must be >= 4).
33    #[error("population size ({pop_size}) must be >= 4")]
34    PopulationTooSmall {
35        /// The invalid population size
36        pop_size: usize,
37    },
38
39    /// Mutation factor is out of valid range [0, 2].
40    #[error("invalid mutation factor: {factor} (must be in [0, 2])")]
41    InvalidMutationFactor {
42        /// The invalid mutation factor
43        factor: f64,
44    },
45
46    /// Crossover rate is out of valid range [0, 1].
47    #[error("invalid crossover rate: {rate} (must be in [0, 1])")]
48    InvalidCrossoverRate {
49        /// The invalid crossover rate
50        rate: f64,
51    },
52
53    /// Initial guess (x0) has wrong dimension.
54    #[error("x0 dimension mismatch: expected {expected}, got {got}")]
55    X0DimensionMismatch {
56        /// Expected dimension
57        expected: usize,
58        /// Actual dimension provided
59        got: usize,
60    },
61
62    /// Integrality mask has wrong dimension.
63    #[error("integrality mask dimension mismatch: expected {expected}, got {got}")]
64    IntegralityDimensionMismatch {
65        /// Expected dimension
66        expected: usize,
67        /// Actual dimension provided
68        got: usize,
69    },
70}
71
72/// A specialized `Result` type for DE operations.
73pub type Result<T> = std::result::Result<T, DEError>;
74
75impl DEError {
76    /// Returns `true` if this is a bounds-related error.
77    ///
78    /// This includes `BoundsMismatch` and `InvalidBounds` variants.
79    pub fn is_bounds_error(&self) -> bool {
80        matches!(self, DEError::BoundsMismatch { .. } | DEError::InvalidBounds { .. })
81    }
82
83    /// Returns `true` if this is a configuration-related error.
84    ///
85    /// This includes `PopulationTooSmall`, `InvalidMutationFactor`,
86    /// and `InvalidCrossoverRate` variants.
87    pub fn is_config_error(&self) -> bool {
88        matches!(
89            self,
90            DEError::PopulationTooSmall { .. }
91                | DEError::InvalidMutationFactor { .. }
92                | DEError::InvalidCrossoverRate { .. }
93        )
94    }
95
96    /// Returns `true` if this is a dimension mismatch error.
97    ///
98    /// This includes `X0DimensionMismatch` and `IntegralityDimensionMismatch`.
99    pub fn is_dimension_error(&self) -> bool {
100        matches!(
101            self,
102            DEError::X0DimensionMismatch { .. } | DEError::IntegralityDimensionMismatch { .. }
103        )
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_error_display() {
113        let err = DEError::BoundsMismatch {
114            lower_len: 3,
115            upper_len: 5,
116        };
117        assert_eq!(
118            err.to_string(),
119            "bounds mismatch: lower has 3 elements, upper has 5"
120        );
121    }
122
123    #[test]
124    fn test_is_bounds_error() {
125        let bounds_err = DEError::BoundsMismatch {
126            lower_len: 1,
127            upper_len: 2,
128        };
129        let config_err = DEError::PopulationTooSmall { pop_size: 2 };
130
131        assert!(bounds_err.is_bounds_error());
132        assert!(!config_err.is_bounds_error());
133    }
134
135    #[test]
136    fn test_is_config_error() {
137        let config_err = DEError::InvalidCrossoverRate { rate: 1.5 };
138        let bounds_err = DEError::InvalidBounds {
139            index: 0,
140            lower: 5.0,
141            upper: 3.0,
142        };
143
144        assert!(config_err.is_config_error());
145        assert!(!bounds_err.is_config_error());
146    }
147
148    #[test]
149    fn test_is_dimension_error() {
150        let dim_err = DEError::X0DimensionMismatch {
151            expected: 10,
152            got: 5,
153        };
154        let bounds_err = DEError::BoundsMismatch {
155            lower_len: 1,
156            upper_len: 2,
157        };
158
159        assert!(dim_err.is_dimension_error());
160        assert!(!bounds_err.is_dimension_error());
161    }
162}