Skip to main content

organism_simulation/
lib.rs

1//! Simulation swarm.
2//!
3//! Parallel stress-testing of candidate plans before commit. Multiple
4//! simulators run concurrently across five dimensions: outcome, cost,
5//! policy, causal, operational. Each returns probability distributions,
6//! not point estimates.
7//!
8//! Mirrors validation patterns from aircraft design, trading systems,
9//! and chip design.
10
11pub mod causal;
12pub mod cost;
13pub mod operational;
14pub mod outcome;
15pub mod policy;
16pub mod types;
17
18use serde::{Deserialize, Serialize};
19use uuid::Uuid;
20
21pub use causal::{CausalSimulationAgent, CausalSimulator, CausalSimulatorConfig};
22pub use cost::{CostSimulationAgent, CostSimulator, CostSimulatorConfig};
23pub use operational::{
24    OperationalSimulationAgent, OperationalSimulator, OperationalSimulatorConfig,
25};
26pub use outcome::{OutcomeSimulationAgent, OutcomeSimulator, OutcomeSimulatorConfig};
27pub use policy::{PolicySimulationAgent, PolicySimulator, PolicySimulatorConfig};
28pub use types::{RiskLikelihood, SimulationVerdict};
29
30// ── Simulation Result ──────────────────────────────────────────────
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct SimulationResult {
34    pub plan_id: Uuid,
35    pub runs: u32,
36    pub dimensions: Vec<DimensionResult>,
37    pub overall_confidence: f64,
38    pub recommendation: SimulationRecommendation,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct DimensionResult {
43    pub dimension: SimulationDimension,
44    pub passed: bool,
45    pub confidence: f64,
46    pub findings: Vec<String>,
47    pub samples: Vec<Sample>,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "snake_case")]
52pub enum SimulationDimension {
53    Outcome,
54    Cost,
55    Policy,
56    Causal,
57    Operational,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum SimulationRecommendation {
63    Proceed,
64    ProceedWithCaution,
65    DoNotProceed,
66}
67
68// ── Sample ─────────────────────────────────────────────────────────
69
70#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
71pub struct Sample {
72    pub value: f64,
73    pub probability: f64,
74}
75
76// ── Simulation Report (legacy compat) ──────────────────────────────
77
78#[derive(Debug, Clone, Default, Serialize, Deserialize)]
79pub struct SimulationReport {
80    pub results: Vec<SimulationResult>,
81}
82
83// Simulation agents are Suggestors — see OutcomeSimulationAgent.
84// No separate trait needed; the convergence loop IS the execution model.
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use proptest::prelude::*;
90
91    fn plan_id() -> Uuid {
92        Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap()
93    }
94
95    fn sample_dimension_result(dim: SimulationDimension, passed: bool) -> DimensionResult {
96        DimensionResult {
97            dimension: dim,
98            passed,
99            confidence: 0.85,
100            findings: vec!["ok".into()],
101            samples: vec![Sample {
102                value: 1.0,
103                probability: 0.9,
104            }],
105        }
106    }
107
108    #[test]
109    fn simulation_dimension_all_variants_distinct() {
110        let variants = [
111            SimulationDimension::Outcome,
112            SimulationDimension::Cost,
113            SimulationDimension::Policy,
114            SimulationDimension::Causal,
115            SimulationDimension::Operational,
116        ];
117        for (i, a) in variants.iter().enumerate() {
118            for (j, b) in variants.iter().enumerate() {
119                assert_eq!(i == j, a == b);
120            }
121        }
122    }
123
124    #[test]
125    fn recommendation_all_variants_distinct() {
126        let variants = [
127            SimulationRecommendation::Proceed,
128            SimulationRecommendation::ProceedWithCaution,
129            SimulationRecommendation::DoNotProceed,
130        ];
131        for (i, a) in variants.iter().enumerate() {
132            for (j, b) in variants.iter().enumerate() {
133                assert_eq!(i == j, a == b);
134            }
135        }
136    }
137
138    #[test]
139    fn simulation_dimension_serde_snake_case() {
140        let json = serde_json::to_string(&SimulationDimension::Outcome).unwrap();
141        assert_eq!(json, "\"outcome\"");
142        let json = serde_json::to_string(&SimulationDimension::Cost).unwrap();
143        assert_eq!(json, "\"cost\"");
144        let json = serde_json::to_string(&SimulationDimension::Operational).unwrap();
145        assert_eq!(json, "\"operational\"");
146    }
147
148    #[test]
149    fn recommendation_serde_snake_case() {
150        let json = serde_json::to_string(&SimulationRecommendation::ProceedWithCaution).unwrap();
151        assert_eq!(json, "\"proceed_with_caution\"");
152        let json = serde_json::to_string(&SimulationRecommendation::DoNotProceed).unwrap();
153        assert_eq!(json, "\"do_not_proceed\"");
154    }
155
156    #[test]
157    fn sample_serde_roundtrip() {
158        let s = Sample {
159            value: 42.5,
160            probability: 0.73,
161        };
162        let json = serde_json::to_string(&s).unwrap();
163        let back: Sample = serde_json::from_str(&json).unwrap();
164        assert!((back.value - 42.5).abs() < f64::EPSILON);
165        assert!((back.probability - 0.73).abs() < f64::EPSILON);
166    }
167
168    #[test]
169    fn dimension_result_serde_roundtrip() {
170        let dr = sample_dimension_result(SimulationDimension::Cost, true);
171        let json = serde_json::to_string(&dr).unwrap();
172        let back: DimensionResult = serde_json::from_str(&json).unwrap();
173        assert_eq!(back.dimension, SimulationDimension::Cost);
174        assert!(back.passed);
175        assert_eq!(back.findings.len(), 1);
176        assert_eq!(back.samples.len(), 1);
177    }
178
179    #[test]
180    fn dimension_result_empty_findings_and_samples() {
181        let dr = DimensionResult {
182            dimension: SimulationDimension::Policy,
183            passed: false,
184            confidence: 0.0,
185            findings: vec![],
186            samples: vec![],
187        };
188        let json = serde_json::to_string(&dr).unwrap();
189        let back: DimensionResult = serde_json::from_str(&json).unwrap();
190        assert!(back.findings.is_empty());
191        assert!(back.samples.is_empty());
192    }
193
194    #[test]
195    fn simulation_result_serde_roundtrip() {
196        let sr = SimulationResult {
197            plan_id: plan_id(),
198            runs: 1000,
199            dimensions: vec![
200                sample_dimension_result(SimulationDimension::Outcome, true),
201                sample_dimension_result(SimulationDimension::Cost, false),
202            ],
203            overall_confidence: 0.72,
204            recommendation: SimulationRecommendation::ProceedWithCaution,
205        };
206        let json = serde_json::to_string(&sr).unwrap();
207        let back: SimulationResult = serde_json::from_str(&json).unwrap();
208        assert_eq!(back.plan_id, plan_id());
209        assert_eq!(back.runs, 1000);
210        assert_eq!(back.dimensions.len(), 2);
211        assert_eq!(
212            back.recommendation,
213            SimulationRecommendation::ProceedWithCaution
214        );
215    }
216
217    #[test]
218    fn simulation_result_zero_runs() {
219        let sr = SimulationResult {
220            plan_id: plan_id(),
221            runs: 0,
222            dimensions: vec![],
223            overall_confidence: 0.0,
224            recommendation: SimulationRecommendation::DoNotProceed,
225        };
226        let json = serde_json::to_string(&sr).unwrap();
227        let back: SimulationResult = serde_json::from_str(&json).unwrap();
228        assert_eq!(back.runs, 0);
229        assert!(back.dimensions.is_empty());
230    }
231
232    #[test]
233    fn simulation_report_default_is_empty() {
234        let report = SimulationReport::default();
235        assert!(report.results.is_empty());
236    }
237
238    #[test]
239    fn simulation_report_serde_roundtrip() {
240        let report = SimulationReport {
241            results: vec![SimulationResult {
242                plan_id: plan_id(),
243                runs: 50,
244                dimensions: vec![],
245                overall_confidence: 0.5,
246                recommendation: SimulationRecommendation::Proceed,
247            }],
248        };
249        let json = serde_json::to_string(&report).unwrap();
250        let back: SimulationReport = serde_json::from_str(&json).unwrap();
251        assert_eq!(back.results.len(), 1);
252        assert_eq!(back.results[0].runs, 50);
253    }
254
255    proptest! {
256        #[test]
257        fn sample_roundtrips_reasonable_values(
258            value in -1e15f64..1e15f64,
259            probability in 0.0f64..=1.0,
260        ) {
261            let s = Sample { value, probability };
262            let json = serde_json::to_string(&s).unwrap();
263            let back: Sample = serde_json::from_str(&json).unwrap();
264            prop_assert!((back.value - value).abs() < 1e-6 * value.abs().max(1.0));
265            prop_assert!((back.probability - probability).abs() < f64::EPSILON);
266        }
267
268        #[test]
269        fn confidence_survives_roundtrip(conf in 0.0f64..=1.0) {
270            let sr = SimulationResult {
271                plan_id: plan_id(),
272                runs: 1,
273                dimensions: vec![],
274                overall_confidence: conf,
275                recommendation: SimulationRecommendation::Proceed,
276            };
277            let json = serde_json::to_string(&sr).unwrap();
278            let back: SimulationResult = serde_json::from_str(&json).unwrap();
279            prop_assert!((back.overall_confidence - conf).abs() < f64::EPSILON);
280        }
281    }
282}