Skip to main content

dsfb_srd/
experiments.rs

1use std::cmp::Ordering;
2use std::path::PathBuf;
3
4use crate::config::{compute_run_id, SimulationConfig, CRATE_NAME, CRATE_VERSION};
5use crate::export::{
6    prepare_output_dir, write_bundle, DynError, ExportBundle, GraphSnapshotRow, RunManifestRow,
7    ThresholdSweepRow, TimeLocalMetricsRow, TransitionSharpnessRow,
8};
9use crate::graph::{
10    build_candidate_graph, collect_active_edges, compute_graph_stats, compute_graph_stats_in_range,
11    CandidateGraph,
12};
13use crate::metrics::{
14    discrete_derivative, low_high_thresholds, nearest_threshold, window_ranges, window_regime,
15};
16use crate::signal::generate_events;
17
18const FINITE_SIZE_SWEEP: [usize; 4] = [250, 500, 1_000, 2_000];
19const SNAPSHOT_TAU_020: f64 = 0.20;
20const SNAPSHOT_TAU_030: f64 = 0.30;
21const SNAPSHOT_TAU_040: f64 = 0.40;
22
23#[derive(Clone, Debug)]
24pub struct GeneratedRun {
25    pub run_id: String,
26    pub config_hash: String,
27    pub timestamp: String,
28    pub output_dir: PathBuf,
29}
30
31pub fn run_simulation(config: SimulationConfig) -> Result<GeneratedRun, DynError> {
32    config
33        .validate()
34        .map_err(|message| -> DynError { message.into() })?;
35
36    let run_id = compute_run_id(&config);
37    let config_hash = config.config_hash();
38    let primary_events = generate_events(&config);
39    let primary_graph = build_candidate_graph(&primary_events, &config);
40    let thresholds = config.tau_thresholds();
41    let (tau_low, tau_high) = low_high_thresholds(&thresholds);
42    let tau_020 = nearest_threshold(&thresholds, SNAPSHOT_TAU_020);
43    let tau_030 = nearest_threshold(&thresholds, SNAPSHOT_TAU_030);
44    let tau_040 = nearest_threshold(&thresholds, SNAPSHOT_TAU_040);
45
46    let mut sweep_sizes = FINITE_SIZE_SWEEP.to_vec();
47    if !sweep_sizes.contains(&config.n_events) {
48        sweep_sizes.push(config.n_events);
49        sweep_sizes.sort_unstable();
50    }
51
52    let mut threshold_sweep = Vec::new();
53    let mut transition_sharpness = Vec::new();
54    let mut primary_critical_threshold = thresholds[thresholds.len() / 2];
55
56    for &n_events in &sweep_sizes {
57        let sized_config = if n_events == config.n_events {
58            config.clone()
59        } else {
60            config.scaled_for_n_events(n_events)
61        };
62        sized_config
63            .validate()
64            .map_err(|message| -> DynError { message.into() })?;
65
66        let (events, graph) = if n_events == config.n_events {
67            (primary_events.clone(), primary_graph.clone())
68        } else {
69            let events = generate_events(&sized_config);
70            let graph = build_candidate_graph(&events, &sized_config);
71            (events, graph)
72        };
73
74        let sweep_rows = build_threshold_sweep_rows(&run_id, &thresholds, &events, &graph);
75        let transition_rows = build_transition_rows(&run_id, n_events, &sweep_rows);
76
77        if n_events == config.n_events {
78            primary_critical_threshold =
79                select_critical_threshold(&transition_rows, primary_critical_threshold);
80        }
81
82        threshold_sweep.extend(sweep_rows);
83        transition_sharpness.extend(transition_rows);
84    }
85
86    let time_local_metrics = build_time_local_rows(
87        &run_id,
88        &config,
89        &primary_events,
90        &primary_graph,
91        &[tau_low, primary_critical_threshold, tau_high],
92    );
93    let graph_snapshot_low =
94        build_graph_snapshot_rows(&run_id, tau_low, &primary_events, &primary_graph);
95    let graph_snapshot_critical = build_graph_snapshot_rows(
96        &run_id,
97        primary_critical_threshold,
98        &primary_events,
99        &primary_graph,
100    );
101    let graph_snapshot_high =
102        build_graph_snapshot_rows(&run_id, tau_high, &primary_events, &primary_graph);
103    let graph_snapshot_tau_020 =
104        build_graph_snapshot_rows(&run_id, tau_020, &primary_events, &primary_graph);
105    let graph_snapshot_tau_030 =
106        build_graph_snapshot_rows(&run_id, tau_030, &primary_events, &primary_graph);
107    let graph_snapshot_tau_040 =
108        build_graph_snapshot_rows(&run_id, tau_040, &primary_events, &primary_graph);
109
110    let output = prepare_output_dir(&SimulationConfig::repo_root())?;
111    let manifest = RunManifestRow {
112        run_id: run_id.clone(),
113        timestamp: output.timestamp.clone(),
114        config_hash: config_hash.clone(),
115        crate_name: CRATE_NAME.to_string(),
116        crate_version: CRATE_VERSION.to_string(),
117        n_events: config.n_events,
118        n_channels: config.n_channels,
119        causal_window: config.causal_window,
120        tau_steps: config.tau_steps,
121        shock_start: config.shock_start,
122        shock_end: config.shock_end,
123        beta: config.beta,
124        envelope_decay: config.envelope_decay,
125    };
126
127    let bundle = ExportBundle {
128        manifest,
129        events: primary_events,
130        threshold_sweep,
131        transition_sharpness,
132        time_local_metrics,
133        graph_snapshot_low,
134        graph_snapshot_critical,
135        graph_snapshot_high,
136        graph_snapshot_tau_020,
137        graph_snapshot_tau_030,
138        graph_snapshot_tau_040,
139    };
140
141    write_bundle(&bundle, &output.run_dir)?;
142
143    Ok(GeneratedRun {
144        run_id,
145        config_hash,
146        timestamp: output.timestamp,
147        output_dir: output.run_dir,
148    })
149}
150
151fn build_threshold_sweep_rows(
152    run_id: &str,
153    thresholds: &[f64],
154    events: &[crate::event::StructuralEvent],
155    graph: &CandidateGraph,
156) -> Vec<ThresholdSweepRow> {
157    thresholds
158        .iter()
159        .copied()
160        .map(|tau_threshold| {
161            let stats = compute_graph_stats(graph, events, tau_threshold, 0);
162            ThresholdSweepRow {
163                run_id: run_id.to_string(),
164                n_events: events.len(),
165                tau_threshold,
166                reachable_count: stats.reachable_count,
167                reachable_fraction: stats.reachable_count as f64 / events.len() as f64,
168                edge_count: stats.edge_count,
169                mean_out_degree: stats.mean_out_degree,
170                largest_component_fraction: stats.largest_component_fraction,
171                component_entropy: stats.component_entropy,
172            }
173        })
174        .collect()
175}
176
177fn build_transition_rows(
178    run_id: &str,
179    n_events: usize,
180    sweep_rows: &[ThresholdSweepRow],
181) -> Vec<TransitionSharpnessRow> {
182    let points: Vec<(f64, f64)> = sweep_rows
183        .iter()
184        .map(|row| (row.tau_threshold, row.reachable_fraction))
185        .collect();
186
187    discrete_derivative(&points)
188        .into_iter()
189        .map(
190            |(tau_midpoint, drho_dtau, abs_drho_dtau)| TransitionSharpnessRow {
191                run_id: run_id.to_string(),
192                n_events,
193                tau_midpoint,
194                drho_dtau,
195                abs_drho_dtau,
196            },
197        )
198        .collect()
199}
200
201fn select_critical_threshold(rows: &[TransitionSharpnessRow], fallback: f64) -> f64 {
202    rows.iter()
203        .max_by(|left, right| {
204            left.abs_drho_dtau
205                .partial_cmp(&right.abs_drho_dtau)
206                .unwrap_or(Ordering::Equal)
207        })
208        .map(|row| row.tau_midpoint)
209        .unwrap_or(fallback)
210}
211
212fn build_time_local_rows(
213    run_id: &str,
214    config: &SimulationConfig,
215    events: &[crate::event::StructuralEvent],
216    graph: &CandidateGraph,
217    thresholds: &[f64],
218) -> Vec<TimeLocalMetricsRow> {
219    let mut rows = Vec::new();
220
221    for &tau_threshold in thresholds {
222        for (window_start, window_end_exclusive) in window_ranges(config) {
223            let anchor_event = window_start;
224            let stats = compute_graph_stats_in_range(
225                graph,
226                events,
227                tau_threshold,
228                anchor_event,
229                (window_start, window_end_exclusive),
230            );
231            let window_len = window_end_exclusive - window_start;
232            rows.push(TimeLocalMetricsRow {
233                run_id: run_id.to_string(),
234                tau_threshold,
235                window_start,
236                window_end: window_end_exclusive - 1,
237                anchor_event,
238                reachable_fraction: stats.reachable_count as f64 / window_len as f64,
239                active_edge_count: stats.edge_count,
240                mean_out_degree: stats.mean_out_degree,
241                regime_label: window_regime(events, window_start, window_end_exclusive)
242                    .as_str()
243                    .to_string(),
244            });
245        }
246    }
247
248    rows
249}
250
251fn build_graph_snapshot_rows(
252    run_id: &str,
253    tau_threshold: f64,
254    events: &[crate::event::StructuralEvent],
255    graph: &CandidateGraph,
256) -> Vec<GraphSnapshotRow> {
257    collect_active_edges(graph, events, tau_threshold, None)
258        .into_iter()
259        .map(|edge| GraphSnapshotRow {
260            run_id: run_id.to_string(),
261            tau_threshold,
262            src: edge.src,
263            dst: edge.dst,
264            src_trust: events[edge.src].trust,
265            dst_trust: events[edge.dst].trust,
266            compatible: edge.compatible,
267        })
268        .collect()
269}