1use serde::Serialize;
2
3use crate::causal::{
4 build_causal_graph, summarize_causal_graph, CausalGraph, CausalMetricsSummary,
5};
6use crate::config::SimulationConfig;
7use crate::metrics::{
8 build_prediction_tubes, notebook_ready_summary, summarize_scenario, NotebookReadySummary,
9 PredictionTubePoint, ScenarioSummaryRow,
10};
11use crate::observer::{simulate_observers, ObserverSeries};
12use crate::scenario::{scenario_suite, ScenarioDefinition};
13use crate::tmtr::{apply_tmtr, CorrectionEvent, RecursionStats};
14
15#[derive(Debug, Clone, Serialize)]
16pub struct ModeArtifacts {
17 pub mode: String,
18 pub observers: Vec<ObserverSeries>,
19 pub correction_events: Vec<CorrectionEvent>,
20 pub prediction_tubes: Vec<PredictionTubePoint>,
21 pub causal_graph: CausalGraph,
22 pub causal_metrics: CausalMetricsSummary,
23 pub recursion_stats: RecursionStats,
24}
25
26#[derive(Debug, Clone, Serialize)]
27pub struct ScenarioArtifacts {
28 pub definition: ScenarioDefinition,
29 pub truth: Vec<f64>,
30 pub baseline: ModeArtifacts,
31 pub tmtr: ModeArtifacts,
32 pub summary: ScenarioSummaryRow,
33}
34
35#[derive(Debug, Clone, Serialize)]
36pub struct SimulationRun {
37 pub config: SimulationConfig,
38 pub config_hash: String,
39 pub scenarios: Vec<ScenarioArtifacts>,
40 pub notebook_summary: NotebookReadySummary,
41}
42
43impl SimulationRun {
44 pub fn stable_signature(&self) -> anyhow::Result<String> {
45 #[derive(Serialize)]
46 struct StableView<'a> {
47 config: &'a SimulationConfig,
48 config_hash: &'a str,
49 scenarios: &'a [ScenarioArtifacts],
50 }
51 Ok(serde_json::to_string(&StableView {
52 config: &self.config,
53 config_hash: &self.config_hash,
54 scenarios: &self.scenarios,
55 })?)
56 }
57}
58
59pub fn run_simulation(config: &SimulationConfig) -> anyhow::Result<SimulationRun> {
60 let scenarios = scenario_suite(config)
61 .into_iter()
62 .map(|definition| run_single_scenario(config, definition))
63 .collect::<anyhow::Result<Vec<_>>>()?;
64 let notebook_summary =
65 notebook_ready_summary(&config.output_root, &collect_summaries(&scenarios));
66 Ok(SimulationRun {
67 config: config.clone(),
68 config_hash: config.stable_hash()?,
69 scenarios,
70 notebook_summary,
71 })
72}
73
74fn run_single_scenario(
75 config: &SimulationConfig,
76 definition: ScenarioDefinition,
77) -> anyhow::Result<ScenarioArtifacts> {
78 let truth = definition.truth_series();
79 let (specs, baseline_observers) = simulate_observers(&definition, &truth);
80 let baseline_primary = baseline_observers[0].clone();
81 let baseline_tubes = build_prediction_tubes(&definition, "baseline", &baseline_primary, &truth);
82 let baseline_graph = build_causal_graph(
83 &definition.name,
84 "baseline",
85 &baseline_observers,
86 &[],
87 config.min_trust_gap,
88 );
89 let baseline_causal = summarize_causal_graph(&baseline_graph, definition.delta);
90
91 let tmtr_result = apply_tmtr(&definition, config, &specs, &baseline_observers, &truth);
92 let tmtr_primary = tmtr_result.observers[0].clone();
93 let tmtr_tubes = build_prediction_tubes(&definition, "tmtr", &tmtr_primary, &truth);
94 let tmtr_graph = build_causal_graph(
95 &definition.name,
96 "tmtr",
97 &tmtr_result.observers,
98 &tmtr_result.correction_events,
99 config.min_trust_gap,
100 );
101 let tmtr_causal = summarize_causal_graph(&tmtr_graph, definition.delta);
102
103 let summary = summarize_scenario(
104 &definition,
105 &baseline_primary,
106 &tmtr_primary,
107 &baseline_tubes,
108 &tmtr_tubes,
109 &baseline_causal,
110 &tmtr_causal,
111 &tmtr_result.recursion_stats,
112 );
113
114 Ok(ScenarioArtifacts {
115 definition,
116 truth,
117 baseline: ModeArtifacts {
118 mode: "baseline".to_string(),
119 observers: baseline_observers,
120 correction_events: Vec::new(),
121 prediction_tubes: baseline_tubes,
122 causal_graph: baseline_graph,
123 causal_metrics: baseline_causal,
124 recursion_stats: RecursionStats {
125 total_correction_events: 0,
126 max_recursion_depth: 0,
127 mean_recursion_depth: 0.0,
128 convergence_iterations: 0,
129 average_correction_magnitude: 0.0,
130 average_correction_trust_weight: 0.0,
131 monotonicity_violations: 0,
132 },
133 },
134 tmtr: ModeArtifacts {
135 mode: "tmtr".to_string(),
136 observers: tmtr_result.observers,
137 correction_events: tmtr_result.correction_events,
138 prediction_tubes: tmtr_tubes,
139 causal_graph: tmtr_graph,
140 causal_metrics: tmtr_causal,
141 recursion_stats: tmtr_result.recursion_stats,
142 },
143 summary,
144 })
145}
146
147fn collect_summaries(scenarios: &[ScenarioArtifacts]) -> Vec<ScenarioSummaryRow> {
148 scenarios
149 .iter()
150 .map(|scenario| scenario.summary.clone())
151 .collect()
152}
153
154#[cfg(test)]
155mod tests {
156 use crate::config::SimulationConfig;
157 use crate::simulation::run_simulation;
158
159 #[test]
160 fn deterministic_signature_is_stable() {
161 let config = SimulationConfig {
162 n_steps: 240,
163 ..SimulationConfig::default()
164 };
165 let first = run_simulation(&config).expect("first simulation");
166 let second = run_simulation(&config).expect("second simulation");
167 assert_eq!(first.config_hash, second.config_hash);
168 assert_eq!(
169 first.stable_signature().expect("first signature"),
170 second.stable_signature().expect("second signature")
171 );
172 }
173}