Skip to main content

oximedia_graph/
edge_weight.rs

1//! Edge weight types for the filter graph pipeline.
2//!
3//! Models bandwidth, latency, and other costs associated with edges
4//! (connections) in a processing graph.
5
6#![allow(dead_code)]
7
8use std::collections::HashMap;
9
10/// The dimension in which an edge weight is expressed.
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12pub enum WeightType {
13    /// Data throughput in bits per second.
14    Bandwidth,
15    /// Propagation or processing latency in microseconds.
16    LatencyUs,
17    /// Relative cost (dimensionless score, lower is cheaper).
18    Cost,
19    /// Packet-loss probability in the range `[0, 1]`.
20    LossProbability,
21}
22
23impl WeightType {
24    /// Returns the measurement unit string for this weight type.
25    pub fn unit(&self) -> &'static str {
26        match self {
27            WeightType::Bandwidth => "bps",
28            WeightType::LatencyUs => "µs",
29            WeightType::Cost => "",
30            WeightType::LossProbability => "",
31        }
32    }
33}
34
35/// A numeric weight attached to a graph edge.
36#[derive(Debug, Clone)]
37pub struct EdgeWeight {
38    /// The kind of measurement this weight represents.
39    weight_type: WeightType,
40    /// The numeric value.
41    value: f64,
42    /// Threshold above which the edge is considered a bottleneck.
43    bottleneck_threshold: f64,
44}
45
46impl EdgeWeight {
47    /// Creates a new `EdgeWeight`.
48    pub fn new(weight_type: WeightType, value: f64, bottleneck_threshold: f64) -> Self {
49        Self {
50            weight_type,
51            value,
52            bottleneck_threshold,
53        }
54    }
55
56    /// Returns the weight type.
57    pub fn weight_type(&self) -> &WeightType {
58        &self.weight_type
59    }
60
61    /// Returns the raw numeric value.
62    pub fn value(&self) -> f64 {
63        self.value
64    }
65
66    /// Returns `true` if `value` exceeds `bottleneck_threshold`.
67    pub fn is_bottleneck(&self) -> bool {
68        self.value > self.bottleneck_threshold
69    }
70
71    /// Sets a new value.
72    pub fn set_value(&mut self, value: f64) {
73        self.value = value;
74    }
75}
76
77/// An edge in the graph that carries a weight between two node IDs.
78#[derive(Debug, Clone)]
79pub struct WeightedEdge {
80    /// Source node ID.
81    from: u64,
82    /// Destination node ID.
83    to: u64,
84    /// Bandwidth weight for this edge in bps.
85    bandwidth_bps: f64,
86    /// Additional named weights.
87    weights: Vec<EdgeWeight>,
88}
89
90impl WeightedEdge {
91    /// Creates a new `WeightedEdge` between `from` and `to` with the given
92    /// bandwidth in bps.
93    pub fn new(from: u64, to: u64, bandwidth_bps: f64) -> Self {
94        Self {
95            from,
96            to,
97            bandwidth_bps,
98            weights: Vec::new(),
99        }
100    }
101
102    /// Returns the source node ID.
103    pub fn from(&self) -> u64 {
104        self.from
105    }
106
107    /// Returns the destination node ID.
108    pub fn to(&self) -> u64 {
109        self.to
110    }
111
112    /// Returns the bandwidth in bps for this edge.
113    pub fn bandwidth_bps(&self) -> f64 {
114        self.bandwidth_bps
115    }
116
117    /// Computes the ratio of this edge's bandwidth to a reference `total_bps`.
118    /// Returns `0.0` if `total_bps` is zero.
119    pub fn bandwidth_ratio(&self, total_bps: f64) -> f64 {
120        if total_bps <= 0.0 {
121            return 0.0;
122        }
123        self.bandwidth_bps / total_bps
124    }
125
126    /// Attaches an additional weight to this edge.
127    pub fn add_weight(&mut self, weight: EdgeWeight) {
128        self.weights.push(weight);
129    }
130
131    /// Returns a slice of all attached weights.
132    pub fn weights(&self) -> &[EdgeWeight] {
133        &self.weights
134    }
135
136    /// Returns `true` if any attached weight is a bottleneck.
137    pub fn has_bottleneck(&self) -> bool {
138        self.weights.iter().any(|w| w.is_bottleneck())
139    }
140}
141
142/// A map of `WeightedEdge` instances keyed by `(from, to)` tuple.
143#[derive(Debug, Clone, Default)]
144pub struct EdgeWeightMap {
145    edges: HashMap<(u64, u64), WeightedEdge>,
146}
147
148impl EdgeWeightMap {
149    /// Creates an empty `EdgeWeightMap`.
150    pub fn new() -> Self {
151        Self {
152            edges: HashMap::new(),
153        }
154    }
155
156    /// Inserts a `WeightedEdge`. Overwrites any existing edge for `(from, to)`.
157    pub fn insert(&mut self, edge: WeightedEdge) {
158        self.edges.insert((edge.from(), edge.to()), edge);
159    }
160
161    /// Returns the edge between `from` and `to` if it exists.
162    pub fn get(&self, from: u64, to: u64) -> Option<&WeightedEdge> {
163        self.edges.get(&(from, to))
164    }
165
166    /// Returns the edge with the smallest `bandwidth_bps`, or `None` if empty.
167    pub fn min_weight(&self) -> Option<&WeightedEdge> {
168        self.edges.values().min_by(|a, b| {
169            a.bandwidth_bps()
170                .partial_cmp(&b.bandwidth_bps())
171                .unwrap_or(std::cmp::Ordering::Equal)
172        })
173    }
174
175    /// Returns the number of edges.
176    pub fn len(&self) -> usize {
177        self.edges.len()
178    }
179
180    /// Returns `true` if no edges are registered.
181    pub fn is_empty(&self) -> bool {
182        self.edges.is_empty()
183    }
184
185    /// Returns all edges that are considered bottlenecks.
186    pub fn bottleneck_edges(&self) -> Vec<&WeightedEdge> {
187        self.edges.values().filter(|e| e.has_bottleneck()).collect()
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn test_weight_type_unit_bandwidth() {
197        assert_eq!(WeightType::Bandwidth.unit(), "bps");
198    }
199
200    #[test]
201    fn test_weight_type_unit_latency() {
202        assert_eq!(WeightType::LatencyUs.unit(), "µs");
203    }
204
205    #[test]
206    fn test_weight_type_unit_cost_empty() {
207        assert_eq!(WeightType::Cost.unit(), "");
208    }
209
210    #[test]
211    fn test_edge_weight_not_bottleneck() {
212        let w = EdgeWeight::new(WeightType::Bandwidth, 100.0, 1000.0);
213        assert!(!w.is_bottleneck());
214    }
215
216    #[test]
217    fn test_edge_weight_is_bottleneck() {
218        let w = EdgeWeight::new(WeightType::LatencyUs, 2000.0, 1000.0);
219        assert!(w.is_bottleneck());
220    }
221
222    #[test]
223    fn test_edge_weight_set_value() {
224        let mut w = EdgeWeight::new(WeightType::Cost, 5.0, 10.0);
225        w.set_value(15.0);
226        assert!(w.is_bottleneck());
227    }
228
229    #[test]
230    fn test_weighted_edge_bandwidth_ratio() {
231        let e = WeightedEdge::new(0, 1, 500_000.0);
232        assert!((e.bandwidth_ratio(1_000_000.0) - 0.5).abs() < 1e-9);
233    }
234
235    #[test]
236    fn test_weighted_edge_bandwidth_ratio_zero_total() {
237        let e = WeightedEdge::new(0, 1, 500_000.0);
238        assert_eq!(e.bandwidth_ratio(0.0), 0.0);
239    }
240
241    #[test]
242    fn test_weighted_edge_has_bottleneck_false() {
243        let e = WeightedEdge::new(0, 1, 1_000_000.0);
244        assert!(!e.has_bottleneck());
245    }
246
247    #[test]
248    fn test_weighted_edge_has_bottleneck_true() {
249        let mut e = WeightedEdge::new(0, 1, 1_000_000.0);
250        e.add_weight(EdgeWeight::new(WeightType::LatencyUs, 5000.0, 1000.0));
251        assert!(e.has_bottleneck());
252    }
253
254    #[test]
255    fn test_edge_weight_map_insert_and_get() {
256        let mut map = EdgeWeightMap::new();
257        map.insert(WeightedEdge::new(0, 1, 100_000.0));
258        assert!(map.get(0, 1).is_some());
259        assert!(map.get(1, 0).is_none());
260    }
261
262    #[test]
263    fn test_edge_weight_map_min_weight() {
264        let mut map = EdgeWeightMap::new();
265        map.insert(WeightedEdge::new(0, 1, 200_000.0));
266        map.insert(WeightedEdge::new(1, 2, 50_000.0));
267        let min = map.min_weight().expect("min_weight should succeed");
268        assert!((min.bandwidth_bps() - 50_000.0).abs() < 1.0);
269    }
270
271    #[test]
272    fn test_edge_weight_map_empty() {
273        let map = EdgeWeightMap::new();
274        assert!(map.is_empty());
275        assert!(map.min_weight().is_none());
276    }
277
278    #[test]
279    fn test_edge_weight_map_bottleneck_edges() {
280        let mut map = EdgeWeightMap::new();
281        let mut e = WeightedEdge::new(0, 1, 1_000_000.0);
282        e.add_weight(EdgeWeight::new(WeightType::Cost, 999.0, 100.0));
283        map.insert(e);
284        map.insert(WeightedEdge::new(2, 3, 500_000.0));
285        assert_eq!(map.bottleneck_edges().len(), 1);
286    }
287}