Skip to main content

simular/edd/
traits.rs

1//! Core EDD traits for simulation compliance.
2//!
3//! This module defines the mandatory traits that every EDD-compliant
4//! simulation must implement:
5//!
6//! - `Reproducible`: Deterministic seeding and RNG state management
7//! - `YamlConfigurable`: Configuration from YAML experiment specs
8//! - `EddSimulation`: Supertrait combining all EDD requirements
9//!
10//! # EDD-03: Deterministic Reproducibility
11//!
12//! > **Claim:** Identical seeds produce bitwise-identical results.
13//! > **Rejection Criteria:** Any non-determinism in simulation output.
14//!
15//! # References
16//!
17//! - [9] Hill, D.R.C. (2023). Numerical Reproducibility of Parallel Stochastic Simulation
18//! - [10] Hinsen, K. (2015). Reproducibility in Computational Neuroscience
19
20use super::equation::GoverningEquation;
21use super::experiment::ExperimentSpec;
22use super::falsifiable::FalsifiableSimulation;
23use super::model_card::EquationModelCard;
24use std::collections::HashMap;
25
26/// Error type for configuration operations.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct ConfigError {
29    /// Error message
30    pub message: String,
31    /// Field that caused the error (if applicable)
32    pub field: Option<String>,
33    /// Underlying cause (if any)
34    pub cause: Option<String>,
35}
36
37impl ConfigError {
38    /// Create a new configuration error.
39    #[must_use]
40    pub fn new(message: &str) -> Self {
41        Self {
42            message: message.to_string(),
43            field: None,
44            cause: None,
45        }
46    }
47
48    /// Create an error for a specific field.
49    #[must_use]
50    pub fn field_error(field: &str, message: &str) -> Self {
51        Self {
52            message: message.to_string(),
53            field: Some(field.to_string()),
54            cause: None,
55        }
56    }
57
58    /// Add a cause to the error.
59    #[must_use]
60    pub fn with_cause(mut self, cause: &str) -> Self {
61        self.cause = Some(cause.to_string());
62        self
63    }
64}
65
66impl std::fmt::Display for ConfigError {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        if let Some(ref field) = self.field {
69            write!(f, "Config error in '{}': {}", field, self.message)?;
70        } else {
71            write!(f, "Config error: {}", self.message)?;
72        }
73        if let Some(ref cause) = self.cause {
74            write!(f, " (caused by: {cause})")?;
75        }
76        Ok(())
77    }
78}
79
80impl std::error::Error for ConfigError {}
81
82/// Result of validating configuration against EMC.
83#[derive(Debug, Clone)]
84pub struct ValidationResult {
85    /// Whether validation passed
86    pub valid: bool,
87    /// List of validation errors
88    pub errors: Vec<String>,
89    /// List of validation warnings
90    pub warnings: Vec<String>,
91    /// Parameters that were validated
92    pub validated_params: Vec<String>,
93}
94
95impl ValidationResult {
96    /// Create a successful validation result.
97    #[must_use]
98    pub fn success(validated_params: Vec<String>) -> Self {
99        Self {
100            valid: true,
101            errors: Vec::new(),
102            warnings: Vec::new(),
103            validated_params,
104        }
105    }
106
107    /// Create a failed validation result.
108    #[must_use]
109    pub fn failure(errors: Vec<String>) -> Self {
110        Self {
111            valid: false,
112            errors,
113            warnings: Vec::new(),
114            validated_params: Vec::new(),
115        }
116    }
117
118    /// Add a warning to the result.
119    #[must_use]
120    pub fn with_warning(mut self, warning: &str) -> Self {
121        self.warnings.push(warning.to_string());
122        self
123    }
124}
125
126/// Result of verifying implementation against EMC test cases.
127#[derive(Debug, Clone)]
128pub struct VerificationResult {
129    /// Whether all verification tests passed
130    pub passed: bool,
131    /// Total number of tests
132    pub total_tests: usize,
133    /// Number of passed tests
134    pub passed_tests: usize,
135    /// Number of failed tests
136    pub failed_tests: usize,
137    /// Details of each test result
138    pub test_results: Vec<TestResult>,
139}
140
141impl VerificationResult {
142    /// Create a new verification result from test outcomes.
143    #[must_use]
144    pub fn from_tests(test_results: Vec<TestResult>) -> Self {
145        let passed_tests = test_results.iter().filter(|t| t.passed).count();
146        let failed_tests = test_results.len() - passed_tests;
147
148        Self {
149            passed: failed_tests == 0,
150            total_tests: test_results.len(),
151            passed_tests,
152            failed_tests,
153            test_results,
154        }
155    }
156
157    /// Create an empty (all passed) result.
158    #[must_use]
159    pub fn empty() -> Self {
160        Self {
161            passed: true,
162            total_tests: 0,
163            passed_tests: 0,
164            failed_tests: 0,
165            test_results: Vec::new(),
166        }
167    }
168}
169
170/// Result of a single verification test.
171#[derive(Debug, Clone)]
172pub struct TestResult {
173    /// Test name
174    pub name: String,
175    /// Whether the test passed
176    pub passed: bool,
177    /// Expected value
178    pub expected: f64,
179    /// Actual value
180    pub actual: f64,
181    /// Tolerance used
182    pub tolerance: f64,
183    /// Human-readable message
184    pub message: String,
185}
186
187impl TestResult {
188    /// Create a passed test result.
189    #[must_use]
190    pub fn pass(name: &str, expected: f64, actual: f64, tolerance: f64) -> Self {
191        Self {
192            name: name.to_string(),
193            passed: true,
194            expected,
195            actual,
196            tolerance,
197            message: format!(
198                "Test '{name}' PASSED: expected {expected:.6}, got {actual:.6} (tol: {tolerance:.6})"
199            ),
200        }
201    }
202
203    /// Create a failed test result.
204    #[must_use]
205    pub fn fail(name: &str, expected: f64, actual: f64, tolerance: f64) -> Self {
206        Self {
207            name: name.to_string(),
208            passed: false,
209            expected,
210            actual,
211            tolerance,
212            message: format!(
213                "Test '{name}' FAILED: expected {expected:.6}, got {actual:.6} (tol: {tolerance:.6})"
214            ),
215        }
216    }
217}
218
219/// Reproducibility trait for deterministic simulation.
220///
221/// Implements EDD-03: Deterministic Reproducibility.
222///
223/// # Guarantee
224///
225/// For all runs r1, r2 with identical seeds:
226/// ```text
227/// S(I, σ) → R₁ ∧ S(I, σ) → R₂ ⟹ R₁ ≡ R₂
228/// ```
229///
230/// # Example
231///
232/// ```ignore
233/// impl Reproducible for MySimulation {
234///     fn set_seed(&mut self, seed: u64) {
235///         self.rng = StdRng::seed_from_u64(seed);
236///     }
237///
238///     fn rng_state(&self) -> [u8; 32] {
239///         // Serialize RNG state for checkpointing
240///     }
241///
242///     fn restore_rng_state(&mut self, state: &[u8; 32]) {
243///         // Restore RNG state from checkpoint
244///     }
245/// }
246/// ```
247pub trait Reproducible {
248    /// Set the master seed for all RNG operations.
249    fn set_seed(&mut self, seed: u64);
250
251    /// Get current RNG state for checkpointing.
252    ///
253    /// Returns a 32-byte array representing the complete RNG state
254    /// that can be used to restore the simulation to this exact point.
255    fn rng_state(&self) -> [u8; 32];
256
257    /// Restore RNG state from a checkpoint.
258    ///
259    /// # Arguments
260    ///
261    /// * `state` - A 32-byte array previously returned by `rng_state()`
262    fn restore_rng_state(&mut self, state: &[u8; 32]);
263
264    /// Get the current seed value.
265    fn current_seed(&self) -> u64;
266}
267
268/// YAML configuration trait for EDD simulations.
269///
270/// Allows simulations to be configured from declarative YAML
271/// experiment specifications without custom code.
272///
273/// # Example
274///
275/// ```ignore
276/// impl YamlConfigurable for HarmonicOscillator {
277///     fn from_yaml(spec: &ExperimentSpec) -> Result<Self, ConfigError> {
278///         let omega = spec.parameter("omega")
279///             .ok_or_else(|| ConfigError::field_error("omega", "required"))?;
280///         Ok(Self::new(omega))
281///     }
282///
283///     fn validate_against_emc(&self, emc: &EquationModelCard) -> ValidationResult {
284///         // Check parameters are within EMC domain of validity
285///     }
286/// }
287/// ```
288pub trait YamlConfigurable: Sized {
289    /// Create simulation from YAML experiment specification.
290    ///
291    /// # Arguments
292    ///
293    /// * `spec` - The experiment specification from YAML
294    ///
295    /// # Errors
296    ///
297    /// Returns `ConfigError` if the specification is invalid or
298    /// missing required parameters.
299    fn from_yaml(spec: &ExperimentSpec) -> Result<Self, ConfigError>;
300
301    /// Validate configuration against EMC domain of validity.
302    ///
303    /// Checks that all parameters fall within the valid ranges
304    /// specified in the Equation Model Card.
305    fn validate_against_emc(&self, emc: &EquationModelCard) -> ValidationResult;
306
307    /// Extract parameters as a hashmap for evaluation.
308    fn parameters(&self) -> HashMap<String, f64>;
309}
310
311/// Core EDD trait bundle.
312///
313/// Every simulation in simular MUST implement this trait, which combines:
314/// - `GoverningEquation`: Mathematical foundation
315/// - `FalsifiableSimulation`: Active falsification search
316/// - `Reproducible`: Deterministic seeding
317/// - `YamlConfigurable`: Declarative configuration
318///
319/// # EDD Compliance
320///
321/// Implementing this trait ensures the simulation complies with all
322/// four pillars of EDD:
323///
324/// 1. **Prove It**: Via `GoverningEquation` (EMC reference)
325/// 2. **Fail It**: Via `FalsifiableSimulation` (falsification criteria)
326/// 3. **Seed It**: Via `Reproducible` (deterministic RNG)
327/// 4. **Falsify It**: Via `verify_against_emc()` (active testing)
328///
329/// # Example
330///
331/// ```ignore
332/// pub struct HarmonicOscillator {
333///     omega: f64,
334///     emc: EquationModelCard,
335///     seed: u64,
336/// }
337///
338/// impl EddSimulation for HarmonicOscillator {
339///     fn emc(&self) -> &EquationModelCard {
340///         &self.emc
341///     }
342///
343///     fn verify_against_emc(&self) -> VerificationResult {
344///         // Run EMC verification tests
345///     }
346/// }
347/// ```
348pub trait EddSimulation:
349    GoverningEquation + FalsifiableSimulation + Reproducible + YamlConfigurable
350{
351    /// Get the associated Equation Model Card.
352    fn emc(&self) -> &EquationModelCard;
353
354    /// Verify implementation against EMC test cases.
355    ///
356    /// Runs all verification tests defined in the EMC and returns
357    /// a detailed result showing which tests passed or failed.
358    fn verify_against_emc(&self) -> VerificationResult;
359
360    /// Get simulation name from EMC.
361    fn simulation_name(&self) -> &str {
362        &self.emc().name
363    }
364
365    /// Get simulation version from EMC.
366    fn simulation_version(&self) -> &str {
367        &self.emc().version
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    #[test]
376    fn test_config_error_new() {
377        let err = ConfigError::new("test error");
378        assert_eq!(err.message, "test error");
379        assert!(err.field.is_none());
380        assert!(err.cause.is_none());
381    }
382
383    #[test]
384    fn test_config_error_field() {
385        let err = ConfigError::field_error("omega", "must be positive");
386        assert_eq!(err.field, Some("omega".to_string()));
387        assert!(err.message.contains("positive"));
388    }
389
390    #[test]
391    fn test_config_error_with_cause() {
392        let err = ConfigError::new("parse failed").with_cause("invalid number");
393        assert!(err.cause.is_some());
394        assert!(err.cause.unwrap().contains("invalid"));
395    }
396
397    #[test]
398    fn test_config_error_display() {
399        let err = ConfigError::field_error("seed", "required").with_cause("missing key");
400        let display = format!("{err}");
401        assert!(display.contains("seed"));
402        assert!(display.contains("required"));
403        assert!(display.contains("missing key"));
404    }
405
406    #[test]
407    fn test_validation_result_success() {
408        let result = ValidationResult::success(vec!["omega".to_string(), "amplitude".to_string()]);
409        assert!(result.valid);
410        assert!(result.errors.is_empty());
411        assert_eq!(result.validated_params.len(), 2);
412    }
413
414    #[test]
415    fn test_validation_result_failure() {
416        let result = ValidationResult::failure(vec!["omega out of range".to_string()]);
417        assert!(!result.valid);
418        assert_eq!(result.errors.len(), 1);
419    }
420
421    #[test]
422    fn test_validation_result_with_warning() {
423        let result = ValidationResult::success(vec![]).with_warning("deprecated parameter");
424        assert!(result.valid);
425        assert_eq!(result.warnings.len(), 1);
426    }
427
428    #[test]
429    fn test_verification_result_from_tests() {
430        let tests = vec![
431            TestResult::pass("test1", 10.0, 10.0, 0.01),
432            TestResult::fail("test2", 5.0, 6.0, 0.01),
433        ];
434        let result = VerificationResult::from_tests(tests);
435        assert!(!result.passed);
436        assert_eq!(result.total_tests, 2);
437        assert_eq!(result.passed_tests, 1);
438        assert_eq!(result.failed_tests, 1);
439    }
440
441    #[test]
442    fn test_verification_result_empty() {
443        let result = VerificationResult::empty();
444        assert!(result.passed);
445        assert_eq!(result.total_tests, 0);
446    }
447
448    #[test]
449    fn test_test_result_pass() {
450        let result = TestResult::pass("energy_conservation", 100.0, 100.001, 0.01);
451        assert!(result.passed);
452        assert!(result.message.contains("PASSED"));
453    }
454
455    #[test]
456    fn test_test_result_fail() {
457        let result = TestResult::fail("energy_conservation", 100.0, 110.0, 0.01);
458        assert!(!result.passed);
459        assert!(result.message.contains("FAILED"));
460    }
461}