Skip to main content

simular/
error.rs

1//! Error types for simular.
2//!
3//! Implements JPL Power of 10 Rule 7: Check all return values.
4//! All functions return `Result<T, SimError>` instead of panicking.
5
6use thiserror::Error;
7
8/// Result type alias for simular operations.
9pub type SimResult<T> = Result<T, SimError>;
10
11/// Unified error type for all simular operations.
12///
13/// # Design
14///
15/// Following Toyota's Jidoka principle, errors are:
16/// 1. Immediately detectable (type-safe)
17/// 2. Self-documenting (descriptive variants)
18/// 3. Actionable (contain recovery hints)
19#[derive(Debug, Error)]
20pub enum SimError {
21    // ===== Jidoka Violations =====
22    /// Numerical instability detected (NaN or Inf).
23    #[error("Jidoka: non-finite value detected at {location}")]
24    NonFiniteValue {
25        /// Location where the non-finite value was detected.
26        location: String,
27    },
28
29    /// Energy conservation violated beyond tolerance.
30    #[error("Jidoka: energy drift {drift:.6e} exceeds tolerance {tolerance:.6e}")]
31    EnergyDrift {
32        /// Relative energy drift from initial state.
33        drift: f64,
34        /// Configured tolerance threshold.
35        tolerance: f64,
36    },
37
38    /// Constraint violation detected.
39    #[error(
40        "Jidoka: constraint '{name}' violated by {violation:.6e} (tolerance: {tolerance:.6e})"
41    )]
42    ConstraintViolation {
43        /// Name of the violated constraint.
44        name: String,
45        /// Amount of violation.
46        violation: f64,
47        /// Configured tolerance.
48        tolerance: f64,
49    },
50
51    // ===== Configuration Errors =====
52    /// Invalid configuration parameter.
53    #[error("Configuration error: {message}")]
54    Config {
55        /// Description of the configuration error.
56        message: String,
57    },
58
59    /// YAML parsing error.
60    #[error("YAML parsing error: {0}")]
61    YamlParse(#[from] serde_yaml::Error),
62
63    /// Validation error.
64    #[error("Validation error: {0}")]
65    Validation(#[from] validator::ValidationErrors),
66
67    // ===== Replay Errors =====
68    /// Checkpoint integrity violation.
69    #[error("Checkpoint integrity violation: hash mismatch")]
70    CheckpointIntegrity,
71
72    /// Checkpoint not found.
73    #[error("Checkpoint not found for time {0:?}")]
74    CheckpointNotFound(crate::engine::SimTime),
75
76    /// Journal read error.
77    #[error("Journal error: {0}")]
78    Journal(String),
79
80    // ===== I/O Errors =====
81    /// File I/O error.
82    #[error("I/O error: {0}")]
83    Io(#[from] std::io::Error),
84
85    /// Serialization error.
86    #[error("Serialization error: {0}")]
87    Serialization(String),
88
89    // ===== Domain Errors =====
90    /// Physics engine error.
91    #[error("Physics error: {0}")]
92    Physics(String),
93
94    /// Monte Carlo error.
95    #[error("Monte Carlo error: {0}")]
96    MonteCarlo(String),
97
98    /// Optimization error.
99    #[error("Optimization error: {0}")]
100    Optimization(String),
101
102    // ===== Falsification Errors =====
103    /// Hypothesis falsified.
104    #[error("Hypothesis falsified: {reason} (p-value: {p_value:.6})")]
105    HypothesisFalsified {
106        /// Reason for falsification.
107        reason: String,
108        /// Statistical p-value.
109        p_value: f64,
110    },
111}
112
113impl SimError {
114    /// Create a configuration error with a message.
115    #[must_use]
116    pub fn config(message: impl Into<String>) -> Self {
117        Self::Config {
118            message: message.into(),
119        }
120    }
121
122    /// Create a serialization error.
123    #[must_use]
124    pub fn serialization(message: impl Into<String>) -> Self {
125        Self::Serialization(message.into())
126    }
127
128    /// Create a journal error.
129    #[must_use]
130    pub fn journal(message: impl Into<String>) -> Self {
131        Self::Journal(message.into())
132    }
133
134    /// Create an optimization error.
135    #[must_use]
136    pub fn optimization(message: impl Into<String>) -> Self {
137        Self::Optimization(message.into())
138    }
139
140    /// Create a Jidoka violation error (requires immediate stop).
141    #[must_use]
142    pub fn jidoka(message: impl Into<String>) -> Self {
143        Self::Config {
144            message: format!("Jidoka violation: {}", message.into()),
145        }
146    }
147
148    /// Create an I/O error with a message (wraps in `std::io::Error`).
149    #[must_use]
150    pub fn io(message: impl Into<String>) -> Self {
151        Self::Io(std::io::Error::other(message.into()))
152    }
153
154    /// Check if this error is a Jidoka violation (requires immediate stop).
155    #[must_use]
156    pub const fn is_jidoka_violation(&self) -> bool {
157        matches!(
158            self,
159            Self::NonFiniteValue { .. }
160                | Self::EnergyDrift { .. }
161                | Self::ConstraintViolation { .. }
162        )
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_jidoka_violation_detection() {
172        let non_finite = SimError::NonFiniteValue {
173            location: "position.x".to_string(),
174        };
175        assert!(non_finite.is_jidoka_violation());
176
177        let energy = SimError::EnergyDrift {
178            drift: 0.001,
179            tolerance: 0.0001,
180        };
181        assert!(energy.is_jidoka_violation());
182
183        let constraint = SimError::ConstraintViolation {
184            name: "mass_positive".to_string(),
185            violation: -1.0,
186            tolerance: 0.0,
187        };
188        assert!(constraint.is_jidoka_violation());
189
190        let config = SimError::config("invalid");
191        assert!(!config.is_jidoka_violation());
192    }
193
194    #[test]
195    fn test_error_display() {
196        let err = SimError::EnergyDrift {
197            drift: 0.001_234_567,
198            tolerance: 0.000_001,
199        };
200        let msg = err.to_string();
201        assert!(msg.contains("energy drift"));
202        assert!(msg.contains("1.234567e-3"));
203    }
204
205    #[test]
206    fn test_error_config() {
207        let err = SimError::config("invalid parameter");
208        assert!(!err.is_jidoka_violation());
209        let msg = err.to_string();
210        assert!(msg.contains("Configuration error"));
211        assert!(msg.contains("invalid parameter"));
212    }
213
214    #[test]
215    fn test_error_serialization() {
216        let err = SimError::serialization("failed to serialize");
217        assert!(!err.is_jidoka_violation());
218        let msg = err.to_string();
219        assert!(msg.contains("Serialization error"));
220        assert!(msg.contains("failed to serialize"));
221    }
222
223    #[test]
224    fn test_error_journal() {
225        let err = SimError::journal("corrupted journal");
226        assert!(!err.is_jidoka_violation());
227        let msg = err.to_string();
228        assert!(msg.contains("Journal error"));
229        assert!(msg.contains("corrupted journal"));
230    }
231
232    #[test]
233    fn test_error_optimization() {
234        let err = SimError::optimization("convergence failed");
235        assert!(!err.is_jidoka_violation());
236        let msg = err.to_string();
237        assert!(msg.contains("Optimization error"));
238        assert!(msg.contains("convergence failed"));
239    }
240
241    #[test]
242    fn test_error_jidoka() {
243        let err = SimError::jidoka("critical failure");
244        // jidoka() creates a Config error wrapping the message
245        assert!(!err.is_jidoka_violation());
246        let msg = err.to_string();
247        assert!(msg.contains("Jidoka violation"));
248        assert!(msg.contains("critical failure"));
249    }
250
251    #[test]
252    fn test_error_io() {
253        let err = SimError::io("file not found");
254        assert!(!err.is_jidoka_violation());
255        let msg = err.to_string();
256        assert!(msg.contains("I/O error"));
257        assert!(msg.contains("file not found"));
258    }
259
260    #[test]
261    fn test_error_non_finite_display() {
262        let err = SimError::NonFiniteValue {
263            location: "velocity.y".to_string(),
264        };
265        let msg = err.to_string();
266        assert!(msg.contains("non-finite value"));
267        assert!(msg.contains("velocity.y"));
268    }
269
270    #[test]
271    fn test_error_constraint_violation_display() {
272        let err = SimError::ConstraintViolation {
273            name: "mass_positive".to_string(),
274            violation: -5.0,
275            tolerance: 0.001,
276        };
277        let msg = err.to_string();
278        assert!(msg.contains("constraint"));
279        assert!(msg.contains("mass_positive"));
280        assert!(msg.contains("violated"));
281    }
282
283    #[test]
284    fn test_error_checkpoint_integrity() {
285        let err = SimError::CheckpointIntegrity;
286        assert!(!err.is_jidoka_violation());
287        let msg = err.to_string();
288        assert!(msg.contains("Checkpoint integrity"));
289    }
290
291    #[test]
292    fn test_error_checkpoint_not_found() {
293        let err = SimError::CheckpointNotFound(crate::engine::SimTime::from_secs(10.0));
294        assert!(!err.is_jidoka_violation());
295        let msg = err.to_string();
296        assert!(msg.contains("Checkpoint not found"));
297    }
298
299    #[test]
300    fn test_error_physics() {
301        let err = SimError::Physics("invalid force".to_string());
302        assert!(!err.is_jidoka_violation());
303        let msg = err.to_string();
304        assert!(msg.contains("Physics error"));
305    }
306
307    #[test]
308    fn test_error_monte_carlo() {
309        let err = SimError::MonteCarlo("invalid sample".to_string());
310        assert!(!err.is_jidoka_violation());
311        let msg = err.to_string();
312        assert!(msg.contains("Monte Carlo error"));
313    }
314
315    #[test]
316    fn test_error_hypothesis_falsified() {
317        let err = SimError::HypothesisFalsified {
318            reason: "energy not conserved".to_string(),
319            p_value: 0.001,
320        };
321        assert!(!err.is_jidoka_violation());
322        let msg = err.to_string();
323        assert!(msg.contains("Hypothesis falsified"));
324        assert!(msg.contains("energy not conserved"));
325        assert!(msg.contains("0.001"));
326    }
327
328    #[test]
329    fn test_error_debug() {
330        let err = SimError::config("test");
331        let debug = format!("{:?}", err);
332        assert!(debug.contains("Config"));
333    }
334}