dirtydata_observer/
divergence.rs1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use dirtydata_core::types::{StableId, Hash, Timestamp};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct DivergencePoint {
8 pub sample_index: u64,
9 pub node_id: StableId,
10 pub node_name: String,
11 pub port_idx: usize,
12 pub expected_value: [f32; 2],
13 pub actual_value: [f32; 2],
14 pub diff_magnitude: f32,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, Default)]
19pub struct DivergenceMap {
20 pub points: Vec<DivergencePoint>,
21 pub golden_hash: Option<Hash>,
22 pub actual_hash: Option<Hash>,
23 pub first_divergence_index: Option<u64>,
24 pub timestamp: Timestamp,
25 pub metadata: HashMap<String, String>,
26}
27
28impl DivergenceMap {
29 pub fn new() -> Self {
30 Self {
31 timestamp: Timestamp::now(),
32 ..Default::default()
33 }
34 }
35
36 pub fn add_point(&mut self, point: DivergencePoint) {
37 if self.first_divergence_index.is_none() {
38 self.first_divergence_index = Some(point.sample_index);
39 }
40 self.points.push(point);
41 }
42
43 pub fn is_diverged(&self) -> bool {
44 !self.points.is_empty()
45 }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct CausalAnalysis {
51 pub parameter_delta: Vec<(StableId, String, f32, f32)>, pub divergence_magnitude_db: f32,
53 pub peak_divergence_sample: u64,
54 pub transient_impact: f32, }
56
57impl CausalAnalysis {
58 pub fn from_divergence(map: &DivergenceMap) -> Self {
59 let max_mag = map.points.iter().map(|p| p.diff_magnitude).fold(0.0, f32::max);
60 let peak_sample = map.points.iter().find(|p| p.diff_magnitude >= max_mag).map(|p| p.sample_index).unwrap_or(0);
61
62 Self {
63 parameter_delta: Vec::new(), divergence_magnitude_db: 20.0 * max_mag.log10().max(-100.0),
65 peak_divergence_sample: peak_sample,
66 transient_impact: max_mag * 1.5, }
68 }
69}