dsfb-srd 0.1.0

Deterministic Structural Regime Dynamics demonstrator for trust-gated causal topology sweeps
Documentation
use std::cmp::Ordering;
use std::path::PathBuf;

use crate::config::{compute_run_id, SimulationConfig, CRATE_NAME, CRATE_VERSION};
use crate::export::{
    prepare_output_dir, write_bundle, DynError, ExportBundle, GraphSnapshotRow, RunManifestRow,
    ThresholdSweepRow, TimeLocalMetricsRow, TransitionSharpnessRow,
};
use crate::graph::{
    build_candidate_graph, collect_active_edges, compute_graph_stats, compute_graph_stats_in_range,
    CandidateGraph,
};
use crate::metrics::{
    discrete_derivative, low_high_thresholds, nearest_threshold, window_ranges, window_regime,
};
use crate::signal::generate_events;

const FINITE_SIZE_SWEEP: [usize; 4] = [250, 500, 1_000, 2_000];
const SNAPSHOT_TAU_020: f64 = 0.20;
const SNAPSHOT_TAU_030: f64 = 0.30;
const SNAPSHOT_TAU_040: f64 = 0.40;

#[derive(Clone, Debug)]
pub struct GeneratedRun {
    pub run_id: String,
    pub config_hash: String,
    pub timestamp: String,
    pub output_dir: PathBuf,
}

pub fn run_simulation(config: SimulationConfig) -> Result<GeneratedRun, DynError> {
    config
        .validate()
        .map_err(|message| -> DynError { message.into() })?;

    let run_id = compute_run_id(&config);
    let config_hash = config.config_hash();
    let primary_events = generate_events(&config);
    let primary_graph = build_candidate_graph(&primary_events, &config);
    let thresholds = config.tau_thresholds();
    let (tau_low, tau_high) = low_high_thresholds(&thresholds);
    let tau_020 = nearest_threshold(&thresholds, SNAPSHOT_TAU_020);
    let tau_030 = nearest_threshold(&thresholds, SNAPSHOT_TAU_030);
    let tau_040 = nearest_threshold(&thresholds, SNAPSHOT_TAU_040);

    let mut sweep_sizes = FINITE_SIZE_SWEEP.to_vec();
    if !sweep_sizes.contains(&config.n_events) {
        sweep_sizes.push(config.n_events);
        sweep_sizes.sort_unstable();
    }

    let mut threshold_sweep = Vec::new();
    let mut transition_sharpness = Vec::new();
    let mut primary_critical_threshold = thresholds[thresholds.len() / 2];

    for &n_events in &sweep_sizes {
        let sized_config = if n_events == config.n_events {
            config.clone()
        } else {
            config.scaled_for_n_events(n_events)
        };
        sized_config
            .validate()
            .map_err(|message| -> DynError { message.into() })?;

        let (events, graph) = if n_events == config.n_events {
            (primary_events.clone(), primary_graph.clone())
        } else {
            let events = generate_events(&sized_config);
            let graph = build_candidate_graph(&events, &sized_config);
            (events, graph)
        };

        let sweep_rows = build_threshold_sweep_rows(&run_id, &thresholds, &events, &graph);
        let transition_rows = build_transition_rows(&run_id, n_events, &sweep_rows);

        if n_events == config.n_events {
            primary_critical_threshold =
                select_critical_threshold(&transition_rows, primary_critical_threshold);
        }

        threshold_sweep.extend(sweep_rows);
        transition_sharpness.extend(transition_rows);
    }

    let time_local_metrics = build_time_local_rows(
        &run_id,
        &config,
        &primary_events,
        &primary_graph,
        &[tau_low, primary_critical_threshold, tau_high],
    );
    let graph_snapshot_low =
        build_graph_snapshot_rows(&run_id, tau_low, &primary_events, &primary_graph);
    let graph_snapshot_critical = build_graph_snapshot_rows(
        &run_id,
        primary_critical_threshold,
        &primary_events,
        &primary_graph,
    );
    let graph_snapshot_high =
        build_graph_snapshot_rows(&run_id, tau_high, &primary_events, &primary_graph);
    let graph_snapshot_tau_020 =
        build_graph_snapshot_rows(&run_id, tau_020, &primary_events, &primary_graph);
    let graph_snapshot_tau_030 =
        build_graph_snapshot_rows(&run_id, tau_030, &primary_events, &primary_graph);
    let graph_snapshot_tau_040 =
        build_graph_snapshot_rows(&run_id, tau_040, &primary_events, &primary_graph);

    let output = prepare_output_dir(&SimulationConfig::repo_root())?;
    let manifest = RunManifestRow {
        run_id: run_id.clone(),
        timestamp: output.timestamp.clone(),
        config_hash: config_hash.clone(),
        crate_name: CRATE_NAME.to_string(),
        crate_version: CRATE_VERSION.to_string(),
        n_events: config.n_events,
        n_channels: config.n_channels,
        causal_window: config.causal_window,
        tau_steps: config.tau_steps,
        shock_start: config.shock_start,
        shock_end: config.shock_end,
        beta: config.beta,
        envelope_decay: config.envelope_decay,
    };

    let bundle = ExportBundle {
        manifest,
        events: primary_events,
        threshold_sweep,
        transition_sharpness,
        time_local_metrics,
        graph_snapshot_low,
        graph_snapshot_critical,
        graph_snapshot_high,
        graph_snapshot_tau_020,
        graph_snapshot_tau_030,
        graph_snapshot_tau_040,
    };

    write_bundle(&bundle, &output.run_dir)?;

    Ok(GeneratedRun {
        run_id,
        config_hash,
        timestamp: output.timestamp,
        output_dir: output.run_dir,
    })
}

fn build_threshold_sweep_rows(
    run_id: &str,
    thresholds: &[f64],
    events: &[crate::event::StructuralEvent],
    graph: &CandidateGraph,
) -> Vec<ThresholdSweepRow> {
    thresholds
        .iter()
        .copied()
        .map(|tau_threshold| {
            let stats = compute_graph_stats(graph, events, tau_threshold, 0);
            ThresholdSweepRow {
                run_id: run_id.to_string(),
                n_events: events.len(),
                tau_threshold,
                reachable_count: stats.reachable_count,
                reachable_fraction: stats.reachable_count as f64 / events.len() as f64,
                edge_count: stats.edge_count,
                mean_out_degree: stats.mean_out_degree,
                largest_component_fraction: stats.largest_component_fraction,
                component_entropy: stats.component_entropy,
            }
        })
        .collect()
}

fn build_transition_rows(
    run_id: &str,
    n_events: usize,
    sweep_rows: &[ThresholdSweepRow],
) -> Vec<TransitionSharpnessRow> {
    let points: Vec<(f64, f64)> = sweep_rows
        .iter()
        .map(|row| (row.tau_threshold, row.reachable_fraction))
        .collect();

    discrete_derivative(&points)
        .into_iter()
        .map(
            |(tau_midpoint, drho_dtau, abs_drho_dtau)| TransitionSharpnessRow {
                run_id: run_id.to_string(),
                n_events,
                tau_midpoint,
                drho_dtau,
                abs_drho_dtau,
            },
        )
        .collect()
}

fn select_critical_threshold(rows: &[TransitionSharpnessRow], fallback: f64) -> f64 {
    rows.iter()
        .max_by(|left, right| {
            left.abs_drho_dtau
                .partial_cmp(&right.abs_drho_dtau)
                .unwrap_or(Ordering::Equal)
        })
        .map(|row| row.tau_midpoint)
        .unwrap_or(fallback)
}

fn build_time_local_rows(
    run_id: &str,
    config: &SimulationConfig,
    events: &[crate::event::StructuralEvent],
    graph: &CandidateGraph,
    thresholds: &[f64],
) -> Vec<TimeLocalMetricsRow> {
    let mut rows = Vec::new();

    for &tau_threshold in thresholds {
        for (window_start, window_end_exclusive) in window_ranges(config) {
            let anchor_event = window_start;
            let stats = compute_graph_stats_in_range(
                graph,
                events,
                tau_threshold,
                anchor_event,
                (window_start, window_end_exclusive),
            );
            let window_len = window_end_exclusive - window_start;
            rows.push(TimeLocalMetricsRow {
                run_id: run_id.to_string(),
                tau_threshold,
                window_start,
                window_end: window_end_exclusive - 1,
                anchor_event,
                reachable_fraction: stats.reachable_count as f64 / window_len as f64,
                active_edge_count: stats.edge_count,
                mean_out_degree: stats.mean_out_degree,
                regime_label: window_regime(events, window_start, window_end_exclusive)
                    .as_str()
                    .to_string(),
            });
        }
    }

    rows
}

fn build_graph_snapshot_rows(
    run_id: &str,
    tau_threshold: f64,
    events: &[crate::event::StructuralEvent],
    graph: &CandidateGraph,
) -> Vec<GraphSnapshotRow> {
    collect_active_edges(graph, events, tau_threshold, None)
        .into_iter()
        .map(|edge| GraphSnapshotRow {
            run_id: run_id.to_string(),
            tau_threshold,
            src: edge.src,
            dst: edge.dst,
            src_trust: events[edge.src].trust,
            dst_trust: events[edge.dst].trust,
            compatible: edge.compatible,
        })
        .collect()
}