Skip to main content

tatara_core/domain/
multi_distance.rs

1//! Multi-dimensional convergence distance.
2//!
3//! Every substrate is its own convergence DAG. The overall distance is a
4//! vector — one component per substrate. The system is converged only when
5//! ALL substrates are at zero (max component, not average).
6
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10use super::convergence_state::SubstrateType;
11
12/// Per-substrate convergence distance vector.
13#[derive(Debug, Clone, Serialize, Deserialize, Default)]
14pub struct MultiDimensionalDistance {
15    /// Distance per substrate (0.0 = converged on that dimension).
16    pub distances: BTreeMap<SubstrateType, f64>,
17}
18
19impl MultiDimensionalDistance {
20    /// Create a new multi-dimensional distance.
21    pub fn new() -> Self {
22        Self::default()
23    }
24
25    /// Set distance for a substrate.
26    pub fn set(&mut self, substrate: SubstrateType, distance: f64) {
27        self.distances.insert(substrate, distance);
28    }
29
30    /// Get distance for a substrate (1.0 if unknown).
31    pub fn get(&self, substrate: &SubstrateType) -> f64 {
32        self.distances.get(substrate).copied().unwrap_or(1.0)
33    }
34
35    /// Overall distance = max component (the worst substrate).
36    /// The system is only converged when ALL substrates are at zero.
37    pub fn overall(&self) -> f64 {
38        self.distances.values().copied().fold(0.0_f64, f64::max)
39    }
40
41    /// The substrate with the worst (highest) distance.
42    pub fn worst_substrate(&self) -> Option<(SubstrateType, f64)> {
43        self.distances
44            .iter()
45            .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
46            .map(|(s, d)| (*s, *d))
47    }
48
49    /// Is the system fully converged across all substrates?
50    pub fn is_converged(&self) -> bool {
51        !self.distances.is_empty() && self.distances.values().all(|d| *d == 0.0)
52    }
53
54    /// Number of substrates tracked.
55    pub fn substrate_count(&self) -> usize {
56        self.distances.len()
57    }
58}
59
60/// Maximum convergence velocity for a substrate.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(rename_all = "snake_case")]
63pub enum ConvergenceBandwidth {
64    /// Local computation, in-memory — effectively instant.
65    Instant,
66    /// API calls, cache lookups.
67    Seconds(u64),
68    /// Provisioning, cert issuance.
69    Minutes(u64),
70    /// Large deployments, data migration.
71    Hours(u64),
72    /// Compliance, procurement.
73    Days(u64),
74    /// Human decisions, external processes — no upper bound.
75    Unbounded,
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn test_empty_distance() {
84        let d = MultiDimensionalDistance::new();
85        assert_eq!(d.overall(), 0.0);
86        assert!(!d.is_converged()); // empty is not converged
87    }
88
89    #[test]
90    fn test_single_substrate() {
91        let mut d = MultiDimensionalDistance::new();
92        d.set(SubstrateType::Compute, 0.5);
93        assert_eq!(d.overall(), 0.5);
94        assert!(!d.is_converged());
95    }
96
97    #[test]
98    fn test_all_converged() {
99        let mut d = MultiDimensionalDistance::new();
100        d.set(SubstrateType::Compute, 0.0);
101        d.set(SubstrateType::Network, 0.0);
102        d.set(SubstrateType::Security, 0.0);
103        assert_eq!(d.overall(), 0.0);
104        assert!(d.is_converged());
105    }
106
107    #[test]
108    fn test_worst_substrate() {
109        let mut d = MultiDimensionalDistance::new();
110        d.set(SubstrateType::Compute, 0.1);
111        d.set(SubstrateType::Network, 0.8);
112        d.set(SubstrateType::Security, 0.3);
113        let (worst, dist) = d.worst_substrate().unwrap();
114        assert_eq!(worst, SubstrateType::Network);
115        assert_eq!(dist, 0.8);
116        assert_eq!(d.overall(), 0.8);
117    }
118
119    #[test]
120    fn test_overall_is_max_not_average() {
121        let mut d = MultiDimensionalDistance::new();
122        d.set(SubstrateType::Compute, 0.0);
123        d.set(SubstrateType::Security, 1.0);
124        // Max is 1.0, not average 0.5
125        assert_eq!(d.overall(), 1.0);
126    }
127
128    #[test]
129    fn test_unknown_substrate_defaults_to_1() {
130        let d = MultiDimensionalDistance::new();
131        assert_eq!(d.get(&SubstrateType::Financial), 1.0);
132    }
133
134    #[test]
135    fn test_bandwidth_serde() {
136        for bw in [
137            ConvergenceBandwidth::Instant,
138            ConvergenceBandwidth::Seconds(30),
139            ConvergenceBandwidth::Minutes(5),
140            ConvergenceBandwidth::Hours(2),
141            ConvergenceBandwidth::Days(7),
142            ConvergenceBandwidth::Unbounded,
143        ] {
144            let json = serde_json::to_string(&bw).unwrap();
145            let parsed: ConvergenceBandwidth = serde_json::from_str(&json).unwrap();
146            assert_eq!(bw, parsed);
147        }
148    }
149}