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#[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#[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#[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
152fn 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}