Skip to main content

simular/demos/
engine.rs

1//! Unified Demo Engine Trait
2//!
3//! Per specification SIMULAR-DEMO-001: All demos MUST implement `DemoEngine`.
4//! This trait ensures:
5//! - YAML-first configuration (single source of truth)
6//! - Deterministic replay (same seed → same output)
7//! - Renderer independence (TUI/WASM produce identical state sequences)
8//! - Metamorphic testing support
9//! - Falsification criteria evaluation
10//!
11//! # Peer-Reviewed Foundation
12//!
13//! - Chen et al. (2018): Metamorphic testing
14//! - Lavoie & Hendren (2015): Deterministic replay
15//! - Faulk et al. (2020): Property-based testing for scientific code
16
17use serde::{de::DeserializeOwned, Deserialize, Serialize};
18use std::fmt::Debug;
19use thiserror::Error;
20
21/// Errors that can occur in demo operations.
22#[derive(Debug, Error)]
23pub enum DemoError {
24    /// YAML parsing failed.
25    #[error("YAML parse error: {0}")]
26    YamlParse(#[from] serde_yaml::Error),
27
28    /// Configuration validation failed.
29    #[error("Validation error: {0}")]
30    Validation(String),
31
32    /// Schema validation failed.
33    #[error("Schema validation error: {0}")]
34    Schema(String),
35
36    /// Metamorphic relation verification failed.
37    #[error("Metamorphic relation {id} failed: {message}")]
38    MetamorphicFailure { id: String, message: String },
39
40    /// Invariant violation detected.
41    #[error("Invariant violation: {0}")]
42    InvariantViolation(String),
43
44    /// State serialization failed.
45    #[error("Serialization error: {0}")]
46    Serialization(String),
47}
48
49/// Severity levels for falsification criteria.
50#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "lowercase")]
52pub enum Severity {
53    /// Must pass - simulation is invalid if this fails.
54    Critical,
55    /// Should pass - indicates a problem but simulation continues.
56    #[default]
57    Major,
58    /// Nice to have - informational only.
59    Minor,
60}
61
62/// A single falsification criterion.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct FalsificationCriterion {
65    /// Unique criterion ID (e.g., "TSP-GAP-001").
66    pub id: String,
67
68    /// Human-readable name.
69    pub name: String,
70
71    /// Metric being evaluated.
72    pub metric: String,
73
74    /// Threshold value.
75    pub threshold: f64,
76
77    /// Condition expression (e.g., "gap <= threshold").
78    pub condition: String,
79
80    /// Tolerance for floating-point comparisons.
81    #[serde(default)]
82    pub tolerance: f64,
83
84    /// Severity level.
85    #[serde(default)]
86    pub severity: Severity,
87}
88
89/// Result of evaluating a falsification criterion.
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct CriterionResult {
92    /// Criterion ID.
93    pub id: String,
94
95    /// Whether the criterion passed.
96    pub passed: bool,
97
98    /// Actual value observed.
99    pub actual: f64,
100
101    /// Expected threshold.
102    pub expected: f64,
103
104    /// Human-readable message.
105    pub message: String,
106
107    /// Severity of this criterion.
108    pub severity: Severity,
109}
110
111/// A metamorphic relation for testing without oracles.
112///
113/// Per Chen et al. (2018): Metamorphic relations verify invariants
114/// without requiring knowledge of the correct output.
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct MetamorphicRelation {
117    /// Unique relation ID (e.g., "MR-PermutationInvariance").
118    pub id: String,
119
120    /// Human-readable description.
121    pub description: String,
122
123    /// Transform to apply to source input.
124    pub source_transform: String,
125
126    /// Expected relation between source and follow-up outputs.
127    pub expected_relation: String,
128
129    /// Tolerance for numerical comparisons.
130    #[serde(default = "default_tolerance")]
131    pub tolerance: f64,
132}
133
134fn default_tolerance() -> f64 {
135    1e-10
136}
137
138/// Result of verifying a metamorphic relation.
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct MrResult {
141    /// Relation ID.
142    pub id: String,
143
144    /// Whether the relation held.
145    pub passed: bool,
146
147    /// Detailed message.
148    pub message: String,
149
150    /// Source output value (if applicable).
151    pub source_value: Option<f64>,
152
153    /// Follow-up output value (if applicable).
154    pub followup_value: Option<f64>,
155}
156
157/// Demo metadata from YAML configuration.
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct DemoMeta {
160    /// Unique identifier (e.g., "TSP-BAY-020").
161    pub id: String,
162
163    /// Semantic version.
164    pub version: String,
165
166    /// Demo type (e.g., "tsp", "orbit", "monte\_carlo").
167    pub demo_type: String,
168
169    /// Human-readable description.
170    #[serde(default)]
171    pub description: String,
172
173    /// Author.
174    #[serde(default)]
175    pub author: String,
176
177    /// Creation date.
178    #[serde(default)]
179    pub created: String,
180}
181
182/// MANDATORY trait for ALL demos (EDD-compliant).
183///
184/// Per specification SIMULAR-DEMO-001, all demos must implement this trait
185/// to ensure:
186/// - YAML-first configuration
187/// - Deterministic replay
188/// - Renderer independence
189/// - Falsification support
190/// - Metamorphic testing
191///
192/// # Example
193///
194/// ```ignore
195/// impl DemoEngine for TspEngine {
196///     type Config = TspConfig;
197///     type State = TspState;
198///     type StepResult = TspStepResult;
199///
200///     fn from_yaml(yaml: &str) -> Result<Self, DemoError> {
201///         let config: TspConfig = serde_yaml::from_str(yaml)?;
202///         Ok(Self::from_config(config))
203///     }
204///     // ... other methods
205/// }
206/// ```
207pub trait DemoEngine: Sized + Clone {
208    /// Configuration type loaded from YAML.
209    type Config: DeserializeOwned + Debug;
210
211    /// State snapshot for replay/audit.
212    type State: Clone + Serialize + DeserializeOwned + PartialEq + Debug;
213
214    /// Result of a single step.
215    type StepResult: Debug;
216
217    // === Lifecycle ===
218
219    /// Create engine from YAML configuration string.
220    ///
221    /// # Errors
222    ///
223    /// Returns `DemoError::YamlParse` if YAML is invalid.
224    /// Returns `DemoError::Validation` if config fails validation.
225    fn from_yaml(yaml: &str) -> Result<Self, DemoError>;
226
227    /// Create engine from config struct.
228    fn from_config(config: Self::Config) -> Self;
229
230    /// Get the current configuration.
231    fn config(&self) -> &Self::Config;
232
233    /// Reset to initial state (same seed = same result).
234    fn reset(&mut self);
235
236    /// Reset with a new seed.
237    fn reset_with_seed(&mut self, seed: u64);
238
239    // === Execution ===
240
241    /// Execute one step (deterministic given state + seed).
242    fn step(&mut self) -> Self::StepResult;
243
244    /// Execute N steps.
245    fn run(&mut self, n: usize) -> Vec<Self::StepResult> {
246        (0..n).map(|_| self.step()).collect()
247    }
248
249    /// Check if simulation is complete/converged.
250    fn is_complete(&self) -> bool;
251
252    // === State Access ===
253
254    /// Get current state snapshot (for replay verification).
255    fn state(&self) -> Self::State;
256
257    /// Restore from a state snapshot.
258    fn restore(&mut self, state: &Self::State);
259
260    /// Get current step number.
261    fn step_count(&self) -> u64;
262
263    /// Get seed for reproducibility.
264    fn seed(&self) -> u64;
265
266    /// Get demo metadata.
267    fn meta(&self) -> &DemoMeta;
268
269    // === EDD Compliance ===
270
271    /// Get falsification criteria from config.
272    fn falsification_criteria(&self) -> Vec<FalsificationCriterion>;
273
274    /// Evaluate all criteria against current state.
275    fn evaluate_criteria(&self) -> Vec<CriterionResult>;
276
277    /// Check if all criteria pass.
278    fn is_verified(&self) -> bool {
279        self.evaluate_criteria()
280            .iter()
281            .filter(|r| r.severity == Severity::Critical)
282            .all(|r| r.passed)
283    }
284
285    // === Metamorphic Relations ===
286
287    /// Get metamorphic relations for this demo.
288    fn metamorphic_relations(&self) -> Vec<MetamorphicRelation>;
289
290    /// Verify a specific metamorphic relation.
291    fn verify_mr(&self, mr: &MetamorphicRelation) -> MrResult;
292
293    /// Verify all metamorphic relations.
294    fn verify_all_mrs(&self) -> Vec<MrResult> {
295        self.metamorphic_relations()
296            .iter()
297            .map(|mr| self.verify_mr(mr))
298            .collect()
299    }
300}
301
302/// Helper trait for demos that support deterministic replay.
303///
304/// Per Lavoie & Hendren (2015): Given identical configuration and seed,
305/// two independent runs must produce bit-identical state sequences.
306pub trait DeterministicReplay: DemoEngine {
307    /// Verify that two runs with same config produce identical results.
308    fn verify_determinism(&self, other: &Self) -> bool {
309        self.state() == other.state()
310    }
311
312    /// Get a checksum of the current state for quick comparison.
313    fn state_checksum(&self) -> u64;
314}
315
316/// Helper trait for demos with renderer-independent core logic.
317///
318/// The core engine MUST be renderer-agnostic. Both TUI and WASM:
319/// - Load from the SAME YAML
320/// - Use the SAME engine
321/// - Produce the SAME state sequence
322pub trait RendererIndependent: DemoEngine {
323    /// Render-independent data for current state.
324    type RenderData: Clone + Serialize;
325
326    /// Get data needed for rendering (without actually rendering).
327    fn render_data(&self) -> Self::RenderData;
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    #[test]
335    fn test_severity_default() {
336        assert_eq!(Severity::default(), Severity::Major);
337    }
338
339    #[test]
340    fn test_severity_serialization() {
341        let critical = Severity::Critical;
342        let json = serde_json::to_string(&critical).expect("serialize");
343        assert_eq!(json, "\"critical\"");
344
345        let deserialized: Severity = serde_json::from_str(&json).expect("deserialize");
346        assert_eq!(deserialized, Severity::Critical);
347    }
348
349    #[test]
350    fn test_demo_meta_serialization() {
351        let meta = DemoMeta {
352            id: "TEST-001".to_string(),
353            version: "1.0.0".to_string(),
354            demo_type: "test".to_string(),
355            description: "Test demo".to_string(),
356            author: "PAIML".to_string(),
357            created: "2025-12-12".to_string(),
358        };
359
360        let json = serde_json::to_string(&meta).expect("serialize");
361        assert!(json.contains("TEST-001"));
362        assert!(json.contains("1.0.0"));
363    }
364
365    #[test]
366    fn test_demo_meta_deserialization() {
367        let yaml = r#"
368id: "DEMO-001"
369version: "2.0.0"
370demo_type: "orbit"
371description: "Orbit demo"
372author: "Test"
373created: "2025-01-01"
374"#;
375        let meta: DemoMeta = serde_yaml::from_str(yaml).expect("deserialize");
376        assert_eq!(meta.id, "DEMO-001");
377        assert_eq!(meta.demo_type, "orbit");
378    }
379
380    #[test]
381    fn test_falsification_criterion_serialization() {
382        let criterion = FalsificationCriterion {
383            id: "GAP-001".to_string(),
384            name: "Optimality gap".to_string(),
385            metric: "gap".to_string(),
386            threshold: 0.20,
387            condition: "gap <= threshold".to_string(),
388            tolerance: 1e-6,
389            severity: Severity::Major,
390        };
391
392        let json = serde_json::to_string(&criterion).expect("serialize");
393        assert!(json.contains("GAP-001"));
394        assert!(json.contains("0.2"));
395    }
396
397    #[test]
398    fn test_criterion_result() {
399        let result = CriterionResult {
400            id: "TEST".to_string(),
401            passed: true,
402            actual: 0.15,
403            expected: 0.20,
404            message: "Passed".to_string(),
405            severity: Severity::Critical,
406        };
407
408        assert!(result.passed);
409        assert!(result.actual < result.expected);
410    }
411
412    #[test]
413    fn test_metamorphic_relation_default_tolerance() {
414        let yaml = r#"
415id: "MR-Test"
416description: "Test relation"
417source_transform: "identity"
418expected_relation: "unchanged"
419"#;
420        let mr: MetamorphicRelation = serde_yaml::from_str(yaml).expect("deserialize");
421        assert!((mr.tolerance - 1e-10).abs() < 1e-15);
422    }
423
424    #[test]
425    fn test_mr_result() {
426        let result = MrResult {
427            id: "MR-Energy".to_string(),
428            passed: true,
429            message: "Energy conserved".to_string(),
430            source_value: Some(1000.0),
431            followup_value: Some(1000.0),
432        };
433
434        assert!(result.passed);
435        assert_eq!(result.source_value, result.followup_value);
436    }
437
438    #[test]
439    fn test_demo_error_display() {
440        let err = DemoError::Validation("invalid config".to_string());
441        let msg = format!("{err}");
442        assert!(msg.contains("Validation error"));
443        assert!(msg.contains("invalid config"));
444    }
445
446    #[test]
447    fn test_demo_error_metamorphic() {
448        let err = DemoError::MetamorphicFailure {
449            id: "MR-001".to_string(),
450            message: "Invariant broken".to_string(),
451        };
452        let msg = format!("{err}");
453        assert!(msg.contains("MR-001"));
454        assert!(msg.contains("Invariant broken"));
455    }
456
457    #[test]
458    fn test_demo_error_from_yaml() {
459        let bad_yaml = "{{{{not valid yaml";
460        let result: Result<DemoMeta, _> = serde_yaml::from_str(bad_yaml);
461        assert!(result.is_err());
462    }
463
464    #[test]
465    fn test_demo_error_validation() {
466        let err = DemoError::Validation("config invalid".to_string());
467        let msg = format!("{err}");
468        assert!(msg.contains("Validation error"));
469        assert!(msg.contains("config invalid"));
470    }
471
472    #[test]
473    fn test_demo_error_schema() {
474        let err = DemoError::Schema("schema mismatch".to_string());
475        let msg = format!("{err}");
476        assert!(msg.contains("Schema validation error"));
477    }
478
479    #[test]
480    fn test_demo_error_invariant_violation() {
481        let err = DemoError::InvariantViolation("state corrupted".to_string());
482        let msg = format!("{err}");
483        assert!(msg.contains("Invariant violation"));
484    }
485
486    #[test]
487    fn test_demo_error_serialization() {
488        let err = DemoError::Serialization("failed to serialize".to_string());
489        let msg = format!("{err}");
490        assert!(msg.contains("Serialization error"));
491    }
492
493    #[test]
494    fn test_severity_minor_serialization() {
495        let minor = Severity::Minor;
496        let json = serde_json::to_string(&minor).expect("serialize");
497        assert_eq!(json, "\"minor\"");
498
499        let deserialized: Severity = serde_json::from_str(&json).expect("deserialize");
500        assert_eq!(deserialized, Severity::Minor);
501    }
502
503    #[test]
504    fn test_severity_all_variants() {
505        let severities = [Severity::Critical, Severity::Major, Severity::Minor];
506        for sev in severities {
507            let json = serde_json::to_string(&sev).expect("serialize");
508            let deserialized: Severity = serde_json::from_str(&json).expect("deserialize");
509            assert_eq!(sev, deserialized);
510        }
511    }
512
513    #[test]
514    fn test_falsification_criterion_deserialization() {
515        let yaml = r#"
516id: "CRIT-001"
517name: "Test Criterion"
518metric: "accuracy"
519threshold: 0.95
520condition: "accuracy >= threshold"
521tolerance: 0.001
522severity: "critical"
523"#;
524        let criterion: FalsificationCriterion = serde_yaml::from_str(yaml).expect("deserialize");
525        assert_eq!(criterion.id, "CRIT-001");
526        assert_eq!(criterion.severity, Severity::Critical);
527        assert!((criterion.tolerance - 0.001).abs() < 1e-10);
528    }
529
530    #[test]
531    fn test_falsification_criterion_defaults() {
532        let yaml = r#"
533id: "CRIT-002"
534name: "Minimal"
535metric: "val"
536threshold: 1.0
537condition: "val > 0"
538"#;
539        let criterion: FalsificationCriterion = serde_yaml::from_str(yaml).expect("deserialize");
540        assert_eq!(criterion.severity, Severity::Major); // default
541        assert!((criterion.tolerance - 0.0).abs() < 1e-15); // default
542    }
543
544    #[test]
545    fn test_criterion_result_serialization() {
546        let result = CriterionResult {
547            id: "RES-001".to_string(),
548            passed: false,
549            actual: 0.85,
550            expected: 0.95,
551            message: "Below threshold".to_string(),
552            severity: Severity::Critical,
553        };
554
555        let json = serde_json::to_string(&result).expect("serialize");
556        assert!(json.contains("RES-001"));
557        assert!(json.contains("false"));
558        assert!(json.contains("0.85"));
559    }
560
561    #[test]
562    fn test_metamorphic_relation_with_tolerance() {
563        let yaml = r#"
564id: "MR-Energy"
565description: "Energy conservation"
566source_transform: "time_reverse"
567expected_relation: "energy_unchanged"
568tolerance: 1e-6
569"#;
570        let mr: MetamorphicRelation = serde_yaml::from_str(yaml).expect("deserialize");
571        assert_eq!(mr.id, "MR-Energy");
572        assert!((mr.tolerance - 1e-6).abs() < 1e-15);
573    }
574
575    #[test]
576    fn test_mr_result_with_none_values() {
577        let result = MrResult {
578            id: "MR-None".to_string(),
579            passed: true,
580            message: "No comparison needed".to_string(),
581            source_value: None,
582            followup_value: None,
583        };
584
585        assert!(result.passed);
586        assert!(result.source_value.is_none());
587        assert!(result.followup_value.is_none());
588    }
589
590    #[test]
591    fn test_mr_result_serialization() {
592        let result = MrResult {
593            id: "MR-Serialize".to_string(),
594            passed: false,
595            message: "Values diverged".to_string(),
596            source_value: Some(100.0),
597            followup_value: Some(101.0),
598        };
599
600        let json = serde_json::to_string(&result).expect("serialize");
601        assert!(json.contains("MR-Serialize"));
602        assert!(json.contains("100"));
603        assert!(json.contains("101"));
604    }
605
606    #[test]
607    fn test_demo_meta_with_defaults() {
608        let yaml = r#"
609id: "MIN-001"
610version: "0.1.0"
611demo_type: "test"
612"#;
613        let meta: DemoMeta = serde_yaml::from_str(yaml).expect("deserialize");
614        assert_eq!(meta.id, "MIN-001");
615        assert!(meta.description.is_empty());
616        assert!(meta.author.is_empty());
617        assert!(meta.created.is_empty());
618    }
619
620    #[test]
621    fn test_demo_meta_roundtrip() {
622        let meta = DemoMeta {
623            id: "ROUND-001".to_string(),
624            version: "1.2.3".to_string(),
625            demo_type: "orbit".to_string(),
626            description: "Round trip test".to_string(),
627            author: "Test Author".to_string(),
628            created: "2025-12-12".to_string(),
629        };
630
631        let yaml = serde_yaml::to_string(&meta).expect("serialize");
632        let restored: DemoMeta = serde_yaml::from_str(&yaml).expect("deserialize");
633
634        assert_eq!(meta.id, restored.id);
635        assert_eq!(meta.version, restored.version);
636        assert_eq!(meta.demo_type, restored.demo_type);
637        assert_eq!(meta.description, restored.description);
638    }
639
640    #[test]
641    fn test_criterion_result_deserialization() {
642        let json = r#"{
643            "id": "DES-001",
644            "passed": true,
645            "actual": 0.99,
646            "expected": 0.95,
647            "message": "Exceeded threshold",
648            "severity": "minor"
649        }"#;
650
651        let result: CriterionResult = serde_json::from_str(json).expect("deserialize");
652        assert_eq!(result.id, "DES-001");
653        assert!(result.passed);
654        assert_eq!(result.severity, Severity::Minor);
655    }
656
657    #[test]
658    fn test_metamorphic_relation_serialization() {
659        let mr = MetamorphicRelation {
660            id: "MR-Serial".to_string(),
661            description: "Test MR".to_string(),
662            source_transform: "identity".to_string(),
663            expected_relation: "equal".to_string(),
664            tolerance: 1e-8,
665        };
666
667        let yaml = serde_yaml::to_string(&mr).expect("serialize");
668        assert!(yaml.contains("MR-Serial"));
669        assert!(yaml.contains("identity"));
670    }
671}