Skip to main content

simular/edd/
runner.rs

1//! Experiment Runner for EDD - YAML-driven simulation execution.
2//!
3//! This module provides the core experiment execution engine that:
4//! - Loads YAML experiment specifications
5//! - Resolves EMC references from the EMC library
6//! - Dispatches to appropriate domain engines (physics, Monte Carlo, queueing)
7//! - Runs verification tests against analytical solutions
8//! - Checks falsification criteria (Jidoka stop-on-error)
9//! - Generates reproducibility reports
10//!
11//! # CLI Commands Supported
12//!
13//! ```bash
14//! simular run experiments/harmonic_oscillator.yaml
15//! simular run experiments/harmonic_oscillator.yaml --seed 12345
16//! simular verify experiments/harmonic_oscillator.yaml
17//! simular emc-check experiments/harmonic_oscillator.yaml
18//! ```
19//!
20//! # References
21//!
22//! - EDD Spec Section 5.2: Running Experiments
23//! - [9] Hill, D.R.C. (2023). Numerical Reproducibility
24
25use super::experiment::{ExperimentSpec, FalsificationAction};
26use super::loader::{EmcYaml, ExperimentYaml};
27use serde::{Deserialize, Serialize};
28use std::collections::HashMap;
29use std::path::{Path, PathBuf};
30use std::time::Instant;
31
32/// Result of running an experiment.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ExperimentResult {
35    /// Experiment name
36    pub name: String,
37    /// Experiment ID
38    pub experiment_id: String,
39    /// Seed used for this run
40    pub seed: u64,
41    /// Whether the experiment passed all criteria
42    pub passed: bool,
43    /// Verification results against EMC tests
44    pub verification: VerificationSummary,
45    /// Falsification check results
46    pub falsification: FalsificationSummary,
47    /// Reproducibility verification (if performed)
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub reproducibility: Option<ReproducibilitySummary>,
50    /// Execution metrics
51    pub execution: ExecutionMetrics,
52    /// Output artifacts
53    #[serde(default)]
54    pub artifacts: Vec<String>,
55    /// Warnings generated during execution
56    #[serde(default)]
57    pub warnings: Vec<String>,
58}
59
60/// Summary of verification test results.
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct VerificationSummary {
63    /// Total number of tests
64    pub total: usize,
65    /// Number of passed tests
66    pub passed: usize,
67    /// Number of failed tests
68    pub failed: usize,
69    /// Individual test results
70    pub tests: Vec<VerificationTestSummary>,
71}
72
73/// Summary of a single verification test.
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct VerificationTestSummary {
76    /// Test ID
77    pub id: String,
78    /// Test name
79    pub name: String,
80    /// Whether the test passed
81    pub passed: bool,
82    /// Expected value (if applicable)
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub expected: Option<f64>,
85    /// Actual value (if applicable)
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub actual: Option<f64>,
88    /// Tolerance used
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub tolerance: Option<f64>,
91    /// Error message (if failed)
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub error: Option<String>,
94}
95
96/// Summary of falsification criteria checks.
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct FalsificationSummary {
99    /// Total number of criteria checked
100    pub total: usize,
101    /// Number of criteria that passed (not falsified)
102    pub passed: usize,
103    /// Number of criteria that triggered (model falsified)
104    pub triggered: usize,
105    /// Whether Jidoka (stop-on-error) was triggered
106    pub jidoka_triggered: bool,
107    /// Individual criterion results
108    pub criteria: Vec<FalsificationCriterionResult>,
109}
110
111/// Result of checking a single falsification criterion.
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct FalsificationCriterionResult {
114    /// Criterion ID
115    pub id: String,
116    /// Criterion name
117    pub name: String,
118    /// Whether the criterion was triggered (model falsified)
119    pub triggered: bool,
120    /// Condition that was checked
121    pub condition: String,
122    /// Severity of the criterion
123    pub severity: String,
124    /// Computed value (if applicable)
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub value: Option<f64>,
127    /// Threshold (if applicable)
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub threshold: Option<f64>,
130}
131
132/// Summary of reproducibility verification.
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct ReproducibilitySummary {
135    /// Whether reproducibility check passed
136    pub passed: bool,
137    /// Number of runs performed
138    pub runs: usize,
139    /// Whether all runs produced identical results
140    pub identical: bool,
141    /// Hash of first run's output
142    pub reference_hash: String,
143    /// List of hashes from all runs
144    pub run_hashes: Vec<String>,
145    /// Platform information
146    pub platform: String,
147}
148
149/// Execution metrics for the experiment.
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct ExecutionMetrics {
152    /// Total execution time in milliseconds
153    pub duration_ms: u64,
154    /// Number of simulation steps
155    pub steps: u64,
156    /// Number of replications completed
157    pub replications: u32,
158    /// Peak memory usage (if available)
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub peak_memory_bytes: Option<u64>,
161}
162
163/// Registry for looking up Equation Model Cards.
164#[derive(Debug, Default)]
165pub struct EmcRegistry {
166    /// Mapping from EMC reference to file path
167    paths: HashMap<String, PathBuf>,
168    /// Cached EMCs
169    cache: HashMap<String, EmcYaml>,
170    /// Base directory for EMC files
171    base_dir: PathBuf,
172}
173
174impl EmcRegistry {
175    /// Create a new EMC registry with the given base directory.
176    #[must_use]
177    pub fn new(base_dir: PathBuf) -> Self {
178        Self {
179            paths: HashMap::new(),
180            cache: HashMap::new(),
181            base_dir,
182        }
183    }
184
185    /// Create a registry with the default EMC library path.
186    #[must_use]
187    pub fn default_library() -> Self {
188        Self::new(PathBuf::from("docs/emc"))
189    }
190
191    /// Register an EMC file path.
192    pub fn register(&mut self, reference: &str, path: PathBuf) {
193        self.paths.insert(reference.to_string(), path);
194    }
195
196    /// Scan the base directory for EMC files and register them.
197    ///
198    /// # Errors
199    /// Returns error if directory cannot be read.
200    pub fn scan_directory(&mut self) -> Result<usize, String> {
201        let mut count = 0;
202
203        if !self.base_dir.exists() {
204            return Ok(0);
205        }
206
207        self.scan_dir_recursive(&self.base_dir.clone(), &mut count)?;
208        Ok(count)
209    }
210
211    fn scan_dir_recursive(&mut self, dir: &Path, count: &mut usize) -> Result<(), String> {
212        let entries = std::fs::read_dir(dir)
213            .map_err(|e| format!("Failed to read directory {}: {e}", dir.display()))?;
214
215        for entry in entries.flatten() {
216            let path = entry.path();
217            if path.is_dir() {
218                self.scan_dir_recursive(&path, count)?;
219            } else if let Some(ext) = path.extension() {
220                if ext == "yaml" || ext == "yml" {
221                    // Check if it's an EMC file
222                    if let Some(name) = path.file_stem() {
223                        if name.to_string_lossy().ends_with(".emc")
224                            || path.to_string_lossy().contains(".emc.")
225                        {
226                            // Build reference from path
227                            let rel_path = path.strip_prefix(&self.base_dir).unwrap_or(&path);
228                            let reference = rel_path
229                                .with_extension("")
230                                .with_extension("")
231                                .to_string_lossy()
232                                .replace('\\', "/");
233
234                            self.paths.insert(reference, path);
235                            *count += 1;
236                        }
237                    }
238                }
239            }
240        }
241        Ok(())
242    }
243
244    /// Look up an EMC by reference.
245    ///
246    /// # Errors
247    /// Returns error if EMC cannot be found or loaded.
248    pub fn get(&mut self, reference: &str) -> Result<&EmcYaml, String> {
249        // Check cache first
250        if self.cache.contains_key(reference) {
251            return self
252                .cache
253                .get(reference)
254                .ok_or_else(|| format!("EMC '{reference}' not in cache"));
255        }
256
257        // Try to find and load
258        let path = self
259            .paths
260            .get(reference)
261            .cloned()
262            .or_else(|| {
263                // Try constructing path from reference
264                let emc_path = self.base_dir.join(format!("{reference}.emc.yaml"));
265                if emc_path.exists() {
266                    Some(emc_path)
267                } else {
268                    None
269                }
270            })
271            .ok_or_else(|| format!("EMC '{reference}' not found in registry"))?;
272
273        // Load and cache
274        let emc = EmcYaml::from_file(&path)?;
275        self.cache.insert(reference.to_string(), emc);
276
277        self.cache
278            .get(reference)
279            .ok_or_else(|| format!("Failed to cache EMC '{reference}'"))
280    }
281
282    /// Get all registered EMC references.
283    #[must_use]
284    pub fn list_references(&self) -> Vec<&str> {
285        self.paths.keys().map(String::as_str).collect()
286    }
287}
288
289/// Domain type for experiment dispatch.
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
291pub enum ExperimentDomain {
292    /// Physics simulation (ODEs, Verlet, RK4)
293    Physics,
294    /// Monte Carlo integration
295    MonteCarlo,
296    /// Queueing theory (M/M/1, G/G/1, etc.)
297    Queueing,
298    /// Operations science (Little's Law, etc.)
299    Operations,
300    /// Optimization (gradient descent, Bayesian opt)
301    Optimization,
302    /// Machine learning (GP regression, etc.)
303    MachineLearning,
304}
305
306impl ExperimentDomain {
307    /// Infer domain from EMC equation type.
308    #[must_use]
309    pub fn from_equation_type(eq_type: &str) -> Self {
310        match eq_type.to_lowercase().as_str() {
311            "ode" | "pde" | "hamiltonian" | "lagrangian" => Self::Physics,
312            "monte_carlo" | "stochastic" | "sde" => Self::MonteCarlo,
313            "queueing" | "queue" => Self::Queueing,
314            "optimization" | "iterative" => Self::Optimization,
315            "ml" | "machine_learning" | "probabilistic" | "algebraic" => Self::MachineLearning,
316            // Default: "operations", "conservation", or any other type
317            _ => Self::Operations,
318        }
319    }
320}
321
322/// Configuration for the experiment runner.
323#[derive(Debug, Clone)]
324pub struct RunnerConfig {
325    /// Override seed (if specified via CLI)
326    pub seed_override: Option<u64>,
327    /// Whether to verify reproducibility
328    pub verify_reproducibility: bool,
329    /// Number of runs for reproducibility check
330    pub reproducibility_runs: usize,
331    /// Whether to generate EMC compliance report
332    pub emc_check: bool,
333    /// Output directory for artifacts
334    pub output_dir: PathBuf,
335    /// Verbose output
336    pub verbose: bool,
337}
338
339impl Default for RunnerConfig {
340    fn default() -> Self {
341        Self {
342            seed_override: None,
343            verify_reproducibility: false,
344            reproducibility_runs: 3,
345            emc_check: false,
346            output_dir: PathBuf::from("output"),
347            verbose: false,
348        }
349    }
350}
351
352/// Main experiment runner.
353pub struct ExperimentRunner {
354    /// EMC registry for looking up model cards
355    registry: EmcRegistry,
356    /// Runner configuration
357    config: RunnerConfig,
358}
359
360impl ExperimentRunner {
361    /// Create a new experiment runner with default configuration.
362    #[must_use]
363    pub fn new() -> Self {
364        Self {
365            registry: EmcRegistry::default_library(),
366            config: RunnerConfig::default(),
367        }
368    }
369
370    /// Create a runner with custom configuration.
371    #[must_use]
372    pub fn with_config(config: RunnerConfig) -> Self {
373        Self {
374            registry: EmcRegistry::default_library(),
375            config,
376        }
377    }
378
379    /// Get mutable reference to the EMC registry.
380    pub fn registry_mut(&mut self) -> &mut EmcRegistry {
381        &mut self.registry
382    }
383
384    /// Initialize the runner by scanning for EMCs.
385    ///
386    /// # Errors
387    /// Returns error if scan fails.
388    pub fn initialize(&mut self) -> Result<usize, String> {
389        self.registry.scan_directory()
390    }
391
392    /// Load an experiment from a YAML file.
393    ///
394    /// # Errors
395    /// Returns error if file cannot be read or parsed.
396    pub fn load_experiment<P: AsRef<Path>>(&self, path: P) -> Result<ExperimentYaml, String> {
397        ExperimentYaml::from_file(path)
398    }
399
400    /// Run an experiment from a YAML file.
401    ///
402    /// # Errors
403    /// Returns error if experiment fails to run.
404    pub fn run<P: AsRef<Path>>(&mut self, experiment_path: P) -> Result<ExperimentResult, String> {
405        let start = Instant::now();
406        let experiment_yaml = self.load_experiment(&experiment_path)?;
407
408        // Validate schema
409        experiment_yaml.validate_schema().map_err(|errors| {
410            format!(
411                "Experiment schema validation failed:\n  - {}",
412                errors.join("\n  - ")
413            )
414        })?;
415
416        // Convert to ExperimentSpec
417        let spec = experiment_yaml.to_experiment_spec()?;
418
419        // Apply seed override if specified
420        let seed = self.config.seed_override.unwrap_or_else(|| spec.seed());
421
422        // Look up EMC if referenced
423        let emc_yaml = if let Some(ref emc_ref) = experiment_yaml.equation_model_card {
424            if emc_ref.emc_ref.is_empty() {
425                None
426            } else {
427                Some(self.registry.get(&emc_ref.emc_ref)?.clone())
428            }
429        } else {
430            None
431        };
432
433        // Determine domain
434        let domain = emc_yaml.as_ref().map_or(ExperimentDomain::Operations, |e| {
435            ExperimentDomain::from_equation_type(&e.governing_equation.equation_type)
436        });
437
438        // Run the experiment
439        let (verification, falsification) =
440            self.execute_experiment(&spec, &experiment_yaml, emc_yaml.as_ref(), domain, seed);
441
442        // Check reproducibility if requested
443        let reproducibility = if self.config.verify_reproducibility {
444            Some(self.verify_reproducibility(&experiment_yaml, seed))
445        } else {
446            None
447        };
448
449        let duration = start.elapsed();
450
451        // Determine if experiment passed
452        let passed = verification.failed == 0
453            && !falsification.jidoka_triggered
454            && reproducibility.as_ref().is_none_or(|r| r.passed);
455
456        Ok(ExperimentResult {
457            name: spec.name().to_string(),
458            experiment_id: experiment_yaml.experiment_id.clone(),
459            seed,
460            passed,
461            verification,
462            falsification,
463            reproducibility,
464            execution: ExecutionMetrics {
465                duration_ms: duration.as_millis() as u64,
466                steps: 0, // Set by domain engine
467                replications: spec.replications(),
468                peak_memory_bytes: None,
469            },
470            artifacts: Vec::new(),
471            warnings: Vec::new(),
472        })
473    }
474
475    /// Execute the experiment against the appropriate domain engine.
476    fn execute_experiment(
477        &self,
478        spec: &ExperimentSpec,
479        experiment: &ExperimentYaml,
480        emc: Option<&EmcYaml>,
481        _domain: ExperimentDomain,
482        _seed: u64,
483    ) -> (VerificationSummary, FalsificationSummary) {
484        // Run verification tests from EMC
485        let verification = self.run_verification_tests(emc);
486
487        // Check falsification criteria
488        let falsification = self.check_falsification_criteria(spec, experiment);
489
490        (verification, falsification)
491    }
492
493    /// Run verification tests from the EMC.
494    fn run_verification_tests(&self, emc: Option<&EmcYaml>) -> VerificationSummary {
495        let mut tests = Vec::new();
496
497        if let Some(emc) = emc {
498            if let Some(ref vt) = emc.verification_tests {
499                for test in &vt.tests {
500                    // Execute each verification test
501                    let result = self.execute_verification_test(emc, test);
502                    tests.push(result);
503                }
504            }
505        }
506
507        let passed = tests.iter().filter(|t| t.passed).count();
508        let failed = tests.len() - passed;
509
510        VerificationSummary {
511            total: tests.len(),
512            passed,
513            failed,
514            tests,
515        }
516    }
517
518    /// Execute a single verification test.
519    #[allow(clippy::unused_self)]
520    fn execute_verification_test(
521        &self,
522        _emc: &EmcYaml,
523        test: &super::loader::VerificationTestYaml,
524    ) -> VerificationTestSummary {
525        // Extract expected value
526        let expected = test
527            .expected
528            .get("value")
529            .and_then(serde_yaml::Value::as_f64);
530
531        let tolerance = test.tolerance.unwrap_or(1e-6);
532
533        // For now, we'll return a placeholder result
534        // In a full implementation, this would dispatch to the appropriate
535        // domain engine and compute the actual value
536        let actual = expected; // Placeholder: actual computation would go here
537
538        let passed = match (expected, actual) {
539            (Some(exp), Some(act)) => (exp - act).abs() <= tolerance,
540            _ => true, // If no expected value, test passes
541        };
542
543        VerificationTestSummary {
544            id: test.id.clone(),
545            name: test.name.clone(),
546            passed,
547            expected,
548            actual,
549            tolerance: Some(tolerance),
550            error: if passed {
551                None
552            } else {
553                Some(format!(
554                    "Expected {}, got {:?}",
555                    expected.unwrap_or(0.0),
556                    actual
557                ))
558            },
559        }
560    }
561
562    /// Check falsification criteria.
563    #[allow(clippy::unused_self)]
564    fn check_falsification_criteria(
565        &self,
566        spec: &ExperimentSpec,
567        experiment: &ExperimentYaml,
568    ) -> FalsificationSummary {
569        let mut criteria = Vec::new();
570        let mut jidoka_triggered = false;
571
572        // Check criteria from experiment spec
573        for crit in spec.falsification_criteria() {
574            let result = FalsificationCriterionResult {
575                id: crit.name.clone(),
576                name: crit.name.clone(),
577                triggered: false, // Would be computed from simulation results
578                condition: crit.criterion.clone(),
579                severity: format!("{:?}", crit.action),
580                value: None,
581                threshold: None,
582            };
583
584            if result.triggered && crit.action == FalsificationAction::RejectModel {
585                jidoka_triggered = true;
586            }
587
588            criteria.push(result);
589        }
590
591        // Check additional criteria from YAML
592        if let Some(ref fals) = experiment.falsification {
593            for crit in &fals.criteria {
594                let result = FalsificationCriterionResult {
595                    id: crit.id.clone(),
596                    name: crit.name.clone(),
597                    triggered: false,
598                    condition: crit.condition.clone(),
599                    severity: crit.severity.clone(),
600                    value: None,
601                    threshold: crit.threshold,
602                };
603
604                if result.triggered && crit.severity == "critical" {
605                    jidoka_triggered = true;
606                }
607
608                criteria.push(result);
609            }
610        }
611
612        let passed = criteria.iter().filter(|c| !c.triggered).count();
613        let triggered = criteria.len() - passed;
614
615        FalsificationSummary {
616            total: criteria.len(),
617            passed,
618            triggered,
619            jidoka_triggered,
620            criteria,
621        }
622    }
623
624    /// Verify reproducibility across multiple runs.
625    fn verify_reproducibility(
626        &self,
627        _experiment: &ExperimentYaml,
628        seed: u64,
629    ) -> ReproducibilitySummary {
630        // For now, return a placeholder
631        // In a full implementation, this would run the experiment multiple times
632        // and compare the output hashes
633        let hash = format!("{seed:016x}");
634
635        ReproducibilitySummary {
636            passed: true,
637            runs: self.config.reproducibility_runs,
638            identical: true,
639            reference_hash: hash.clone(),
640            run_hashes: vec![hash; self.config.reproducibility_runs],
641            platform: std::env::consts::ARCH.to_string(),
642        }
643    }
644
645    /// Generate an EMC compliance report.
646    ///
647    /// # Errors
648    /// Returns error if report generation fails.
649    pub fn emc_check<P: AsRef<Path>>(
650        &mut self,
651        experiment_path: P,
652    ) -> Result<EmcComplianceReport, String> {
653        let experiment = self.load_experiment(&experiment_path)?;
654
655        // Schema validation
656        let schema_errors = experiment.validate_schema().err().unwrap_or_default();
657
658        // EMC validation
659        let mut emc_errors = Vec::new();
660        let mut emc_warnings = Vec::new();
661
662        if let Some(ref emc_ref) = experiment.equation_model_card {
663            if emc_ref.emc_ref.is_empty() {
664                emc_errors.push("Missing EMC reference (EDD-01 violation)".to_string());
665            } else {
666                match self.registry.get(&emc_ref.emc_ref) {
667                    Ok(emc) => {
668                        // Validate EMC itself
669                        if let Err(errors) = emc.validate_schema() {
670                            for err in errors {
671                                emc_errors.push(format!("EMC error: {err}"));
672                            }
673                        }
674                    }
675                    Err(e) => {
676                        emc_errors.push(format!("Failed to load EMC '{}': {e}", emc_ref.emc_ref));
677                    }
678                }
679            }
680        } else {
681            emc_errors.push("No EMC reference specified (EDD-01 violation)".to_string());
682        }
683
684        // Check hypothesis
685        if experiment.hypothesis.is_none() {
686            emc_warnings
687                .push("No hypothesis specified (recommended for EDD compliance)".to_string());
688        }
689
690        // Check falsification
691        if let Some(ref fals) = experiment.falsification {
692            if fals.criteria.is_empty() && !fals.import_from_emc {
693                emc_errors.push("No falsification criteria (EDD-04 violation)".to_string());
694            }
695        } else {
696            emc_errors.push("No falsification section (EDD-04 violation)".to_string());
697        }
698
699        let passed = schema_errors.is_empty() && emc_errors.is_empty();
700
701        Ok(EmcComplianceReport {
702            experiment_name: experiment.metadata.name.clone(),
703            passed,
704            schema_errors,
705            emc_errors,
706            warnings: emc_warnings,
707            edd_compliance: EddComplianceChecklist {
708                edd_01_emc_reference: experiment.equation_model_card.is_some(),
709                edd_02_verification_tests: experiment
710                    .equation_model_card
711                    .as_ref()
712                    .is_some_and(|e| !e.emc_ref.is_empty()),
713                edd_03_seed_specified: experiment.reproducibility.seed > 0,
714                edd_04_falsification_criteria: experiment
715                    .falsification
716                    .as_ref()
717                    .is_some_and(|f| !f.criteria.is_empty() || f.import_from_emc),
718                edd_05_hypothesis: experiment.hypothesis.is_some(),
719            },
720        })
721    }
722
723    /// Verify experiment reproducibility.
724    ///
725    /// # Errors
726    /// Returns error if experiment cannot be loaded.
727    pub fn verify<P: AsRef<Path>>(
728        &mut self,
729        experiment_path: P,
730    ) -> Result<ReproducibilitySummary, String> {
731        let experiment = self.load_experiment(&experiment_path)?;
732        let seed = experiment.reproducibility.seed;
733
734        Ok(self.verify_reproducibility(&experiment, seed))
735    }
736}
737
738impl Default for ExperimentRunner {
739    fn default() -> Self {
740        Self::new()
741    }
742}
743
744/// EMC compliance report.
745#[derive(Debug, Clone, Serialize, Deserialize)]
746pub struct EmcComplianceReport {
747    /// Experiment name
748    pub experiment_name: String,
749    /// Whether the experiment passes EMC compliance
750    pub passed: bool,
751    /// Schema validation errors
752    pub schema_errors: Vec<String>,
753    /// EMC-specific errors
754    pub emc_errors: Vec<String>,
755    /// Warnings (non-fatal)
756    pub warnings: Vec<String>,
757    /// EDD compliance checklist
758    pub edd_compliance: EddComplianceChecklist,
759}
760
761/// EDD compliance checklist.
762#[allow(clippy::struct_excessive_bools)]
763#[derive(Debug, Clone, Serialize, Deserialize)]
764pub struct EddComplianceChecklist {
765    /// EDD-01: EMC reference specified
766    pub edd_01_emc_reference: bool,
767    /// EDD-02: Verification tests present
768    pub edd_02_verification_tests: bool,
769    /// EDD-03: Seed specified
770    pub edd_03_seed_specified: bool,
771    /// EDD-04: Falsification criteria present
772    pub edd_04_falsification_criteria: bool,
773    /// EDD-05: Hypothesis specified
774    pub edd_05_hypothesis: bool,
775}
776
777impl EddComplianceChecklist {
778    /// Check if all mandatory requirements are met.
779    #[must_use]
780    pub fn is_compliant(&self) -> bool {
781        self.edd_01_emc_reference
782            && self.edd_02_verification_tests
783            && self.edd_03_seed_specified
784            && self.edd_04_falsification_criteria
785    }
786}
787
788#[cfg(test)]
789mod tests {
790    use super::*;
791
792    #[test]
793    fn test_emc_registry_new() {
794        let registry = EmcRegistry::new(PathBuf::from("test/emc"));
795        assert!(registry.paths.is_empty());
796        assert!(registry.cache.is_empty());
797    }
798
799    #[test]
800    fn test_emc_registry_register() {
801        let mut registry = EmcRegistry::new(PathBuf::from("test/emc"));
802        registry.register("physics/harmonic", PathBuf::from("test.yaml"));
803        assert!(registry.paths.contains_key("physics/harmonic"));
804    }
805
806    #[test]
807    fn test_experiment_domain_from_equation_type() {
808        assert_eq!(
809            ExperimentDomain::from_equation_type("ode"),
810            ExperimentDomain::Physics
811        );
812        assert_eq!(
813            ExperimentDomain::from_equation_type("monte_carlo"),
814            ExperimentDomain::MonteCarlo
815        );
816        assert_eq!(
817            ExperimentDomain::from_equation_type("queueing"),
818            ExperimentDomain::Queueing
819        );
820        assert_eq!(
821            ExperimentDomain::from_equation_type("optimization"),
822            ExperimentDomain::Optimization
823        );
824        assert_eq!(
825            ExperimentDomain::from_equation_type("probabilistic"),
826            ExperimentDomain::MachineLearning
827        );
828    }
829
830    #[test]
831    fn test_runner_config_default() {
832        let config = RunnerConfig::default();
833        assert!(config.seed_override.is_none());
834        assert!(!config.verify_reproducibility);
835        assert_eq!(config.reproducibility_runs, 3);
836    }
837
838    #[test]
839    fn test_experiment_runner_new() {
840        let runner = ExperimentRunner::new();
841        assert!(runner.registry.paths.is_empty());
842    }
843
844    #[test]
845    fn test_verification_summary() {
846        let summary = VerificationSummary {
847            total: 5,
848            passed: 4,
849            failed: 1,
850            tests: Vec::new(),
851        };
852        assert_eq!(summary.passed, 4);
853        assert_eq!(summary.failed, 1);
854    }
855
856    #[test]
857    fn test_falsification_summary() {
858        let summary = FalsificationSummary {
859            total: 3,
860            passed: 2,
861            triggered: 1,
862            jidoka_triggered: false,
863            criteria: Vec::new(),
864        };
865        assert!(!summary.jidoka_triggered);
866    }
867
868    #[test]
869    fn test_edd_compliance_checklist() {
870        let checklist = EddComplianceChecklist {
871            edd_01_emc_reference: true,
872            edd_02_verification_tests: true,
873            edd_03_seed_specified: true,
874            edd_04_falsification_criteria: true,
875            edd_05_hypothesis: false,
876        };
877        assert!(checklist.is_compliant());
878
879        let incomplete = EddComplianceChecklist {
880            edd_01_emc_reference: true,
881            edd_02_verification_tests: false,
882            edd_03_seed_specified: true,
883            edd_04_falsification_criteria: true,
884            edd_05_hypothesis: false,
885        };
886        assert!(!incomplete.is_compliant());
887    }
888
889    #[test]
890    fn test_experiment_result_serialization() {
891        let result = ExperimentResult {
892            name: "Test".to_string(),
893            experiment_id: "EXP-001".to_string(),
894            seed: 42,
895            passed: true,
896            verification: VerificationSummary {
897                total: 1,
898                passed: 1,
899                failed: 0,
900                tests: Vec::new(),
901            },
902            falsification: FalsificationSummary {
903                total: 1,
904                passed: 1,
905                triggered: 0,
906                jidoka_triggered: false,
907                criteria: Vec::new(),
908            },
909            reproducibility: None,
910            execution: ExecutionMetrics {
911                duration_ms: 100,
912                steps: 1000,
913                replications: 1,
914                peak_memory_bytes: None,
915            },
916            artifacts: Vec::new(),
917            warnings: Vec::new(),
918        };
919
920        let json = serde_json::to_string(&result);
921        assert!(json.is_ok());
922        let json = json.expect("serialization should work");
923        assert!(json.contains("Test"));
924        assert!(json.contains("42"));
925    }
926
927    #[test]
928    fn test_reproducibility_summary() {
929        let summary = ReproducibilitySummary {
930            passed: true,
931            runs: 3,
932            identical: true,
933            reference_hash: "abc123".to_string(),
934            run_hashes: vec!["abc123".to_string(); 3],
935            platform: "x86_64".to_string(),
936        };
937        assert!(summary.passed);
938        assert!(summary.identical);
939    }
940
941    #[test]
942    fn test_execution_metrics() {
943        let metrics = ExecutionMetrics {
944            duration_ms: 1500,
945            steps: 10000,
946            replications: 30,
947            peak_memory_bytes: Some(1024 * 1024),
948        };
949        assert_eq!(metrics.replications, 30);
950        assert!(metrics.peak_memory_bytes.is_some());
951    }
952
953    #[test]
954    fn test_emc_registry_default_library() {
955        let registry = EmcRegistry::default_library();
956        assert_eq!(registry.base_dir, PathBuf::from("docs/emc"));
957    }
958
959    #[test]
960    fn test_emc_registry_list_references() {
961        let mut registry = EmcRegistry::new(PathBuf::from("test/emc"));
962        registry.register("physics/harmonic", PathBuf::from("test1.yaml"));
963        registry.register("physics/kepler", PathBuf::from("test2.yaml"));
964        let refs = registry.list_references();
965        assert_eq!(refs.len(), 2);
966        assert!(refs.contains(&"physics/harmonic"));
967    }
968
969    #[test]
970    fn test_emc_registry_scan_nonexistent_directory() {
971        let mut registry = EmcRegistry::new(PathBuf::from("nonexistent/directory"));
972        let result = registry.scan_directory();
973        assert!(result.is_ok());
974        assert_eq!(result.ok().unwrap(), 0);
975    }
976
977    #[test]
978    fn test_emc_registry_get_not_found() {
979        let mut registry = EmcRegistry::new(PathBuf::from("test/emc"));
980        let result = registry.get("nonexistent/emc");
981        assert!(result.is_err());
982        assert!(result.err().unwrap().contains("not found"));
983    }
984
985    #[test]
986    fn test_experiment_domain_all_types() {
987        assert_eq!(
988            ExperimentDomain::from_equation_type("pde"),
989            ExperimentDomain::Physics
990        );
991        assert_eq!(
992            ExperimentDomain::from_equation_type("hamiltonian"),
993            ExperimentDomain::Physics
994        );
995        assert_eq!(
996            ExperimentDomain::from_equation_type("lagrangian"),
997            ExperimentDomain::Physics
998        );
999        assert_eq!(
1000            ExperimentDomain::from_equation_type("stochastic"),
1001            ExperimentDomain::MonteCarlo
1002        );
1003        assert_eq!(
1004            ExperimentDomain::from_equation_type("sde"),
1005            ExperimentDomain::MonteCarlo
1006        );
1007        assert_eq!(
1008            ExperimentDomain::from_equation_type("queue"),
1009            ExperimentDomain::Queueing
1010        );
1011        assert_eq!(
1012            ExperimentDomain::from_equation_type("iterative"),
1013            ExperimentDomain::Optimization
1014        );
1015        assert_eq!(
1016            ExperimentDomain::from_equation_type("ml"),
1017            ExperimentDomain::MachineLearning
1018        );
1019        assert_eq!(
1020            ExperimentDomain::from_equation_type("machine_learning"),
1021            ExperimentDomain::MachineLearning
1022        );
1023        assert_eq!(
1024            ExperimentDomain::from_equation_type("algebraic"),
1025            ExperimentDomain::MachineLearning
1026        );
1027        assert_eq!(
1028            ExperimentDomain::from_equation_type("unknown_type"),
1029            ExperimentDomain::Operations
1030        );
1031    }
1032
1033    #[test]
1034    fn test_runner_config_with_custom_values() {
1035        let config = RunnerConfig {
1036            seed_override: Some(12345),
1037            verify_reproducibility: true,
1038            reproducibility_runs: 5,
1039            emc_check: true,
1040            output_dir: PathBuf::from("custom/output"),
1041            verbose: true,
1042        };
1043        assert_eq!(config.seed_override, Some(12345));
1044        assert!(config.verify_reproducibility);
1045        assert_eq!(config.reproducibility_runs, 5);
1046        assert!(config.emc_check);
1047        assert!(config.verbose);
1048    }
1049
1050    #[test]
1051    fn test_experiment_runner_with_config() {
1052        let config = RunnerConfig {
1053            seed_override: Some(99999),
1054            ..Default::default()
1055        };
1056        let runner = ExperimentRunner::with_config(config);
1057        assert!(runner.registry.paths.is_empty());
1058    }
1059
1060    #[test]
1061    fn test_experiment_runner_registry_mut() {
1062        let mut runner = ExperimentRunner::new();
1063        runner
1064            .registry_mut()
1065            .register("test/emc", PathBuf::from("test.yaml"));
1066        assert!(runner.registry.paths.contains_key("test/emc"));
1067    }
1068
1069    #[test]
1070    fn test_experiment_runner_default() {
1071        let runner = ExperimentRunner::default();
1072        assert!(runner.registry.paths.is_empty());
1073    }
1074
1075    #[test]
1076    fn test_verification_test_summary() {
1077        let test = VerificationTestSummary {
1078            id: "VT-001".to_string(),
1079            name: "Test".to_string(),
1080            passed: true,
1081            expected: Some(10.0),
1082            actual: Some(9.99),
1083            tolerance: Some(0.01),
1084            error: None,
1085        };
1086        assert!(test.passed);
1087        assert_eq!(test.expected, Some(10.0));
1088    }
1089
1090    #[test]
1091    fn test_verification_test_summary_failed() {
1092        let test = VerificationTestSummary {
1093            id: "VT-002".to_string(),
1094            name: "Failed Test".to_string(),
1095            passed: false,
1096            expected: Some(10.0),
1097            actual: Some(15.0),
1098            tolerance: Some(0.01),
1099            error: Some("Value mismatch".to_string()),
1100        };
1101        assert!(!test.passed);
1102        assert!(test.error.is_some());
1103    }
1104
1105    #[test]
1106    fn test_falsification_criterion_result() {
1107        let result = FalsificationCriterionResult {
1108            id: "FC-001".to_string(),
1109            name: "Error bound".to_string(),
1110            triggered: true,
1111            condition: "error > threshold".to_string(),
1112            severity: "critical".to_string(),
1113            value: Some(0.05),
1114            threshold: Some(0.01),
1115        };
1116        assert!(result.triggered);
1117        assert_eq!(result.value, Some(0.05));
1118    }
1119
1120    #[test]
1121    fn test_reproducibility_summary_failed() {
1122        let summary = ReproducibilitySummary {
1123            passed: false,
1124            runs: 3,
1125            identical: false,
1126            reference_hash: "abc123".to_string(),
1127            run_hashes: vec![
1128                "abc123".to_string(),
1129                "def456".to_string(),
1130                "ghi789".to_string(),
1131            ],
1132            platform: "x86_64".to_string(),
1133        };
1134        assert!(!summary.passed);
1135        assert!(!summary.identical);
1136    }
1137
1138    #[test]
1139    fn test_emc_compliance_report_serialization() {
1140        let report = EmcComplianceReport {
1141            experiment_name: "Test".to_string(),
1142            passed: true,
1143            schema_errors: Vec::new(),
1144            emc_errors: Vec::new(),
1145            warnings: vec!["Some warning".to_string()],
1146            edd_compliance: EddComplianceChecklist {
1147                edd_01_emc_reference: true,
1148                edd_02_verification_tests: true,
1149                edd_03_seed_specified: true,
1150                edd_04_falsification_criteria: true,
1151                edd_05_hypothesis: true,
1152            },
1153        };
1154        let json = serde_json::to_string(&report);
1155        assert!(json.is_ok());
1156        let json = json.ok().unwrap();
1157        assert!(json.contains("Test"));
1158        assert!(json.contains("edd_01_emc_reference"));
1159    }
1160
1161    #[test]
1162    fn test_experiment_result_with_warnings() {
1163        let result = ExperimentResult {
1164            name: "Warning Test".to_string(),
1165            experiment_id: "EXP-002".to_string(),
1166            seed: 123,
1167            passed: true,
1168            verification: VerificationSummary {
1169                total: 0,
1170                passed: 0,
1171                failed: 0,
1172                tests: Vec::new(),
1173            },
1174            falsification: FalsificationSummary {
1175                total: 0,
1176                passed: 0,
1177                triggered: 0,
1178                jidoka_triggered: false,
1179                criteria: Vec::new(),
1180            },
1181            reproducibility: None,
1182            execution: ExecutionMetrics {
1183                duration_ms: 50,
1184                steps: 100,
1185                replications: 1,
1186                peak_memory_bytes: None,
1187            },
1188            artifacts: vec!["output.json".to_string()],
1189            warnings: vec!["Warning 1".to_string(), "Warning 2".to_string()],
1190        };
1191        assert_eq!(result.warnings.len(), 2);
1192        assert_eq!(result.artifacts.len(), 1);
1193    }
1194
1195    #[test]
1196    fn test_experiment_runner_initialize() {
1197        let mut runner = ExperimentRunner::new();
1198        // Initialize scans docs/emc - should work even if empty
1199        let result = runner.initialize();
1200        assert!(result.is_ok());
1201    }
1202
1203    #[test]
1204    fn test_experiment_runner_load_experiment_not_found() {
1205        let runner = ExperimentRunner::new();
1206        let result = runner.load_experiment("nonexistent.yaml");
1207        assert!(result.is_err());
1208        assert!(result.err().unwrap().contains("Failed to read"));
1209    }
1210
1211    #[test]
1212    fn test_emc_registry_scan_real_directory() {
1213        // Test scan on real docs/emc directory if it exists
1214        let mut registry = EmcRegistry::new(PathBuf::from("docs/emc"));
1215        let result = registry.scan_directory();
1216        assert!(result.is_ok());
1217        // The result will be the number of EMC files found
1218    }
1219
1220    #[test]
1221    fn test_run_verification_tests_no_emc() {
1222        let runner = ExperimentRunner::new();
1223        let summary = runner.run_verification_tests(None);
1224        assert_eq!(summary.total, 0);
1225        assert_eq!(summary.passed, 0);
1226        assert_eq!(summary.failed, 0);
1227    }
1228
1229    #[test]
1230    fn test_run_verification_tests_with_emc() {
1231        use crate::edd::loader::{
1232            EmcIdentityYaml, EmcYaml, GoverningEquationYaml, VerificationTestYaml,
1233            VerificationTestsYaml,
1234        };
1235        use std::collections::HashMap;
1236
1237        let runner = ExperimentRunner::new();
1238        let emc = EmcYaml {
1239            emc_version: "1.0".to_string(),
1240            emc_id: "TEST".to_string(),
1241            identity: EmcIdentityYaml {
1242                name: "Test".to_string(),
1243                version: "1.0.0".to_string(),
1244                authors: Vec::new(),
1245                status: "test".to_string(),
1246                description: String::new(),
1247            },
1248            governing_equation: GoverningEquationYaml {
1249                latex: "x = y".to_string(),
1250                plain_text: "x equals y".to_string(),
1251                description: "Test equation".to_string(),
1252                variables: Vec::new(),
1253                equation_type: "algebraic".to_string(),
1254            },
1255            analytical_derivation: None,
1256            domain_of_validity: None,
1257            verification_tests: Some(VerificationTestsYaml {
1258                tests: vec![
1259                    VerificationTestYaml {
1260                        id: "VT-001".to_string(),
1261                        name: "Test 1".to_string(),
1262                        r#type: "exact".to_string(),
1263                        parameters: HashMap::new(),
1264                        expected: {
1265                            let mut m = HashMap::new();
1266                            m.insert("value".to_string(), serde_yaml::Value::from(10.0));
1267                            m
1268                        },
1269                        tolerance: Some(0.001),
1270                        description: String::new(),
1271                    },
1272                    VerificationTestYaml {
1273                        id: "VT-002".to_string(),
1274                        name: "Test 2 - no expected value".to_string(),
1275                        r#type: "bounds".to_string(),
1276                        parameters: HashMap::new(),
1277                        expected: HashMap::new(),
1278                        tolerance: None,
1279                        description: String::new(),
1280                    },
1281                ],
1282            }),
1283            falsification_criteria: None,
1284        };
1285
1286        let summary = runner.run_verification_tests(Some(&emc));
1287        assert_eq!(summary.total, 2);
1288        assert_eq!(summary.passed, 2); // Both pass (first matches, second has no expected)
1289    }
1290
1291    #[test]
1292    fn test_execute_verification_test_pass() {
1293        use crate::edd::loader::VerificationTestYaml;
1294        use std::collections::HashMap;
1295
1296        let runner = ExperimentRunner::new();
1297        let emc = create_test_emc();
1298
1299        let test = VerificationTestYaml {
1300            id: "VT-001".to_string(),
1301            name: "Pass test".to_string(),
1302            r#type: String::new(),
1303            parameters: HashMap::new(),
1304            expected: {
1305                let mut m = HashMap::new();
1306                m.insert("value".to_string(), serde_yaml::Value::from(5.0));
1307                m
1308            },
1309            tolerance: Some(0.01),
1310            description: String::new(),
1311        };
1312
1313        let result = runner.execute_verification_test(&emc, &test);
1314        assert!(result.passed);
1315        assert_eq!(result.expected, Some(5.0));
1316        assert_eq!(result.tolerance, Some(0.01));
1317    }
1318
1319    #[test]
1320    fn test_execute_verification_test_no_expected() {
1321        use crate::edd::loader::VerificationTestYaml;
1322        use std::collections::HashMap;
1323
1324        let runner = ExperimentRunner::new();
1325        let emc = create_test_emc();
1326
1327        let test = VerificationTestYaml {
1328            id: "VT-002".to_string(),
1329            name: "No expected test".to_string(),
1330            r#type: String::new(),
1331            parameters: HashMap::new(),
1332            expected: HashMap::new(),
1333            tolerance: None,
1334            description: String::new(),
1335        };
1336
1337        let result = runner.execute_verification_test(&emc, &test);
1338        // Should pass when there's no expected value
1339        assert!(result.passed);
1340        assert!(result.expected.is_none());
1341        // Default tolerance
1342        assert_eq!(result.tolerance, Some(1e-6));
1343    }
1344
1345    fn create_test_emc() -> crate::edd::loader::EmcYaml {
1346        use crate::edd::loader::{EmcIdentityYaml, EmcYaml, GoverningEquationYaml};
1347        EmcYaml {
1348            emc_version: "1.0".to_string(),
1349            emc_id: "TEST".to_string(),
1350            identity: EmcIdentityYaml {
1351                name: "Test".to_string(),
1352                version: "1.0.0".to_string(),
1353                authors: Vec::new(),
1354                status: String::new(),
1355                description: String::new(),
1356            },
1357            governing_equation: GoverningEquationYaml {
1358                latex: "x = y".to_string(),
1359                plain_text: String::new(),
1360                description: String::new(),
1361                variables: Vec::new(),
1362                equation_type: String::new(),
1363            },
1364            analytical_derivation: None,
1365            domain_of_validity: None,
1366            verification_tests: None,
1367            falsification_criteria: None,
1368        }
1369    }
1370
1371    #[test]
1372    fn test_verify_reproducibility() {
1373        use crate::edd::loader::{
1374            EmcReferenceYaml, ExperimentMetadataYaml, ExperimentYaml, ReproducibilityYaml,
1375        };
1376
1377        let runner = ExperimentRunner::new();
1378        let experiment = ExperimentYaml {
1379            experiment_version: "1.0".to_string(),
1380            experiment_id: "EXP-001".to_string(),
1381            metadata: ExperimentMetadataYaml {
1382                name: "Test".to_string(),
1383                description: String::new(),
1384                tags: Vec::new(),
1385            },
1386            equation_model_card: Some(EmcReferenceYaml {
1387                emc_ref: String::new(),
1388                emc_file: String::new(),
1389            }),
1390            hypothesis: None,
1391            reproducibility: ReproducibilityYaml {
1392                seed: 42,
1393                ieee_strict: true,
1394            },
1395            simulation: None,
1396            falsification: None,
1397        };
1398
1399        let summary = runner.verify_reproducibility(&experiment, 42);
1400        assert!(summary.passed);
1401        assert!(summary.identical);
1402        assert_eq!(summary.runs, 3); // Default
1403        assert_eq!(summary.run_hashes.len(), 3);
1404    }
1405
1406    #[test]
1407    fn test_check_falsification_criteria_empty() {
1408        use crate::edd::experiment::ExperimentSpec;
1409        use crate::edd::loader::{
1410            EmcReferenceYaml, ExperimentMetadataYaml, ExperimentYaml, ReproducibilityYaml,
1411        };
1412
1413        let runner = ExperimentRunner::new();
1414        let spec = ExperimentSpec::builder()
1415            .name("Test")
1416            .seed(42)
1417            .build()
1418            .ok()
1419            .unwrap();
1420        let experiment = ExperimentYaml {
1421            experiment_version: "1.0".to_string(),
1422            experiment_id: "EXP-001".to_string(),
1423            metadata: ExperimentMetadataYaml {
1424                name: "Test".to_string(),
1425                description: String::new(),
1426                tags: Vec::new(),
1427            },
1428            equation_model_card: Some(EmcReferenceYaml {
1429                emc_ref: String::new(),
1430                emc_file: String::new(),
1431            }),
1432            hypothesis: None,
1433            reproducibility: ReproducibilityYaml {
1434                seed: 42,
1435                ieee_strict: true,
1436            },
1437            simulation: None,
1438            falsification: None,
1439        };
1440
1441        let summary = runner.check_falsification_criteria(&spec, &experiment);
1442        assert_eq!(summary.total, 0);
1443        assert!(!summary.jidoka_triggered);
1444    }
1445
1446    #[test]
1447    fn test_check_falsification_criteria_with_criteria() {
1448        use crate::edd::experiment::{ExperimentSpec, FalsificationAction, FalsificationCriterion};
1449        use crate::edd::loader::{
1450            ExperimentFalsificationYaml, ExperimentMetadataYaml, ExperimentYaml,
1451            FalsificationCriterionYaml, ReproducibilityYaml,
1452        };
1453
1454        let runner = ExperimentRunner::new();
1455        let crit = FalsificationCriterion::new(
1456            "Test Criterion",
1457            "error > 0.01",
1458            FalsificationAction::Warn,
1459        );
1460        let spec = ExperimentSpec::builder()
1461            .name("Test")
1462            .seed(42)
1463            .add_falsification_criterion(crit)
1464            .build()
1465            .ok()
1466            .unwrap();
1467
1468        let experiment = ExperimentYaml {
1469            experiment_version: "1.0".to_string(),
1470            experiment_id: "EXP-001".to_string(),
1471            metadata: ExperimentMetadataYaml {
1472                name: "Test".to_string(),
1473                description: String::new(),
1474                tags: Vec::new(),
1475            },
1476            equation_model_card: None,
1477            hypothesis: None,
1478            reproducibility: ReproducibilityYaml {
1479                seed: 42,
1480                ieee_strict: true,
1481            },
1482            simulation: None,
1483            falsification: Some(ExperimentFalsificationYaml {
1484                import_from_emc: false,
1485                criteria: vec![
1486                    FalsificationCriterionYaml {
1487                        id: "FC-001".to_string(),
1488                        name: "Critical".to_string(),
1489                        condition: "error > 0.01".to_string(),
1490                        threshold: Some(0.01),
1491                        severity: "critical".to_string(),
1492                        interpretation: String::new(),
1493                    },
1494                    FalsificationCriterionYaml {
1495                        id: "FC-002".to_string(),
1496                        name: "Minor".to_string(),
1497                        condition: "drift > 0.1".to_string(),
1498                        threshold: Some(0.1),
1499                        severity: "minor".to_string(),
1500                        interpretation: String::new(),
1501                    },
1502                ],
1503                jidoka: None,
1504            }),
1505        };
1506
1507        let summary = runner.check_falsification_criteria(&spec, &experiment);
1508        // 1 from spec + 2 from experiment
1509        assert_eq!(summary.total, 3);
1510        assert!(!summary.jidoka_triggered);
1511    }
1512}