Skip to main content

fugue_evo/
error.rs

1//! Error types for fugue-evo
2//!
3//! This module defines all error types used throughout the library.
4
5use thiserror::Error;
6
7/// Address type for genome trace operations
8pub type Address = String;
9
10/// Error type for genome operations
11#[derive(Debug, Error, Clone, PartialEq)]
12pub enum GenomeError {
13    /// A required address was missing from the trace
14    #[error("Missing address in trace: {0}")]
15    MissingAddress(Address),
16
17    /// Type mismatch when converting from trace
18    #[error("Type mismatch at address {address}: expected {expected}, got {actual}")]
19    TypeMismatch {
20        address: Address,
21        expected: String,
22        actual: String,
23    },
24
25    /// Invalid genome structure
26    #[error("Invalid genome structure: {0}")]
27    InvalidStructure(String),
28
29    /// Constraint violation in genome
30    #[error("Constraint violation: {0}")]
31    ConstraintViolation(String),
32
33    /// Dimension mismatch
34    #[error("Dimension mismatch: expected {expected}, got {actual}")]
35    DimensionMismatch { expected: usize, actual: usize },
36}
37
38/// Error type for operator failures
39#[derive(Debug, Error, Clone, PartialEq)]
40pub enum OperatorError {
41    /// Crossover operation failed
42    #[error("Crossover failed: {0}")]
43    CrossoverFailed(String),
44
45    /// Mutation operation failed
46    #[error("Mutation failed: {0}")]
47    MutationFailed(String),
48
49    /// Selection operation failed
50    #[error("Selection failed: {0}")]
51    SelectionFailed(String),
52
53    /// Invalid operator configuration
54    #[error("Invalid operator configuration: {0}")]
55    InvalidConfiguration(String),
56}
57
58/// Error type for checkpoint operations
59#[derive(Debug, Error)]
60pub enum CheckpointError {
61    /// IO error during checkpoint (native only)
62    #[cfg(not(target_arch = "wasm32"))]
63    #[error("IO error: {0}")]
64    Io(#[from] std::io::Error),
65
66    /// Storage error (WASM-compatible alternative to IO)
67    #[cfg(target_arch = "wasm32")]
68    #[error("Storage error: {0}")]
69    Storage(String),
70
71    /// Serialization error
72    #[error("Serialization error: {0}")]
73    Serialization(String),
74
75    /// Deserialization error
76    #[error("Deserialization error: {0}")]
77    Deserialization(String),
78
79    /// Checkpoint version mismatch
80    #[error("Version mismatch: expected {expected}, found {found}")]
81    VersionMismatch { expected: u32, found: u32 },
82
83    /// Checkpoint version is too new
84    #[error("Checkpoint version {0} is newer than supported")]
85    VersionTooNew(u32),
86
87    /// Checkpoint version is too old
88    #[error("Checkpoint version {0} is too old to load")]
89    VersionTooOld(u32),
90
91    /// Checkpoint file not found
92    #[error("Checkpoint not found: {0}")]
93    NotFound(String),
94
95    /// Corrupted checkpoint data
96    #[error("Corrupted checkpoint: {0}")]
97    Corrupted(String),
98}
99
100/// Top-level error type for evolution operations
101#[derive(Debug, Error)]
102pub enum EvolutionError {
103    /// Genome error
104    #[error("Genome error: {0}")]
105    Genome(#[from] GenomeError),
106
107    /// Operator error
108    #[error("Operator error: {0}")]
109    Operator(#[from] OperatorError),
110
111    /// Fitness evaluation failed
112    #[error("Fitness evaluation failed: {0}")]
113    FitnessEvaluation(String),
114
115    /// Invalid configuration
116    #[error("Invalid configuration: {0}")]
117    Configuration(String),
118
119    /// Checkpoint error
120    #[error("Checkpoint error: {0}")]
121    Checkpoint(#[from] CheckpointError),
122
123    /// Numerical instability
124    #[error("Numerical instability: {0}")]
125    Numerical(String),
126
127    /// Empty population
128    #[error("Empty population")]
129    EmptyPopulation,
130
131    /// Interactive evaluation error
132    #[error("Interactive evaluation error: {0}")]
133    InteractiveEvaluation(String),
134
135    /// Insufficient evaluation coverage
136    #[error("Insufficient coverage: {coverage:.1}% (need {required:.1}%)")]
137    InsufficientCoverage {
138        /// Actual coverage achieved
139        coverage: f64,
140        /// Required coverage threshold
141        required: f64,
142    },
143}
144
145/// Result type alias for evolution operations
146pub type EvoResult<T> = Result<T, EvolutionError>;
147
148/// Repair information when an operator needs to fix a constraint violation
149#[derive(Debug, Clone)]
150pub struct RepairInfo {
151    /// List of constraint violations that were repaired
152    pub constraint_violations: Vec<String>,
153    /// Method used to repair the genome
154    pub repair_method: &'static str,
155}
156
157/// Result of an operator application with optional repair information
158#[derive(Debug, Clone)]
159pub enum OperatorResult<G> {
160    /// Operation succeeded without repairs
161    Success(G),
162    /// Operation succeeded but required repairs
163    Repaired(G, RepairInfo),
164    /// Operation failed unrecoverably
165    Failed(OperatorError),
166}
167
168impl<G> OperatorResult<G> {
169    /// Returns the genome if successful or repaired, None if failed
170    pub fn genome(self) -> Option<G> {
171        match self {
172            Self::Success(g) | Self::Repaired(g, _) => Some(g),
173            Self::Failed(_) => None,
174        }
175    }
176
177    /// Returns true if the operation was successful (with or without repairs)
178    pub fn is_ok(&self) -> bool {
179        !matches!(self, Self::Failed(_))
180    }
181
182    /// Returns true if repairs were needed
183    pub fn was_repaired(&self) -> bool {
184        matches!(self, Self::Repaired(_, _))
185    }
186
187    /// Maps the genome type
188    pub fn map<U, F: FnOnce(G) -> U>(self, f: F) -> OperatorResult<U> {
189        match self {
190            Self::Success(g) => OperatorResult::Success(f(g)),
191            Self::Repaired(g, info) => OperatorResult::Repaired(f(g), info),
192            Self::Failed(e) => OperatorResult::Failed(e),
193        }
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_genome_error_display() {
203        let err = GenomeError::MissingAddress("gene_0".to_string());
204        assert_eq!(err.to_string(), "Missing address in trace: gene_0");
205
206        let err = GenomeError::TypeMismatch {
207            address: "gene_1".to_string(),
208            expected: "f64".to_string(),
209            actual: "bool".to_string(),
210        };
211        assert_eq!(
212            err.to_string(),
213            "Type mismatch at address gene_1: expected f64, got bool"
214        );
215
216        let err = GenomeError::DimensionMismatch {
217            expected: 10,
218            actual: 5,
219        };
220        assert_eq!(err.to_string(), "Dimension mismatch: expected 10, got 5");
221    }
222
223    #[test]
224    fn test_operator_error_display() {
225        let err = OperatorError::CrossoverFailed("incompatible parents".to_string());
226        assert_eq!(err.to_string(), "Crossover failed: incompatible parents");
227
228        let err = OperatorError::InvalidConfiguration("eta must be positive".to_string());
229        assert_eq!(
230            err.to_string(),
231            "Invalid operator configuration: eta must be positive"
232        );
233    }
234
235    #[test]
236    fn test_evolution_error_from_genome_error() {
237        let genome_err = GenomeError::InvalidStructure("bad shape".to_string());
238        let evo_err: EvolutionError = genome_err.into();
239        assert!(matches!(evo_err, EvolutionError::Genome(_)));
240    }
241
242    #[test]
243    fn test_operator_result_success() {
244        let result: OperatorResult<i32> = OperatorResult::Success(42);
245        assert!(result.is_ok());
246        assert!(!result.was_repaired());
247        assert_eq!(result.genome(), Some(42));
248    }
249
250    #[test]
251    fn test_operator_result_repaired() {
252        let repair_info = RepairInfo {
253            constraint_violations: vec!["out of bounds".to_string()],
254            repair_method: "clamp",
255        };
256        let result: OperatorResult<i32> = OperatorResult::Repaired(42, repair_info);
257        assert!(result.is_ok());
258        assert!(result.was_repaired());
259        assert_eq!(result.genome(), Some(42));
260    }
261
262    #[test]
263    fn test_operator_result_failed() {
264        let result: OperatorResult<i32> =
265            OperatorResult::Failed(OperatorError::MutationFailed("test".to_string()));
266        assert!(!result.is_ok());
267        assert!(!result.was_repaired());
268        assert_eq!(result.genome(), None);
269    }
270
271    #[test]
272    fn test_operator_result_map() {
273        let result: OperatorResult<i32> = OperatorResult::Success(42);
274        let mapped = result.map(|x| x * 2);
275        assert_eq!(mapped.genome(), Some(84));
276    }
277}