1use super::equation::{Citation, EquationClass, EquationVariable, GoverningEquation};
19use std::collections::HashMap;
20
21pub use super::equation::Citation as EmcCitation;
23
24#[derive(Debug, Clone)]
26pub struct DomainConstraint {
27 pub name: String,
29 pub expression: String,
31 pub rationale: String,
33 pub violation_behavior: ViolationBehavior,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum ViolationBehavior {
40 Warn,
42 Halt,
44 Clamp,
46 Special,
48}
49
50#[derive(Debug, Clone)]
52pub struct VerificationTest {
53 pub description: String,
55 pub inputs: HashMap<String, f64>,
57 pub expected: f64,
59 pub tolerance: f64,
61 pub source: Option<String>,
63}
64
65impl VerificationTest {
66 #[must_use]
68 pub fn new(description: &str, expected: f64, tolerance: f64) -> Self {
69 Self {
70 description: description.to_string(),
71 inputs: HashMap::new(),
72 expected,
73 tolerance,
74 source: None,
75 }
76 }
77
78 #[must_use]
80 pub fn with_input(mut self, name: &str, value: f64) -> Self {
81 self.inputs.insert(name.to_string(), value);
82 self
83 }
84
85 #[must_use]
87 pub fn with_source(mut self, source: &str) -> Self {
88 self.source = Some(source.to_string());
89 self
90 }
91}
92
93#[derive(Debug, Clone)]
95pub struct FalsificationCriterion {
96 pub name: String,
98 pub description: String,
100 pub test_method: String,
102 pub alpha: f64,
104}
105
106#[derive(Debug, Clone)]
108pub struct ImplementationNote {
109 pub topic: String,
111 pub content: String,
113 pub importance: NoteImportance,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum NoteImportance {
120 Info,
122 Important,
124 Critical,
126}
127
128#[derive(Debug, Clone)]
136pub struct EquationModelCard {
137 pub name: String,
139 pub version: String,
141 pub equation: String,
143 pub class: EquationClass,
145 pub citation: Citation,
147 pub references: Vec<Citation>,
149 pub variables: Vec<EquationVariable>,
151 pub verification_tests: Vec<VerificationTest>,
153 pub domain_constraints: Vec<DomainConstraint>,
155 pub falsification_criteria: Vec<FalsificationCriterion>,
157 pub implementation_notes: Vec<ImplementationNote>,
159 pub description: String,
161 pub lineage: Vec<String>,
163}
164
165#[derive(Debug, Default)]
169pub struct EmcBuilder {
170 name: Option<String>,
171 version: String,
172 equation: Option<String>,
173 class: Option<EquationClass>,
174 citation: Option<Citation>,
175 references: Vec<Citation>,
176 variables: Vec<EquationVariable>,
177 verification_tests: Vec<VerificationTest>,
178 domain_constraints: Vec<DomainConstraint>,
179 falsification_criteria: Vec<FalsificationCriterion>,
180 implementation_notes: Vec<ImplementationNote>,
181 description: String,
182 lineage: Vec<String>,
183}
184
185impl EmcBuilder {
186 #[must_use]
188 pub fn new() -> Self {
189 Self {
190 version: "1.0.0".to_string(),
191 ..Default::default()
192 }
193 }
194
195 #[must_use]
197 pub fn name(mut self, name: &str) -> Self {
198 self.name = Some(name.to_string());
199 self
200 }
201
202 #[must_use]
204 pub fn version(mut self, version: &str) -> Self {
205 self.version = version.to_string();
206 self
207 }
208
209 #[must_use]
211 pub fn equation(mut self, equation: &str) -> Self {
212 self.equation = Some(equation.to_string());
213 self
214 }
215
216 #[must_use]
218 pub fn class(mut self, class: EquationClass) -> Self {
219 self.class = Some(class);
220 self
221 }
222
223 #[must_use]
225 pub fn citation(mut self, citation: Citation) -> Self {
226 self.citation = Some(citation);
227 self
228 }
229
230 #[must_use]
232 pub fn add_reference(mut self, reference: Citation) -> Self {
233 self.references.push(reference);
234 self
235 }
236
237 #[must_use]
239 pub fn add_variable(mut self, symbol: &str, name: &str, units: &str) -> Self {
240 self.variables
241 .push(EquationVariable::new(symbol, name, units));
242 self
243 }
244
245 #[must_use]
247 pub fn add_variable_full(mut self, variable: EquationVariable) -> Self {
248 self.variables.push(variable);
249 self
250 }
251
252 #[must_use]
254 pub fn add_verification_test(
255 mut self,
256 description: &str,
257 expected: f64,
258 tolerance: f64,
259 ) -> Self {
260 self.verification_tests
261 .push(VerificationTest::new(description, expected, tolerance));
262 self
263 }
264
265 #[must_use]
267 pub fn add_verification_test_full(mut self, test: VerificationTest) -> Self {
268 self.verification_tests.push(test);
269 self
270 }
271
272 #[must_use]
274 pub fn add_domain_constraint(mut self, constraint: DomainConstraint) -> Self {
275 self.domain_constraints.push(constraint);
276 self
277 }
278
279 #[must_use]
281 pub fn add_falsification_criterion(mut self, criterion: FalsificationCriterion) -> Self {
282 self.falsification_criteria.push(criterion);
283 self
284 }
285
286 #[must_use]
288 pub fn add_implementation_note(mut self, note: ImplementationNote) -> Self {
289 self.implementation_notes.push(note);
290 self
291 }
292
293 #[must_use]
295 pub fn description(mut self, description: &str) -> Self {
296 self.description = description.to_string();
297 self
298 }
299
300 #[must_use]
302 pub fn add_lineage(mut self, parent: &str) -> Self {
303 self.lineage.push(parent.to_string());
304 self
305 }
306
307 pub fn build(self) -> Result<EquationModelCard, String> {
318 let name = self.name.ok_or("EMC requires a name")?;
319 let equation = self.equation.ok_or("EMC requires a governing equation")?;
320 let citation = self
321 .citation
322 .ok_or("EMC requires a citation (analytical derivation)")?;
323
324 if self.verification_tests.is_empty() {
325 return Err("EMC requires at least one verification test".to_string());
326 }
327
328 Ok(EquationModelCard {
329 name,
330 version: self.version,
331 equation,
332 class: self.class.unwrap_or(EquationClass::Conservation),
333 citation,
334 references: self.references,
335 variables: self.variables,
336 verification_tests: self.verification_tests,
337 domain_constraints: self.domain_constraints,
338 falsification_criteria: self.falsification_criteria,
339 implementation_notes: self.implementation_notes,
340 description: self.description,
341 lineage: self.lineage,
342 })
343 }
344}
345
346impl EquationModelCard {
347 #[must_use]
349 pub fn builder() -> EmcBuilder {
350 EmcBuilder::new()
351 }
352
353 #[must_use]
355 pub fn from_equation<E: GoverningEquation>(equation: &E) -> EmcBuilder {
356 EmcBuilder::new()
357 .name(equation.name())
358 .equation(equation.latex())
359 .class(equation.class())
360 .citation(equation.citation())
361 .description(equation.description())
362 }
363
364 pub fn run_verification_tests<F>(&self, evaluator: F) -> Vec<(String, bool, String)>
368 where
369 F: Fn(&HashMap<String, f64>) -> f64,
370 {
371 self.verification_tests
372 .iter()
373 .map(|test| {
374 let actual = evaluator(&test.inputs);
375 let passed = (actual - test.expected).abs() <= test.tolerance;
376 let message = if passed {
377 format!(
378 "PASS: {} (expected={}, actual={})",
379 test.description, test.expected, actual
380 )
381 } else {
382 format!(
383 "FAIL: {} (expected={}, actual={}, tolerance={})",
384 test.description, test.expected, actual, test.tolerance
385 )
386 };
387 (test.description.clone(), passed, message)
388 })
389 .collect()
390 }
391
392 pub fn verify<F>(&self, evaluator: F) -> Result<(), Vec<String>>
397 where
398 F: Fn(&HashMap<String, f64>) -> f64,
399 {
400 let results = self.run_verification_tests(evaluator);
401 let failures: Vec<String> = results
402 .into_iter()
403 .filter(|(_, passed, _)| !passed)
404 .map(|(_, _, msg)| msg)
405 .collect();
406
407 if failures.is_empty() {
408 Ok(())
409 } else {
410 Err(failures)
411 }
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418
419 #[test]
420 fn test_emc_builder_requires_name() {
421 let result = EmcBuilder::new()
422 .equation("L = \\lambda W")
423 .citation(Citation::new(&["Test"], "Test", 2024))
424 .add_verification_test("test", 1.0, 0.1)
425 .build();
426
427 assert!(result.is_err());
428 assert!(result.err().map(|e| e.contains("name")).unwrap_or(false));
429 }
430
431 #[test]
432 fn test_emc_builder_requires_equation() {
433 let result = EmcBuilder::new()
434 .name("Test EMC")
435 .citation(Citation::new(&["Test"], "Test", 2024))
436 .add_verification_test("test", 1.0, 0.1)
437 .build();
438
439 assert!(result.is_err());
440 assert!(result
441 .err()
442 .map(|e| e.contains("equation"))
443 .unwrap_or(false));
444 }
445
446 #[test]
447 fn test_emc_builder_requires_citation() {
448 let result = EmcBuilder::new()
449 .name("Test EMC")
450 .equation("L = \\lambda W")
451 .add_verification_test("test", 1.0, 0.1)
452 .build();
453
454 assert!(result.is_err());
455 assert!(result
456 .err()
457 .map(|e| e.contains("citation"))
458 .unwrap_or(false));
459 }
460
461 #[test]
462 fn test_emc_builder_requires_verification_tests() {
463 let result = EmcBuilder::new()
464 .name("Test EMC")
465 .equation("L = \\lambda W")
466 .citation(Citation::new(&["Test"], "Test", 2024))
467 .build();
468
469 assert!(result.is_err());
470 assert!(result
471 .err()
472 .map(|e| e.contains("verification"))
473 .unwrap_or(false));
474 }
475
476 #[test]
477 fn test_emc_builder_complete_builds() {
478 let result = EmcBuilder::new()
479 .name("Little's Law")
480 .equation("L = \\lambda W")
481 .citation(Citation::new(
482 &["Little, J.D.C."],
483 "Operations Research",
484 1961,
485 ))
486 .add_variable("L", "wip", "items")
487 .add_variable("lambda", "arrival_rate", "items/time")
488 .add_variable("W", "cycle_time", "time")
489 .add_verification_test("L = λW for λ=5, W=2 => L=10", 10.0, 1e-10)
490 .build();
491
492 assert!(result.is_ok());
493 let emc = result.ok();
494 assert!(emc.is_some());
495 let emc = emc.unwrap();
496 assert_eq!(emc.name, "Little's Law");
497 assert_eq!(emc.variables.len(), 3);
498 assert_eq!(emc.verification_tests.len(), 1);
499 }
500
501 #[test]
502 fn test_emc_run_verification_tests() {
503 let emc = EmcBuilder::new()
504 .name("Test")
505 .equation("y = x")
506 .citation(Citation::new(&["Test"], "Test", 2024))
507 .add_verification_test_full(
508 VerificationTest::new("identity", 5.0, 0.1).with_input("x", 5.0),
509 )
510 .build()
511 .ok();
512
513 assert!(emc.is_some());
514 let emc = emc.unwrap();
515 let results = emc.run_verification_tests(|inputs| inputs.get("x").copied().unwrap_or(0.0));
516
517 assert_eq!(results.len(), 1);
518 assert!(results[0].1); }
520
521 #[test]
522 fn test_domain_constraint() {
523 let constraint = DomainConstraint {
524 name: "Utilization bound".to_string(),
525 expression: "0 < ρ < 1".to_string(),
526 rationale: "Utilization must be less than 100% for stable queue".to_string(),
527 violation_behavior: ViolationBehavior::Halt,
528 };
529
530 assert_eq!(constraint.violation_behavior, ViolationBehavior::Halt);
531 }
532
533 #[test]
534 fn test_falsification_criterion() {
535 let criterion = FalsificationCriterion {
536 name: "Little's Law".to_string(),
537 description: "L should equal λW within statistical tolerance".to_string(),
538 test_method: "Two-sample t-test".to_string(),
539 alpha: 0.05,
540 };
541
542 assert!((criterion.alpha - 0.05).abs() < f64::EPSILON);
543 }
544
545 #[test]
546 fn test_emc_verify_pass() {
547 let emc = EmcBuilder::new()
548 .name("Test")
549 .equation("y = x")
550 .citation(Citation::new(&["Test"], "Test", 2024))
551 .add_verification_test_full(
552 VerificationTest::new("identity", 5.0, 0.1).with_input("x", 5.0),
553 )
554 .build()
555 .ok()
556 .unwrap();
557
558 let result = emc.verify(|inputs| inputs.get("x").copied().unwrap_or(0.0));
559 assert!(result.is_ok());
560 }
561
562 #[test]
563 fn test_emc_verify_fail() {
564 let emc = EmcBuilder::new()
565 .name("Test")
566 .equation("y = 2*x")
567 .citation(Citation::new(&["Test"], "Test", 2024))
568 .add_verification_test_full(
569 VerificationTest::new("double", 10.0, 0.01).with_input("x", 5.0),
570 )
571 .build()
572 .ok()
573 .unwrap();
574
575 let result = emc.verify(|_inputs| 100.0);
577 assert!(result.is_err());
578 let failures = result.err().unwrap();
579 assert_eq!(failures.len(), 1);
580 assert!(failures[0].contains("FAIL"));
581 }
582
583 #[test]
584 fn test_emc_run_verification_tests_fail() {
585 let emc = EmcBuilder::new()
586 .name("Test")
587 .equation("y = x")
588 .citation(Citation::new(&["Test"], "Test", 2024))
589 .add_verification_test_full(
590 VerificationTest::new("identity", 5.0, 0.001).with_input("x", 5.0),
591 )
592 .build()
593 .ok()
594 .unwrap();
595
596 let results = emc.run_verification_tests(|_| 999.0); assert_eq!(results.len(), 1);
598 assert!(!results[0].1); assert!(results[0].2.contains("FAIL"));
600 }
601
602 #[test]
603 fn test_emc_builder_version() {
604 let emc = EmcBuilder::new()
605 .name("Test")
606 .equation("y = x")
607 .version("2.0.0")
608 .citation(Citation::new(&["Test"], "Test", 2024))
609 .add_verification_test("test", 1.0, 0.1)
610 .build()
611 .ok()
612 .unwrap();
613
614 assert_eq!(emc.version, "2.0.0");
615 }
616
617 #[test]
618 fn test_emc_builder_class() {
619 let emc = EmcBuilder::new()
620 .name("Test")
621 .equation("y = x")
622 .class(EquationClass::Optimization)
623 .citation(Citation::new(&["Test"], "Test", 2024))
624 .add_verification_test("test", 1.0, 0.1)
625 .build()
626 .ok()
627 .unwrap();
628
629 assert_eq!(emc.class, EquationClass::Optimization);
630 }
631
632 #[test]
633 fn test_emc_builder_add_reference() {
634 let emc = EmcBuilder::new()
635 .name("Test")
636 .equation("y = x")
637 .citation(Citation::new(&["Test"], "Test", 2024))
638 .add_reference(Citation::new(&["Other"], "Other Journal", 2020))
639 .add_verification_test("test", 1.0, 0.1)
640 .build()
641 .ok()
642 .unwrap();
643
644 assert_eq!(emc.references.len(), 1);
645 }
646
647 #[test]
648 fn test_emc_builder_add_variable_full() {
649 let var =
650 EquationVariable::new("x", "input", "units").with_description("Test input variable");
651
652 let emc = EmcBuilder::new()
653 .name("Test")
654 .equation("y = x")
655 .citation(Citation::new(&["Test"], "Test", 2024))
656 .add_variable_full(var)
657 .add_verification_test("test", 1.0, 0.1)
658 .build()
659 .ok()
660 .unwrap();
661
662 assert_eq!(emc.variables.len(), 1);
663 assert_eq!(emc.variables[0].description, "Test input variable");
664 }
665
666 #[test]
667 fn test_emc_builder_add_domain_constraint() {
668 let constraint = DomainConstraint {
669 name: "positivity".to_string(),
670 expression: "x > 0".to_string(),
671 rationale: "Must be positive".to_string(),
672 violation_behavior: ViolationBehavior::Warn,
673 };
674 let emc = EmcBuilder::new()
675 .name("Test")
676 .equation("y = x")
677 .citation(Citation::new(&["Test"], "Test", 2024))
678 .add_domain_constraint(constraint)
679 .add_verification_test("test", 1.0, 0.1)
680 .build()
681 .ok()
682 .unwrap();
683
684 assert_eq!(emc.domain_constraints.len(), 1);
685 assert_eq!(
686 emc.domain_constraints[0].violation_behavior,
687 ViolationBehavior::Warn
688 );
689 }
690
691 #[test]
692 fn test_emc_builder_add_falsification_criterion() {
693 let criterion = FalsificationCriterion {
694 name: "test".to_string(),
695 description: "description".to_string(),
696 test_method: "method".to_string(),
697 alpha: 0.01,
698 };
699 let emc = EmcBuilder::new()
700 .name("Test")
701 .equation("y = x")
702 .citation(Citation::new(&["Test"], "Test", 2024))
703 .add_falsification_criterion(criterion)
704 .add_verification_test("test", 1.0, 0.1)
705 .build()
706 .ok()
707 .unwrap();
708
709 assert_eq!(emc.falsification_criteria.len(), 1);
710 assert!((emc.falsification_criteria[0].alpha - 0.01).abs() < f64::EPSILON);
711 }
712
713 #[test]
714 fn test_emc_builder_add_implementation_note() {
715 let note = ImplementationNote {
716 topic: "Test Topic".to_string(),
717 content: "Important Note".to_string(),
718 importance: NoteImportance::Info,
719 };
720 let emc = EmcBuilder::new()
721 .name("Test")
722 .equation("y = x")
723 .citation(Citation::new(&["Test"], "Test", 2024))
724 .add_implementation_note(note)
725 .add_verification_test("test", 1.0, 0.1)
726 .build()
727 .ok()
728 .unwrap();
729
730 assert_eq!(emc.implementation_notes.len(), 1);
731 assert_eq!(emc.implementation_notes[0].content, "Important Note");
732 }
733
734 #[test]
735 fn test_emc_builder_description() {
736 let emc = EmcBuilder::new()
737 .name("Test")
738 .equation("y = x")
739 .citation(Citation::new(&["Test"], "Test", 2024))
740 .description("A test description")
741 .add_verification_test("test", 1.0, 0.1)
742 .build()
743 .ok()
744 .unwrap();
745
746 assert_eq!(emc.description, "A test description");
747 }
748
749 #[test]
750 fn test_emc_builder_add_lineage() {
751 let emc = EmcBuilder::new()
752 .name("Test")
753 .equation("y = x")
754 .citation(Citation::new(&["Test"], "Test", 2024))
755 .add_lineage("parent-emc")
756 .add_verification_test("test", 1.0, 0.1)
757 .build()
758 .ok()
759 .unwrap();
760
761 assert_eq!(emc.lineage.len(), 1);
762 assert_eq!(emc.lineage[0], "parent-emc");
763 }
764
765 #[test]
766 fn test_verification_test_builder() {
767 let test = VerificationTest::new("test desc", 10.0, 0.01)
768 .with_input("x", 5.0)
769 .with_input("y", 2.0);
770
771 assert_eq!(test.description, "test desc");
772 assert!((test.expected - 10.0).abs() < f64::EPSILON);
773 assert!((test.tolerance - 0.01).abs() < f64::EPSILON);
774 assert_eq!(test.inputs.len(), 2);
775 }
776
777 #[test]
778 fn test_implementation_note() {
779 let note = ImplementationNote {
780 topic: "Numerical".to_string(),
781 content: "Use Welford's algorithm for numerical stability".to_string(),
782 importance: NoteImportance::Important,
783 };
784 assert!(note.content.contains("Welford"));
785 assert_eq!(note.importance, NoteImportance::Important);
786 }
787
788 #[test]
789 fn test_note_importance_variants() {
790 assert_ne!(NoteImportance::Info, NoteImportance::Important);
791 assert_ne!(NoteImportance::Important, NoteImportance::Critical);
792 assert_ne!(NoteImportance::Critical, NoteImportance::Info);
793 }
794
795 #[test]
796 fn test_violation_behavior_variants() {
797 assert_ne!(ViolationBehavior::Halt, ViolationBehavior::Warn);
798 assert_ne!(ViolationBehavior::Warn, ViolationBehavior::Clamp);
799 assert_ne!(ViolationBehavior::Clamp, ViolationBehavior::Halt);
800 }
801
802 #[test]
803 fn test_equation_model_card_builder() {
804 let builder = EquationModelCard::builder();
805 assert!(builder.build().is_err());
806 }
807}