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}