Skip to main content

dsfb_dscd/
integrations.rs

1use anyhow::{ensure, Result};
2use dsfb::sim::{run_simulation, SimConfig};
3use dsfb::DsfbParams;
4use dsfb_add::aet::run_aet_sweep;
5use dsfb_add::analysis::structural_law::fit_with_ci;
6use dsfb_add::iwlt::run_iwlt_sweep;
7use dsfb_add::SimulationConfig;
8
9use crate::graph::{Event, EventId};
10
11/// Deterministic per-observer trust profile used to induce non-uniform
12/// trust threshold crossings without randomness.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum TrustProfile {
15    Tight,
16    Medium,
17    Loose,
18}
19
20impl TrustProfile {
21    pub fn from_observer_index(observer_index: u32) -> Self {
22        match observer_index % 3 {
23            0 => Self::Tight,
24            1 => Self::Medium,
25            _ => Self::Loose,
26        }
27    }
28
29    fn trust_floor(self) -> f64 {
30        match self {
31            Self::Tight => 0.05,
32            Self::Medium => 0.08,
33            Self::Loose => 0.10,
34        }
35    }
36
37    fn trust_ceiling(self) -> f64 {
38        match self {
39            Self::Tight => 0.70,
40            Self::Medium => 0.85,
41            Self::Loose => 0.95,
42        }
43    }
44
45    fn growth_gain(self) -> f64 {
46        match self {
47            Self::Tight => 0.020,
48            Self::Medium => 0.030,
49            Self::Loose => 0.042,
50        }
51    }
52
53    fn decay_factor(self) -> f64 {
54        match self {
55            Self::Tight => 0.965,
56            Self::Medium => 0.975,
57            Self::Loose => 0.985,
58        }
59    }
60
61    fn envelope_limit(self) -> f64 {
62        match self {
63            Self::Tight => 0.10,
64            Self::Medium => 0.20,
65            Self::Loose => 0.30,
66        }
67    }
68
69    pub fn as_str(self) -> &'static str {
70        match self {
71            Self::Tight => "tight",
72            Self::Medium => "medium",
73            Self::Loose => "loose",
74        }
75    }
76}
77
78/// Deterministic residual state bucket used for event/edge provenance exports.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum ResidualState {
81    Low,
82    Medium,
83    High,
84}
85
86impl ResidualState {
87    pub fn from_residual(residual_ema: f64) -> Self {
88        if residual_ema <= 0.10 {
89            Self::Low
90        } else if residual_ema <= 0.30 {
91            Self::Medium
92        } else {
93            Self::High
94        }
95    }
96
97    pub fn as_str(self) -> &'static str {
98        match self {
99            Self::Low => "L",
100            Self::Medium => "M",
101            Self::High => "H",
102        }
103    }
104}
105
106/// Symbolic deterministic local rewrite rule identifier used for traceability.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum RewriteRule {
109    StableEnvelope,
110    ModerateEnvelope,
111    HighResidualRecovery,
112    EnvelopeDecay,
113}
114
115impl RewriteRule {
116    pub fn from_residual_state(state: ResidualState, envelope_ok: bool) -> Self {
117        if !envelope_ok {
118            return Self::EnvelopeDecay;
119        }
120
121        match state {
122            ResidualState::Low => Self::StableEnvelope,
123            ResidualState::Medium => Self::ModerateEnvelope,
124            ResidualState::High => Self::HighResidualRecovery,
125        }
126    }
127
128    pub fn id(self) -> u32 {
129        match self {
130            Self::StableEnvelope => 0,
131            Self::ModerateEnvelope => 1,
132            Self::HighResidualRecovery => 2,
133            Self::EnvelopeDecay => 3,
134        }
135    }
136
137    pub fn as_str(self) -> &'static str {
138        match self {
139            Self::StableEnvelope => "stable_envelope",
140            Self::ModerateEnvelope => "moderate_envelope",
141            Self::HighResidualRecovery => "high_residual_recovery",
142            Self::EnvelopeDecay => "envelope_decay",
143        }
144    }
145}
146
147fn initialize_profiled_trust(profile: TrustProfile, baseline_weight: f64) -> f64 {
148    let blended = 0.5 * baseline_weight + 0.5 * profile.trust_floor();
149    blended.clamp(profile.trust_floor(), profile.trust_ceiling())
150}
151
152/// Deterministic trust update that keeps the DSCD monotonicity condition:
153/// trust only increases when the residual envelope is satisfied.
154fn update_profiled_trust(
155    profile: TrustProfile,
156    current: f64,
157    residual_ema: f64,
158    envelope_ok: bool,
159) -> f64 {
160    let floor = profile.trust_floor();
161    let ceiling = profile.trust_ceiling();
162
163    if envelope_ok {
164        let residual_factor = (1.0 / (1.0 + residual_ema.max(0.0))).clamp(0.0, 1.0);
165        let proposal = current + profile.growth_gain() * residual_factor;
166        proposal.max(current).clamp(floor, ceiling)
167    } else {
168        (current * profile.decay_factor()).clamp(floor, ceiling)
169    }
170}
171
172#[derive(Debug, Clone)]
173pub struct DscdObserverSample {
174    pub event_id: u64,
175    pub time_index: usize,
176    pub observer_id: u32,
177    pub trust: f64,
178    pub residual_summary: f64,
179    pub residual_state: ResidualState,
180    pub rewrite_rule_id: u32,
181    pub rewrite_rule_label: &'static str,
182    pub trust_profile: TrustProfile,
183    pub envelope_ok: bool,
184}
185
186#[derive(Debug, Clone)]
187pub struct DscdEventBatch {
188    pub events: Vec<Event>,
189    pub observer_samples: Vec<DscdObserverSample>,
190}
191
192#[derive(Debug, Clone, Copy)]
193pub struct StructuralGrowthSummary {
194    pub s_infty: f64,
195    pub law_slope: f64,
196}
197
198pub fn generate_dscd_events_from_dsfb(
199    scenario: &SimConfig,
200    dsfb_params: DsfbParams,
201    num_events: usize,
202) -> Result<DscdEventBatch> {
203    ensure!(num_events > 0, "num_events must be greater than zero");
204
205    let mut run_cfg = scenario.clone();
206    run_cfg.steps = num_events;
207
208    let simulation = run_simulation(run_cfg, dsfb_params);
209    let mut events = Vec::with_capacity(simulation.len());
210    let mut observer_samples = Vec::new();
211    let channels = 2_usize;
212    let mut trust_state = vec![0.0; channels];
213
214    if let Some(first_step) = simulation.first() {
215        for (observer_index, trust_slot) in trust_state.iter_mut().enumerate().take(channels) {
216            let baseline = if observer_index == 1 {
217                first_step.w2
218            } else {
219                (1.0 - first_step.w2).clamp(0.0, 1.0)
220            };
221            let profile = TrustProfile::from_observer_index(observer_index as u32);
222            *trust_slot = initialize_profiled_trust(profile, baseline);
223        }
224    }
225
226    for (step_idx, step) in simulation.into_iter().enumerate() {
227        let synthesized_residuals = [
228            step.s2 * 0.85 + 0.02 * (step_idx as f64 * 0.017).sin().abs(),
229            step.s2,
230        ];
231        let structural_tag = Some(
232            synthesized_residuals.iter().copied().sum::<f64>() / synthesized_residuals.len() as f64,
233        );
234
235        events.push(Event {
236            id: EventId(step_idx as u64),
237            timestamp: Some(step.t),
238            structural_tag,
239        });
240
241        for (observer_id, residual_summary) in synthesized_residuals.iter().enumerate() {
242            let observer_id_u32 = observer_id as u32;
243            let profile = TrustProfile::from_observer_index(observer_id_u32);
244            let residual_state = ResidualState::from_residual(*residual_summary);
245            let envelope_ok = *residual_summary <= profile.envelope_limit();
246            let next_trust = update_profiled_trust(
247                profile,
248                trust_state[observer_id],
249                *residual_summary,
250                envelope_ok,
251            );
252            trust_state[observer_id] = next_trust;
253            let rewrite_rule = RewriteRule::from_residual_state(residual_state, envelope_ok);
254
255            observer_samples.push(DscdObserverSample {
256                event_id: step_idx as u64,
257                time_index: step_idx,
258                observer_id: observer_id_u32,
259                trust: next_trust,
260                residual_summary: *residual_summary,
261                residual_state,
262                rewrite_rule_id: rewrite_rule.id(),
263                rewrite_rule_label: rewrite_rule.as_str(),
264                trust_profile: profile,
265                envelope_ok,
266            });
267        }
268    }
269
270    Ok(DscdEventBatch {
271        events,
272        observer_samples,
273    })
274}
275
276pub fn compute_structural_growth_for_dscd(
277    add_cfg: &SimulationConfig,
278) -> Result<StructuralGrowthSummary> {
279    add_cfg.validate()?;
280
281    let lambda_grid = add_cfg.lambda_grid();
282    let aet = run_aet_sweep(add_cfg, &lambda_grid)?;
283    let iwlt = run_iwlt_sweep(add_cfg, &lambda_grid)?;
284    let fit = fit_with_ci(&aet.echo_slope, &iwlt.entropy_density)?;
285
286    let s_infty = iwlt
287        .entropy_density
288        .iter()
289        .copied()
290        .reduce(f64::max)
291        .unwrap_or(0.0);
292
293    Ok(StructuralGrowthSummary {
294        s_infty,
295        law_slope: fit.slope,
296    })
297}