Skip to main content

simular/edd/
loader.rs

1//! EMC and Experiment YAML file loader.
2//!
3//! Provides functionality to load Equation Model Cards and Experiment
4//! specifications from YAML files, following the EDD specification.
5
6use super::equation::{Citation, EquationClass};
7use super::experiment::{
8    ExperimentHypothesis, ExperimentSpec, FalsificationAction, FalsificationCriterion,
9};
10use super::model_card::{EmcBuilder, EquationModelCard, VerificationTest};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::Path;
14
15/// YAML representation of an Equation Model Card.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct EmcYaml {
18    /// EMC schema version
19    #[serde(default = "default_emc_version")]
20    pub emc_version: String,
21    /// Unique EMC identifier
22    #[serde(default)]
23    pub emc_id: String,
24    /// Identity section
25    pub identity: EmcIdentityYaml,
26    /// Governing equation section
27    pub governing_equation: GoverningEquationYaml,
28    /// Analytical derivation section
29    #[serde(default)]
30    pub analytical_derivation: Option<AnalyticalDerivationYaml>,
31    /// Domain of validity
32    #[serde(default)]
33    pub domain_of_validity: Option<DomainValidityYaml>,
34    /// Verification tests
35    #[serde(default)]
36    pub verification_tests: Option<VerificationTestsYaml>,
37    /// Falsification criteria
38    #[serde(default)]
39    pub falsification_criteria: Option<FalsificationCriteriaYaml>,
40}
41
42fn default_emc_version() -> String {
43    "1.0".to_string()
44}
45
46/// Identity section of EMC.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct EmcIdentityYaml {
49    pub name: String,
50    #[serde(default = "default_version")]
51    pub version: String,
52    #[serde(default)]
53    pub authors: Vec<AuthorYaml>,
54    #[serde(default)]
55    pub status: String,
56    #[serde(default)]
57    pub description: String,
58}
59
60fn default_version() -> String {
61    "1.0.0".to_string()
62}
63
64/// Author information.
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct AuthorYaml {
67    pub name: String,
68    #[serde(default)]
69    pub affiliation: String,
70}
71
72/// Governing equation section.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct GoverningEquationYaml {
75    /// LaTeX representation
76    pub latex: String,
77    /// Plain text representation
78    #[serde(default)]
79    pub plain_text: String,
80    /// Description
81    #[serde(default)]
82    pub description: String,
83    /// Variables
84    #[serde(default)]
85    pub variables: Vec<VariableYaml>,
86    /// Equation type classification
87    #[serde(default)]
88    pub equation_type: String,
89}
90
91/// Variable definition.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct VariableYaml {
94    pub symbol: String,
95    pub description: String,
96    #[serde(default)]
97    pub units: String,
98    #[serde(default)]
99    pub r#type: String,
100}
101
102/// Analytical derivation section.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct AnalyticalDerivationYaml {
105    #[serde(default)]
106    pub primary_citation: Option<CitationYaml>,
107    #[serde(default)]
108    pub supporting_citations: Vec<CitationYaml>,
109    #[serde(default)]
110    pub derivation_method: String,
111    #[serde(default)]
112    pub derivation_summary: String,
113}
114
115/// Citation information.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct CitationYaml {
118    pub authors: Vec<String>,
119    #[serde(default)]
120    pub title: String,
121    #[serde(default)]
122    pub year: u32,
123    #[serde(default)]
124    pub journal: String,
125    #[serde(default)]
126    pub publisher: String,
127    #[serde(default)]
128    pub volume: Option<u32>,
129    #[serde(default)]
130    pub issue: Option<u32>,
131    #[serde(default)]
132    pub pages: String,
133    #[serde(default)]
134    pub doi: Option<String>,
135}
136
137/// Domain of validity section.
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct DomainValidityYaml {
140    #[serde(default)]
141    pub parameters: HashMap<String, ParameterConstraintYaml>,
142    #[serde(default)]
143    pub assumptions: Vec<String>,
144    #[serde(default)]
145    pub limitations: Vec<LimitationYaml>,
146}
147
148/// Parameter constraint.
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct ParameterConstraintYaml {
151    #[serde(default)]
152    pub min: Option<f64>,
153    #[serde(default)]
154    pub max: Option<f64>,
155    #[serde(default)]
156    pub units: String,
157    #[serde(default)]
158    pub physical_constraint: String,
159}
160
161/// Limitation description.
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct LimitationYaml {
164    pub description: String,
165}
166
167/// Verification tests section.
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct VerificationTestsYaml {
170    #[serde(default)]
171    pub tests: Vec<VerificationTestYaml>,
172}
173
174/// Single verification test.
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct VerificationTestYaml {
177    pub id: String,
178    pub name: String,
179    #[serde(default)]
180    pub r#type: String,
181    #[serde(default)]
182    pub parameters: HashMap<String, f64>,
183    #[serde(default)]
184    pub expected: HashMap<String, serde_yaml::Value>,
185    #[serde(default)]
186    pub tolerance: Option<f64>,
187    #[serde(default)]
188    pub description: String,
189}
190
191/// Falsification criteria section.
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct FalsificationCriteriaYaml {
194    #[serde(default)]
195    pub criteria: Vec<FalsificationCriterionYaml>,
196}
197
198/// Single falsification criterion.
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct FalsificationCriterionYaml {
201    pub id: String,
202    pub name: String,
203    pub condition: String,
204    #[serde(default)]
205    pub threshold: Option<f64>,
206    #[serde(default)]
207    pub severity: String,
208    #[serde(default)]
209    pub interpretation: String,
210}
211
212impl EmcYaml {
213    /// Load an EMC from a YAML file.
214    ///
215    /// # Errors
216    /// Returns error if file cannot be read or parsed.
217    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, String> {
218        let content = std::fs::read_to_string(path.as_ref())
219            .map_err(|e| format!("Failed to read EMC file: {e}"))?;
220        Self::from_yaml(&content)
221    }
222
223    /// Parse an EMC from a YAML string.
224    ///
225    /// # Errors
226    /// Returns error if YAML is invalid.
227    pub fn from_yaml(yaml: &str) -> Result<Self, String> {
228        serde_yaml::from_str(yaml).map_err(|e| format!("Failed to parse EMC YAML: {e}"))
229    }
230
231    /// Convert to an `EquationModelCard`.
232    ///
233    /// # Errors
234    /// Returns error if required fields are missing.
235    pub fn to_model_card(&self) -> Result<EquationModelCard, String> {
236        let mut builder = EmcBuilder::new()
237            .name(&self.identity.name)
238            .version(&self.identity.version)
239            .equation(&self.governing_equation.latex)
240            .description(&self.governing_equation.description);
241
242        // Set equation class based on type
243        let class = match self
244            .governing_equation
245            .equation_type
246            .to_lowercase()
247            .as_str()
248        {
249            "queueing" | "queue" => EquationClass::Queueing,
250            "statistical" => EquationClass::Statistical,
251            "inventory" => EquationClass::Inventory,
252            "optimization" => EquationClass::Optimization,
253            "ml" | "machine_learning" => EquationClass::MachineLearning,
254            // Default to Conservation for "conservation", "ode", or any other type
255            _ => EquationClass::Conservation,
256        };
257        builder = builder.class(class);
258
259        // Add citation
260        if let Some(ref derivation) = self.analytical_derivation {
261            if let Some(ref cite) = derivation.primary_citation {
262                let authors: Vec<&str> = cite.authors.iter().map(String::as_str).collect();
263                let venue = if cite.journal.is_empty() {
264                    &cite.publisher
265                } else {
266                    &cite.journal
267                };
268                let mut citation = Citation::new(&authors, venue, cite.year);
269                if !cite.title.is_empty() {
270                    citation = citation.with_title(&cite.title);
271                }
272                if let Some(ref doi) = cite.doi {
273                    citation = citation.with_doi(doi);
274                }
275                builder = builder.citation(citation);
276            }
277        }
278
279        // Add variables
280        for var in &self.governing_equation.variables {
281            builder = builder.add_variable(&var.symbol, &var.description, &var.units);
282        }
283
284        // Add verification tests
285        if let Some(ref tests) = self.verification_tests {
286            for test in &tests.tests {
287                let expected = test
288                    .expected
289                    .get("value")
290                    .and_then(serde_yaml::Value::as_f64)
291                    .unwrap_or(0.0);
292                let tolerance = test.tolerance.unwrap_or(1e-6);
293                let mut vtest = VerificationTest::new(&test.name, expected, tolerance);
294                for (name, &val) in &test.parameters {
295                    vtest = vtest.with_input(name, val);
296                }
297                builder = builder.add_verification_test_full(vtest);
298            }
299        }
300
301        // If no verification tests, add a placeholder (will fail validation correctly)
302        // This ensures the builder pattern works but validation catches missing tests
303
304        builder.build()
305    }
306
307    /// Validate EMC against the JSON schema.
308    ///
309    /// Performs structural validation to ensure all required fields
310    /// are present and correctly formatted.
311    ///
312    /// # Errors
313    ///
314    /// Returns error messages for each schema violation.
315    pub fn validate_schema(&self) -> Result<(), Vec<String>> {
316        let mut errors = Vec::new();
317
318        // EDD-01: EMC must have identity
319        if self.identity.name.is_empty() {
320            errors.push("EMC must have a name (identity.name)".to_string());
321        }
322
323        // EDD-02: EMC must have governing equation
324        if self.governing_equation.latex.is_empty() {
325            errors
326                .push("EMC must have a governing equation (governing_equation.latex)".to_string());
327        }
328
329        // Check description is meaningful
330        if self.governing_equation.description.len() < 10 {
331            errors.push(
332                "EMC governing equation should have a meaningful description (min 10 chars)"
333                    .to_string(),
334            );
335        }
336
337        // EDD-03: EMC must have citation
338        if let Some(ref derivation) = self.analytical_derivation {
339            if let Some(ref cite) = derivation.primary_citation {
340                if cite.authors.is_empty() {
341                    errors.push("Primary citation must have at least one author".to_string());
342                }
343                if cite.year == 0 {
344                    errors.push("Primary citation must have a valid year".to_string());
345                }
346            } else {
347                errors.push("EMC must have a primary citation (EDD-03)".to_string());
348            }
349        } else {
350            errors.push("EMC must have analytical derivation section with citation".to_string());
351        }
352
353        // EDD-04: EMC must have verification tests
354        if let Some(ref tests) = self.verification_tests {
355            if tests.tests.is_empty() {
356                errors.push("EMC must have at least one verification test (EDD-04)".to_string());
357            }
358        } else {
359            errors.push("EMC must have verification tests section".to_string());
360        }
361
362        // EDD-04: EMC must have falsification criteria
363        if let Some(ref criteria) = self.falsification_criteria {
364            if criteria.criteria.is_empty() {
365                errors.push("EMC must have at least one falsification criterion".to_string());
366            }
367        }
368
369        // Validate variables
370        for var in &self.governing_equation.variables {
371            if var.symbol.is_empty() {
372                errors.push("Variable must have a symbol".to_string());
373            }
374        }
375
376        if errors.is_empty() {
377            Ok(())
378        } else {
379            Err(errors)
380        }
381    }
382}
383
384/// YAML representation of an Experiment specification.
385#[derive(Debug, Clone, Serialize, Deserialize)]
386pub struct ExperimentYaml {
387    /// Experiment schema version
388    #[serde(default = "default_experiment_version")]
389    pub experiment_version: String,
390    /// Experiment ID
391    #[serde(default)]
392    pub experiment_id: String,
393    /// Metadata
394    pub metadata: ExperimentMetadataYaml,
395    /// Reference to EMC
396    #[serde(default)]
397    pub equation_model_card: Option<EmcReferenceYaml>,
398    /// Hypothesis
399    #[serde(default)]
400    pub hypothesis: Option<HypothesisYaml>,
401    /// Reproducibility settings
402    pub reproducibility: ReproducibilityYaml,
403    /// Simulation parameters
404    #[serde(default)]
405    pub simulation: Option<SimulationYaml>,
406    /// Falsification criteria
407    #[serde(default)]
408    pub falsification: Option<ExperimentFalsificationYaml>,
409}
410
411fn default_experiment_version() -> String {
412    "1.0".to_string()
413}
414
415/// Experiment metadata.
416#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct ExperimentMetadataYaml {
418    pub name: String,
419    #[serde(default)]
420    pub description: String,
421    #[serde(default)]
422    pub tags: Vec<String>,
423}
424
425/// EMC reference.
426#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct EmcReferenceYaml {
428    #[serde(default)]
429    pub emc_ref: String,
430    #[serde(default)]
431    pub emc_file: String,
432}
433
434/// Hypothesis definition.
435#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct HypothesisYaml {
437    pub null_hypothesis: String,
438    #[serde(default)]
439    pub alternative_hypothesis: String,
440    #[serde(default)]
441    pub expected_outcome: String,
442}
443
444/// Reproducibility settings.
445#[derive(Debug, Clone, Serialize, Deserialize)]
446pub struct ReproducibilityYaml {
447    pub seed: u64,
448    #[serde(default = "default_true")]
449    pub ieee_strict: bool,
450}
451
452fn default_true() -> bool {
453    true
454}
455
456/// Simulation parameters.
457#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct SimulationYaml {
459    #[serde(default)]
460    pub duration: Option<DurationYaml>,
461    #[serde(default)]
462    pub parameters: HashMap<String, serde_yaml::Value>,
463}
464
465/// Duration settings.
466#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct DurationYaml {
468    #[serde(default)]
469    pub warmup: f64,
470    #[serde(default)]
471    pub simulation: f64,
472    #[serde(default = "default_replications")]
473    pub replications: u32,
474}
475
476fn default_replications() -> u32 {
477    30
478}
479
480/// Experiment falsification settings.
481#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct ExperimentFalsificationYaml {
483    #[serde(default)]
484    pub import_from_emc: bool,
485    #[serde(default)]
486    pub criteria: Vec<FalsificationCriterionYaml>,
487    #[serde(default)]
488    pub jidoka: Option<JidokaYaml>,
489}
490
491/// Jidoka (stop-on-error) settings.
492#[derive(Debug, Clone, Serialize, Deserialize)]
493pub struct JidokaYaml {
494    #[serde(default)]
495    pub enabled: bool,
496    #[serde(default)]
497    pub stop_on_severity: String,
498}
499
500impl ExperimentYaml {
501    /// Load an experiment from a YAML file.
502    ///
503    /// # Errors
504    /// Returns error if file cannot be read or parsed.
505    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, String> {
506        let content = std::fs::read_to_string(path.as_ref())
507            .map_err(|e| format!("Failed to read experiment file: {e}"))?;
508        Self::from_yaml(&content)
509    }
510
511    /// Parse an experiment from a YAML string.
512    ///
513    /// # Errors
514    /// Returns error if YAML is invalid.
515    pub fn from_yaml(yaml: &str) -> Result<Self, String> {
516        serde_yaml::from_str(yaml).map_err(|e| format!("Failed to parse experiment YAML: {e}"))
517    }
518
519    /// Convert to an `ExperimentSpec`.
520    ///
521    /// # Errors
522    /// Returns error if required fields are missing.
523    pub fn to_experiment_spec(&self) -> Result<ExperimentSpec, String> {
524        let mut builder = ExperimentSpec::builder()
525            .name(&self.metadata.name)
526            .seed(self.reproducibility.seed)
527            .description(&self.metadata.description);
528
529        // Add EMC reference
530        if let Some(ref emc) = self.equation_model_card {
531            if !emc.emc_ref.is_empty() {
532                builder = builder.emc_reference(&emc.emc_ref);
533            }
534        }
535
536        // Add hypothesis
537        if let Some(ref hyp) = self.hypothesis {
538            let hypothesis =
539                ExperimentHypothesis::new(&hyp.null_hypothesis, &hyp.alternative_hypothesis);
540            builder = builder.hypothesis(hypothesis);
541        }
542
543        // Add duration settings
544        if let Some(ref sim) = self.simulation {
545            if let Some(ref dur) = sim.duration {
546                builder = builder
547                    .warmup(dur.warmup)
548                    .run_length(dur.simulation)
549                    .replications(dur.replications);
550            }
551        }
552
553        // Add falsification criteria
554        if let Some(ref fals) = self.falsification {
555            for crit in &fals.criteria {
556                let action = match crit.severity.to_lowercase().as_str() {
557                    "critical" => FalsificationAction::RejectModel,
558                    "major" => FalsificationAction::Stop,
559                    "minor" => FalsificationAction::Warn,
560                    _ => FalsificationAction::FlagReview,
561                };
562                let criterion = FalsificationCriterion::new(&crit.name, &crit.condition, action);
563                builder = builder.add_falsification_criterion(criterion);
564            }
565        }
566
567        builder.build()
568    }
569
570    /// Validate experiment against the JSON schema.
571    ///
572    /// Performs structural validation to ensure all required fields
573    /// are present and correctly formatted.
574    ///
575    /// # Errors
576    ///
577    /// Returns error messages for each schema violation.
578    pub fn validate_schema(&self) -> Result<(), Vec<String>> {
579        let mut errors = Vec::new();
580
581        // Metadata validation
582        if self.metadata.name.is_empty() {
583            errors.push("Experiment must have a name (metadata.name)".to_string());
584        }
585
586        // EDD-05: Seed is required
587        // Note: seed is always present due to struct definition, but we validate it's reasonable
588        if self.reproducibility.seed == 0 {
589            // 0 is technically valid but unusual - add warning
590        }
591
592        // EMC reference validation
593        if let Some(ref emc) = self.equation_model_card {
594            if emc.emc_ref.is_empty() && emc.emc_file.is_empty() {
595                errors.push("Experiment must reference an EMC (emc_ref or emc_file)".to_string());
596            }
597        }
598
599        // Hypothesis validation
600        if let Some(ref hyp) = self.hypothesis {
601            if hyp.null_hypothesis.len() < 10 {
602                errors.push(
603                    "Hypothesis must have a meaningful null hypothesis (min 10 chars)".to_string(),
604                );
605            }
606            if !hyp.expected_outcome.is_empty()
607                && hyp.expected_outcome != "reject"
608                && hyp.expected_outcome != "fail_to_reject"
609            {
610                errors.push(format!(
611                    "Expected outcome must be 'reject' or 'fail_to_reject', got '{}'",
612                    hyp.expected_outcome
613                ));
614            }
615        } else {
616            errors.push("Experiment must have a hypothesis section".to_string());
617        }
618
619        // Falsification criteria validation
620        if let Some(ref fals) = self.falsification {
621            if fals.criteria.is_empty() && !fals.import_from_emc {
622                errors.push(
623                    "Experiment must have falsification criteria or import_from_emc=true"
624                        .to_string(),
625                );
626            }
627            for crit in &fals.criteria {
628                if crit.condition.is_empty() {
629                    errors.push(format!("Criterion '{}' must have a condition", crit.name));
630                }
631            }
632        } else {
633            errors.push("Experiment must have falsification section".to_string());
634        }
635
636        // Simulation duration validation
637        if let Some(ref sim) = self.simulation {
638            if let Some(ref dur) = sim.duration {
639                if dur.simulation <= 0.0 {
640                    errors.push("Simulation duration must be positive".to_string());
641                }
642                if dur.replications == 0 {
643                    errors.push("Replications must be at least 1".to_string());
644                }
645            }
646        }
647
648        if errors.is_empty() {
649            Ok(())
650        } else {
651            Err(errors)
652        }
653    }
654}
655
656#[cfg(test)]
657mod tests {
658    use super::*;
659
660    const SAMPLE_EMC_YAML: &str = r#"
661emc_version: "1.0"
662emc_id: "EMC-TEST-001"
663
664identity:
665  name: "Little's Law"
666  version: "1.0.0"
667  status: "production"
668
669governing_equation:
670  latex: "L = \\lambda W"
671  plain_text: "WIP = Throughput × Cycle Time"
672  description: "Fundamental theorem of queueing theory"
673  equation_type: "queueing"
674  variables:
675    - symbol: "L"
676      description: "Average WIP"
677      units: "items"
678    - symbol: "λ"
679      description: "Arrival rate"
680      units: "items/time"
681    - symbol: "W"
682      description: "Cycle time"
683      units: "time"
684
685analytical_derivation:
686  primary_citation:
687    authors: ["Little, J.D.C."]
688    title: "A Proof for the Queuing Formula: L = λW"
689    journal: "Operations Research"
690    year: 1961
691
692verification_tests:
693  tests:
694    - id: "LL-001"
695      name: "Basic validation"
696      parameters:
697        lambda: 5.0
698        W: 2.0
699      expected:
700        value: 10.0
701      tolerance: 0.001
702"#;
703
704    #[test]
705    fn test_parse_emc_yaml() {
706        let emc = EmcYaml::from_yaml(SAMPLE_EMC_YAML);
707        assert!(emc.is_ok());
708        let emc = emc.ok().unwrap();
709        assert_eq!(emc.identity.name, "Little's Law");
710        assert_eq!(emc.governing_equation.variables.len(), 3);
711    }
712
713    #[test]
714    fn test_emc_to_model_card() {
715        let emc_yaml = EmcYaml::from_yaml(SAMPLE_EMC_YAML).ok().unwrap();
716        let model_card = emc_yaml.to_model_card();
717        assert!(model_card.is_ok());
718        let mc = model_card.ok().unwrap();
719        assert_eq!(mc.name, "Little's Law");
720        assert!(mc.equation.contains("lambda"));
721    }
722
723    const SAMPLE_EXPERIMENT_YAML: &str = r#"
724experiment_version: "1.0"
725experiment_id: "EXP-001"
726
727metadata:
728  name: "Little's Law Validation"
729  description: "Validate L = λW under stochastic conditions"
730
731equation_model_card:
732  emc_ref: "operations/littles_law"
733
734hypothesis:
735  null_hypothesis: "L ≠ λW under stochastic conditions"
736  alternative_hypothesis: "L = λW holds"
737  expected_outcome: "reject"
738
739reproducibility:
740  seed: 42
741  ieee_strict: true
742
743simulation:
744  duration:
745    warmup: 100.0
746    simulation: 1000.0
747    replications: 30
748
749falsification:
750  criteria:
751    - id: "FC-001"
752      name: "Linear relationship"
753      condition: "R² < 0.95"
754      severity: "critical"
755"#;
756
757    #[test]
758    fn test_parse_experiment_yaml() {
759        let exp = ExperimentYaml::from_yaml(SAMPLE_EXPERIMENT_YAML);
760        assert!(exp.is_ok());
761        let exp = exp.ok().unwrap();
762        assert_eq!(exp.metadata.name, "Little's Law Validation");
763        assert_eq!(exp.reproducibility.seed, 42);
764    }
765
766    #[test]
767    fn test_experiment_to_spec() {
768        let exp_yaml = ExperimentYaml::from_yaml(SAMPLE_EXPERIMENT_YAML)
769            .ok()
770            .unwrap();
771        let spec = exp_yaml.to_experiment_spec();
772        assert!(spec.is_ok());
773        let spec = spec.ok().unwrap();
774        assert_eq!(spec.seed(), 42);
775        assert_eq!(spec.name(), "Little's Law Validation");
776    }
777
778    #[test]
779    fn test_emc_schema_validation() {
780        let emc = EmcYaml::from_yaml(SAMPLE_EMC_YAML).ok().unwrap();
781        let result = emc.validate_schema();
782        assert!(
783            result.is_ok(),
784            "EMC should pass schema validation: {:?}",
785            result.err()
786        );
787    }
788
789    #[test]
790    fn test_experiment_schema_validation() {
791        let exp = ExperimentYaml::from_yaml(SAMPLE_EXPERIMENT_YAML)
792            .ok()
793            .unwrap();
794        let result = exp.validate_schema();
795        assert!(
796            result.is_ok(),
797            "Experiment should pass schema validation: {:?}",
798            result.err()
799        );
800    }
801
802    #[test]
803    fn test_emc_missing_required_field() {
804        let invalid_yaml = r#"
805emc_version: "1.0"
806identity:
807  name: "Missing equation"
808  version: "1.0.0"
809governing_equation:
810  latex: "x = y"
811"#;
812        let emc = EmcYaml::from_yaml(invalid_yaml);
813        // Should parse but fail schema validation
814        if let Ok(emc) = emc {
815            // validate_schema may or may not catch missing analytical_derivation
816            let _result = emc.validate_schema();
817        }
818    }
819
820    #[test]
821    fn test_default_emc_version() {
822        assert_eq!(default_emc_version(), "1.0");
823    }
824
825    #[test]
826    fn test_default_version() {
827        assert_eq!(default_version(), "1.0.0");
828    }
829
830    #[test]
831    fn test_default_experiment_version() {
832        assert_eq!(default_experiment_version(), "1.0");
833    }
834
835    #[test]
836    fn test_default_true() {
837        assert!(default_true());
838    }
839
840    #[test]
841    fn test_default_replications() {
842        assert_eq!(default_replications(), 30);
843    }
844
845    #[test]
846    fn test_emc_from_file_not_found() {
847        let result = EmcYaml::from_file("nonexistent.yaml");
848        assert!(result.is_err());
849        assert!(result.err().unwrap().contains("Failed to read"));
850    }
851
852    #[test]
853    fn test_experiment_from_file_not_found() {
854        let result = ExperimentYaml::from_file("nonexistent.yaml");
855        assert!(result.is_err());
856        assert!(result.err().unwrap().contains("Failed to read"));
857    }
858
859    #[test]
860    fn test_emc_invalid_yaml() {
861        let invalid = "not: [valid: yaml: content";
862        let result = EmcYaml::from_yaml(invalid);
863        assert!(result.is_err());
864        assert!(result.err().unwrap().contains("Failed to parse"));
865    }
866
867    #[test]
868    fn test_experiment_invalid_yaml() {
869        let invalid = "not: [valid: yaml: content";
870        let result = ExperimentYaml::from_yaml(invalid);
871        assert!(result.is_err());
872        assert!(result.err().unwrap().contains("Failed to parse"));
873    }
874
875    #[test]
876    fn test_emc_validation_missing_name() {
877        let yaml = r#"
878identity:
879  name: ""
880  version: "1.0.0"
881governing_equation:
882  latex: "x = y"
883  description: "A valid description"
884"#;
885        let emc = EmcYaml::from_yaml(yaml).ok().unwrap();
886        let result = emc.validate_schema();
887        assert!(result.is_err());
888        let errors = result.err().unwrap();
889        assert!(errors.iter().any(|e| e.contains("name")));
890    }
891
892    #[test]
893    fn test_emc_validation_missing_latex() {
894        let yaml = r#"
895identity:
896  name: "Test"
897  version: "1.0.0"
898governing_equation:
899  latex: ""
900  description: "A valid description"
901"#;
902        let emc = EmcYaml::from_yaml(yaml).ok().unwrap();
903        let result = emc.validate_schema();
904        assert!(result.is_err());
905        let errors = result.err().unwrap();
906        assert!(errors.iter().any(|e| e.contains("governing equation")));
907    }
908
909    #[test]
910    fn test_emc_validation_short_description() {
911        let yaml = r#"
912identity:
913  name: "Test"
914  version: "1.0.0"
915governing_equation:
916  latex: "x = y"
917  description: "Short"
918"#;
919        let emc = EmcYaml::from_yaml(yaml).ok().unwrap();
920        let result = emc.validate_schema();
921        assert!(result.is_err());
922        let errors = result.err().unwrap();
923        assert!(errors.iter().any(|e| e.contains("meaningful description")));
924    }
925
926    #[test]
927    fn test_emc_validation_empty_authors() {
928        let yaml = r#"
929identity:
930  name: "Test"
931  version: "1.0.0"
932governing_equation:
933  latex: "x = y"
934  description: "A valid description for the equation"
935analytical_derivation:
936  primary_citation:
937    authors: []
938    title: "Test"
939    year: 2020
940"#;
941        let emc = EmcYaml::from_yaml(yaml).ok().unwrap();
942        let result = emc.validate_schema();
943        assert!(result.is_err());
944        let errors = result.err().unwrap();
945        assert!(errors.iter().any(|e| e.contains("author")));
946    }
947
948    #[test]
949    fn test_emc_validation_invalid_year() {
950        let yaml = r#"
951identity:
952  name: "Test"
953  version: "1.0.0"
954governing_equation:
955  latex: "x = y"
956  description: "A valid description for the equation"
957analytical_derivation:
958  primary_citation:
959    authors: ["Author"]
960    title: "Test"
961    year: 0
962"#;
963        let emc = EmcYaml::from_yaml(yaml).ok().unwrap();
964        let result = emc.validate_schema();
965        assert!(result.is_err());
966        let errors = result.err().unwrap();
967        assert!(errors.iter().any(|e| e.contains("year")));
968    }
969
970    #[test]
971    fn test_emc_validation_empty_verification_tests() {
972        let yaml = r#"
973identity:
974  name: "Test"
975  version: "1.0.0"
976governing_equation:
977  latex: "x = y"
978  description: "A valid description for the equation"
979analytical_derivation:
980  primary_citation:
981    authors: ["Author"]
982    title: "Test"
983    year: 2020
984verification_tests:
985  tests: []
986"#;
987        let emc = EmcYaml::from_yaml(yaml).ok().unwrap();
988        let result = emc.validate_schema();
989        assert!(result.is_err());
990        let errors = result.err().unwrap();
991        assert!(errors.iter().any(|e| e.contains("verification test")));
992    }
993
994    #[test]
995    fn test_emc_validation_empty_falsification() {
996        let yaml = r#"
997identity:
998  name: "Test"
999  version: "1.0.0"
1000governing_equation:
1001  latex: "x = y"
1002  description: "A valid description for the equation"
1003analytical_derivation:
1004  primary_citation:
1005    authors: ["Author"]
1006    title: "Test"
1007    year: 2020
1008verification_tests:
1009  tests:
1010    - id: "T1"
1011      name: "Test"
1012falsification_criteria:
1013  criteria: []
1014"#;
1015        let emc = EmcYaml::from_yaml(yaml).ok().unwrap();
1016        let result = emc.validate_schema();
1017        assert!(result.is_err());
1018        let errors = result.err().unwrap();
1019        assert!(errors.iter().any(|e| e.contains("falsification")));
1020    }
1021
1022    #[test]
1023    fn test_emc_validation_empty_variable_symbol() {
1024        let yaml = r#"
1025identity:
1026  name: "Test"
1027  version: "1.0.0"
1028governing_equation:
1029  latex: "x = y"
1030  description: "A valid description for the equation"
1031  variables:
1032    - symbol: ""
1033      description: "Empty symbol"
1034analytical_derivation:
1035  primary_citation:
1036    authors: ["Author"]
1037    title: "Test"
1038    year: 2020
1039verification_tests:
1040  tests:
1041    - id: "T1"
1042      name: "Test"
1043"#;
1044        let emc = EmcYaml::from_yaml(yaml).ok().unwrap();
1045        let result = emc.validate_schema();
1046        assert!(result.is_err());
1047        let errors = result.err().unwrap();
1048        assert!(errors.iter().any(|e| e.contains("symbol")));
1049    }
1050
1051    #[test]
1052    fn test_experiment_validation_missing_name() {
1053        let yaml = r#"
1054metadata:
1055  name: ""
1056reproducibility:
1057  seed: 42
1058"#;
1059        let exp = ExperimentYaml::from_yaml(yaml).ok().unwrap();
1060        let result = exp.validate_schema();
1061        assert!(result.is_err());
1062        let errors = result.err().unwrap();
1063        assert!(errors.iter().any(|e| e.contains("name")));
1064    }
1065
1066    #[test]
1067    fn test_experiment_validation_empty_emc_ref() {
1068        let yaml = r#"
1069metadata:
1070  name: "Test"
1071reproducibility:
1072  seed: 42
1073equation_model_card:
1074  emc_ref: ""
1075  emc_file: ""
1076"#;
1077        let exp = ExperimentYaml::from_yaml(yaml).ok().unwrap();
1078        let result = exp.validate_schema();
1079        assert!(result.is_err());
1080        let errors = result.err().unwrap();
1081        assert!(errors.iter().any(|e| e.contains("EMC")));
1082    }
1083
1084    #[test]
1085    fn test_experiment_validation_short_hypothesis() {
1086        let yaml = r#"
1087metadata:
1088  name: "Test"
1089reproducibility:
1090  seed: 42
1091hypothesis:
1092  null_hypothesis: "Short"
1093"#;
1094        let exp = ExperimentYaml::from_yaml(yaml).ok().unwrap();
1095        let result = exp.validate_schema();
1096        assert!(result.is_err());
1097        let errors = result.err().unwrap();
1098        assert!(errors
1099            .iter()
1100            .any(|e| e.contains("meaningful null hypothesis")));
1101    }
1102
1103    #[test]
1104    fn test_experiment_validation_invalid_expected_outcome() {
1105        let yaml = r#"
1106metadata:
1107  name: "Test"
1108reproducibility:
1109  seed: 42
1110hypothesis:
1111  null_hypothesis: "A valid null hypothesis for testing"
1112  expected_outcome: "invalid_outcome"
1113"#;
1114        let exp = ExperimentYaml::from_yaml(yaml).ok().unwrap();
1115        let result = exp.validate_schema();
1116        assert!(result.is_err());
1117        let errors = result.err().unwrap();
1118        assert!(errors.iter().any(|e| e.contains("Expected outcome")));
1119    }
1120
1121    #[test]
1122    fn test_experiment_validation_empty_falsification_criteria() {
1123        let yaml = r#"
1124metadata:
1125  name: "Test"
1126reproducibility:
1127  seed: 42
1128hypothesis:
1129  null_hypothesis: "A valid null hypothesis for testing"
1130  expected_outcome: "reject"
1131falsification:
1132  import_from_emc: false
1133  criteria: []
1134"#;
1135        let exp = ExperimentYaml::from_yaml(yaml).ok().unwrap();
1136        let result = exp.validate_schema();
1137        assert!(result.is_err());
1138        let errors = result.err().unwrap();
1139        assert!(errors.iter().any(|e| e.contains("falsification criteria")));
1140    }
1141
1142    #[test]
1143    fn test_experiment_validation_criterion_no_condition() {
1144        let yaml = r#"
1145metadata:
1146  name: "Test"
1147reproducibility:
1148  seed: 42
1149hypothesis:
1150  null_hypothesis: "A valid null hypothesis for testing"
1151  expected_outcome: "reject"
1152falsification:
1153  criteria:
1154    - id: "FC-001"
1155      name: "Test"
1156      condition: ""
1157"#;
1158        let exp = ExperimentYaml::from_yaml(yaml).ok().unwrap();
1159        let result = exp.validate_schema();
1160        assert!(result.is_err());
1161        let errors = result.err().unwrap();
1162        assert!(errors.iter().any(|e| e.contains("condition")));
1163    }
1164
1165    #[test]
1166    fn test_experiment_validation_negative_duration() {
1167        let yaml = r#"
1168metadata:
1169  name: "Test"
1170reproducibility:
1171  seed: 42
1172hypothesis:
1173  null_hypothesis: "A valid null hypothesis for testing"
1174  expected_outcome: "reject"
1175falsification:
1176  criteria:
1177    - id: "FC-001"
1178      name: "Test"
1179      condition: "error < 0.01"
1180simulation:
1181  duration:
1182    simulation: -10.0
1183    replications: 30
1184"#;
1185        let exp = ExperimentYaml::from_yaml(yaml).ok().unwrap();
1186        let result = exp.validate_schema();
1187        assert!(result.is_err());
1188        let errors = result.err().unwrap();
1189        assert!(errors.iter().any(|e| e.contains("positive")));
1190    }
1191
1192    #[test]
1193    fn test_experiment_validation_zero_replications() {
1194        let yaml = r#"
1195metadata:
1196  name: "Test"
1197reproducibility:
1198  seed: 42
1199hypothesis:
1200  null_hypothesis: "A valid null hypothesis for testing"
1201  expected_outcome: "reject"
1202falsification:
1203  criteria:
1204    - id: "FC-001"
1205      name: "Test"
1206      condition: "error < 0.01"
1207simulation:
1208  duration:
1209    simulation: 100.0
1210    replications: 0
1211"#;
1212        let exp = ExperimentYaml::from_yaml(yaml).ok().unwrap();
1213        let result = exp.validate_schema();
1214        assert!(result.is_err());
1215        let errors = result.err().unwrap();
1216        assert!(errors.iter().any(|e| e.contains("Replications")));
1217    }
1218
1219    #[test]
1220    fn test_emc_to_model_card_with_doi() {
1221        let yaml = r#"
1222emc_version: "1.0"
1223identity:
1224  name: "Test EMC"
1225  version: "1.0.0"
1226governing_equation:
1227  latex: "x = y"
1228  description: "Test equation for model card"
1229  equation_type: "optimization"
1230  variables:
1231    - symbol: "x"
1232      description: "Input"
1233      units: "m"
1234    - symbol: "y"
1235      description: "Output"
1236      units: "m"
1237analytical_derivation:
1238  primary_citation:
1239    authors: ["Test Author"]
1240    title: "Test Title"
1241    year: 2020
1242    journal: "Test Journal"
1243    doi: "10.1000/test"
1244verification_tests:
1245  tests:
1246    - id: "VT-001"
1247      name: "Basic Test"
1248      parameters:
1249        x: 1.0
1250      expected:
1251        value: 1.0
1252      tolerance: 0.001
1253"#;
1254        let emc = EmcYaml::from_yaml(yaml).ok().unwrap();
1255        let model_card = emc.to_model_card();
1256        assert!(model_card.is_ok());
1257        let mc = model_card.ok().unwrap();
1258        assert_eq!(mc.name, "Test EMC");
1259    }
1260
1261    #[test]
1262    fn test_emc_to_model_card_equation_classes() {
1263        // Helper to create valid EMC YAML with different equation types
1264        fn make_emc_yaml(name: &str, eq_type: &str) -> String {
1265            format!(
1266                r#"
1267identity:
1268  name: "{name}"
1269governing_equation:
1270  latex: "x = y"
1271  description: "Test description for {eq_type}"
1272  equation_type: "{eq_type}"
1273analytical_derivation:
1274  primary_citation:
1275    authors: ["Author"]
1276    title: "Title"
1277    year: 2020
1278verification_tests:
1279  tests:
1280    - id: "VT-001"
1281      name: "Basic Test"
1282      expected:
1283        value: 1.0
1284"#
1285            )
1286        }
1287
1288        // Test queueing
1289        let yaml = make_emc_yaml("Queue", "queue");
1290        let emc = EmcYaml::from_yaml(&yaml).ok().unwrap();
1291        let mc = emc.to_model_card().ok().unwrap();
1292        assert_eq!(mc.class, EquationClass::Queueing);
1293
1294        // Test statistical
1295        let yaml = make_emc_yaml("Stat", "statistical");
1296        let emc = EmcYaml::from_yaml(&yaml).ok().unwrap();
1297        let mc = emc.to_model_card().ok().unwrap();
1298        assert_eq!(mc.class, EquationClass::Statistical);
1299
1300        // Test inventory
1301        let yaml = make_emc_yaml("Inv", "inventory");
1302        let emc = EmcYaml::from_yaml(&yaml).ok().unwrap();
1303        let mc = emc.to_model_card().ok().unwrap();
1304        assert_eq!(mc.class, EquationClass::Inventory);
1305
1306        // Test ML
1307        let yaml = make_emc_yaml("ML", "machine_learning");
1308        let emc = EmcYaml::from_yaml(&yaml).ok().unwrap();
1309        let mc = emc.to_model_card().ok().unwrap();
1310        assert_eq!(mc.class, EquationClass::MachineLearning);
1311    }
1312
1313    #[test]
1314    fn test_emc_to_model_card_with_publisher() {
1315        let yaml = r#"
1316identity:
1317  name: "Test"
1318governing_equation:
1319  latex: "x = y"
1320  description: "Test description that is valid"
1321analytical_derivation:
1322  primary_citation:
1323    authors: ["Author"]
1324    title: "Title"
1325    year: 2020
1326    publisher: "Publisher Name"
1327verification_tests:
1328  tests:
1329    - id: "VT-001"
1330      name: "Basic Test"
1331      expected:
1332        value: 1.0
1333"#;
1334        let emc = EmcYaml::from_yaml(yaml).ok().unwrap();
1335        let mc = emc.to_model_card().ok().unwrap();
1336        assert!(mc.citation.venue.contains("Publisher"));
1337    }
1338
1339    #[test]
1340    fn test_experiment_to_spec_with_severity_levels() {
1341        // Test critical severity
1342        let yaml = r#"
1343metadata:
1344  name: "Test"
1345  description: "Test description"
1346reproducibility:
1347  seed: 42
1348equation_model_card:
1349  emc_ref: "test/emc"
1350hypothesis:
1351  null_hypothesis: "Test null hypothesis for validation"
1352  alternative_hypothesis: "Alternative"
1353falsification:
1354  criteria:
1355    - id: "FC-001"
1356      name: "Critical"
1357      condition: "error < 0.01"
1358      severity: "critical"
1359    - id: "FC-002"
1360      name: "Major"
1361      condition: "error < 0.05"
1362      severity: "major"
1363    - id: "FC-003"
1364      name: "Minor"
1365      condition: "error < 0.10"
1366      severity: "minor"
1367    - id: "FC-004"
1368      name: "Unknown"
1369      condition: "error < 0.20"
1370      severity: "unknown"
1371simulation:
1372  duration:
1373    warmup: 10.0
1374    simulation: 100.0
1375    replications: 10
1376"#;
1377        let exp = ExperimentYaml::from_yaml(yaml).ok().unwrap();
1378        let spec = exp.to_experiment_spec();
1379        assert!(spec.is_ok());
1380        let spec = spec.ok().unwrap();
1381        assert_eq!(spec.falsification_criteria().len(), 4);
1382    }
1383
1384    #[test]
1385    fn test_experiment_to_spec_no_emc_ref() {
1386        let yaml = r#"
1387metadata:
1388  name: "Test"
1389reproducibility:
1390  seed: 42
1391equation_model_card:
1392  emc_ref: ""
1393"#;
1394        let exp = ExperimentYaml::from_yaml(yaml).ok().unwrap();
1395        let spec = exp.to_experiment_spec();
1396        assert!(spec.is_ok());
1397    }
1398
1399    #[test]
1400    fn test_experiment_to_spec_no_simulation() {
1401        let yaml = r#"
1402metadata:
1403  name: "Test"
1404reproducibility:
1405  seed: 42
1406"#;
1407        let exp = ExperimentYaml::from_yaml(yaml).ok().unwrap();
1408        let spec = exp.to_experiment_spec();
1409        assert!(spec.is_ok());
1410    }
1411
1412    #[test]
1413    fn test_experiment_validation_valid_expected_outcomes() {
1414        // Test reject
1415        let yaml = r#"
1416metadata:
1417  name: "Test"
1418reproducibility:
1419  seed: 42
1420hypothesis:
1421  null_hypothesis: "A valid null hypothesis for testing"
1422  expected_outcome: "reject"
1423falsification:
1424  import_from_emc: true
1425"#;
1426        let exp = ExperimentYaml::from_yaml(yaml).ok().unwrap();
1427        let result = exp.validate_schema();
1428        assert!(result.is_ok());
1429
1430        // Test fail_to_reject
1431        let yaml = r#"
1432metadata:
1433  name: "Test"
1434reproducibility:
1435  seed: 42
1436hypothesis:
1437  null_hypothesis: "A valid null hypothesis for testing"
1438  expected_outcome: "fail_to_reject"
1439falsification:
1440  import_from_emc: true
1441"#;
1442        let exp = ExperimentYaml::from_yaml(yaml).ok().unwrap();
1443        let result = exp.validate_schema();
1444        assert!(result.is_ok());
1445    }
1446
1447    #[test]
1448    fn test_author_yaml_serialization() {
1449        let author = AuthorYaml {
1450            name: "Test Author".to_string(),
1451            affiliation: "Test University".to_string(),
1452        };
1453        let json = serde_json::to_string(&author);
1454        assert!(json.is_ok());
1455    }
1456
1457    #[test]
1458    fn test_jidoka_yaml() {
1459        let jidoka = JidokaYaml {
1460            enabled: true,
1461            stop_on_severity: "critical".to_string(),
1462        };
1463        assert!(jidoka.enabled);
1464    }
1465
1466    #[test]
1467    fn test_limitation_yaml() {
1468        let limitation = LimitationYaml {
1469            description: "Test limitation".to_string(),
1470        };
1471        assert!(!limitation.description.is_empty());
1472    }
1473
1474    #[test]
1475    fn test_parameter_constraint_yaml() {
1476        let constraint = ParameterConstraintYaml {
1477            min: Some(0.0),
1478            max: Some(100.0),
1479            units: "meters".to_string(),
1480            physical_constraint: "must be positive".to_string(),
1481        };
1482        assert!(constraint.min.is_some());
1483        assert!(constraint.max.is_some());
1484    }
1485}