use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NodeId(pub u64);
impl NodeId {
pub fn new(id: u64) -> Self {
Self(id)
}
pub fn value(&self) -> u64 {
self.0
}
}
#[derive(Debug, Clone)]
pub struct Edge {
pub source: NodeId,
pub target: NodeId,
pub capacity: f64,
pub latency: f64,
}
impl Edge {
pub fn new(source: NodeId, target: NodeId, capacity: f64, latency: f64) -> Self {
Self {
source,
target,
capacity,
latency,
}
}
pub fn with_capacity(source: NodeId, target: NodeId, capacity: f64) -> Self {
Self {
source,
target,
capacity,
latency: 1.0 / capacity,
}
}
}
#[derive(Debug, Clone)]
pub struct Topology {
pub nodes: HashSet<NodeId>,
pub edges: Vec<Edge>,
pub active_tips: HashSet<NodeId>,
}
impl Default for Topology {
fn default() -> Self {
Self::new()
}
}
impl Topology {
pub fn new() -> Self {
Self {
nodes: HashSet::new(),
edges: vec![],
active_tips: HashSet::new(),
}
}
pub fn add_tip(&mut self, id: NodeId) {
self.nodes.insert(id);
self.active_tips.insert(id);
}
pub fn add_node(&mut self, id: NodeId) {
self.nodes.insert(id);
}
pub fn deactivate_tip(&mut self, id: NodeId) {
self.active_tips.remove(&id);
}
pub fn connect(&mut self, source: NodeId, target: NodeId, capacity: f64) {
self.nodes.insert(source);
self.nodes.insert(target);
self.edges.push(Edge {
source,
target,
capacity,
latency: 1.0 / capacity,
});
}
pub fn connect_bidirectional(&mut self, a: NodeId, b: NodeId, capacity: f64) {
self.connect(a, b, capacity);
self.connect(b, a, capacity);
}
pub fn neighbors(&self, node: NodeId) -> Vec<NodeId> {
self.edges
.iter()
.filter(|e| e.source == node)
.map(|e| e.target)
.collect()
}
pub fn edges_from(&self, node: NodeId) -> Vec<&Edge> {
self.edges.iter().filter(|e| e.source == node).collect()
}
pub fn edges_to(&self, node: NodeId) -> Vec<&Edge> {
self.edges.iter().filter(|e| e.target == node).collect()
}
pub fn fuse_tips(&mut self, a: NodeId, b: NodeId) -> Option<NodeId> {
if !self.active_tips.contains(&a) || !self.active_tips.contains(&b) {
return None;
}
self.edges.push(Edge {
source: a,
target: b,
capacity: 2.0, latency: 0.5,
});
self.edges.push(Edge {
source: b,
target: a,
capacity: 2.0,
latency: 0.5,
});
self.active_tips.remove(&b);
Some(a)
}
pub fn branch_tip(
&mut self,
tip: NodeId,
left_id: NodeId,
right_id: NodeId,
capacity: f64,
) -> (NodeId, NodeId) {
self.nodes.insert(left_id);
self.nodes.insert(right_id);
self.active_tips.insert(left_id);
self.active_tips.insert(right_id);
self.active_tips.remove(&tip);
self.edges.push(Edge {
source: tip,
target: left_id,
capacity,
latency: 1.0 / capacity,
});
self.edges.push(Edge {
source: tip,
target: right_id,
capacity,
latency: 1.0 / capacity,
});
(left_id, right_id)
}
pub fn shortest_path(&self, from: NodeId, to: NodeId) -> Option<Vec<NodeId>> {
if !self.nodes.contains(&from) || !self.nodes.contains(&to) {
return None;
}
if from == to {
return Some(vec![from]);
}
let mut distances: HashMap<NodeId, f64> = HashMap::new();
let mut previous: HashMap<NodeId, NodeId> = HashMap::new();
let mut unvisited: HashSet<NodeId> = self.nodes.clone();
distances.insert(from, 0.0);
while !unvisited.is_empty() {
let current = unvisited
.iter()
.filter(|n| distances.contains_key(n))
.min_by(|a, b| {
let da = distances.get(a).unwrap_or(&f64::INFINITY);
let db = distances.get(b).unwrap_or(&f64::INFINITY);
da.partial_cmp(db).unwrap_or(std::cmp::Ordering::Equal)
})
.copied();
let current = match current {
Some(c) => c,
None => break, };
if current == to {
let mut path = vec![to];
let mut curr = to;
while let Some(&prev) = previous.get(&curr) {
path.push(prev);
curr = prev;
}
path.reverse();
return Some(path);
}
unvisited.remove(¤t);
let current_dist = *distances.get(¤t).unwrap_or(&f64::INFINITY);
for edge in &self.edges {
if edge.source == current && unvisited.contains(&edge.target) {
let alt = current_dist + edge.latency;
if alt < *distances.get(&edge.target).unwrap_or(&f64::INFINITY) {
distances.insert(edge.target, alt);
previous.insert(edge.target, current);
}
}
}
}
None
}
pub fn is_connected(&self, from: NodeId, to: NodeId) -> bool {
self.shortest_path(from, to).is_some()
}
pub fn is_fully_connected(&self) -> bool {
if self.nodes.is_empty() {
return true;
}
let start = *self.nodes.iter().next().unwrap();
self.nodes.iter().all(|&n| self.is_connected(start, n))
}
pub fn prune(&mut self, threshold: f64) {
self.edges.retain(|e| e.capacity >= threshold);
}
pub fn metrics(&self) -> TopologyMetrics {
let node_count = self.nodes.len();
let edge_count = self.edges.len();
let active_tip_count = self.active_tips.len();
let total_capacity: f64 = self.edges.iter().map(|e| e.capacity).sum();
let avg_capacity = if edge_count > 0 {
total_capacity / edge_count as f64
} else {
0.0
};
let total_latency: f64 = self.edges.iter().map(|e| e.latency).sum();
let avg_latency = if edge_count > 0 {
total_latency / edge_count as f64
} else {
0.0
};
let density = if node_count > 1 {
edge_count as f64 / (node_count * (node_count - 1)) as f64
} else {
0.0
};
TopologyMetrics {
node_count,
edge_count,
active_tip_count,
avg_capacity,
avg_latency,
density,
}
}
}
#[derive(Debug, Clone)]
pub struct TopologyMetrics {
pub node_count: usize,
pub edge_count: usize,
pub active_tip_count: usize,
pub avg_capacity: f64,
pub avg_latency: f64,
pub density: f64,
}
impl TopologyMetrics {
pub fn has_explorers(&self) -> bool {
self.active_tip_count > 0
}
pub fn is_sparse(&self) -> bool {
self.density < 0.3
}
pub fn is_dense(&self) -> bool {
self.density > 0.7
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_topology() {
let mut topo = Topology::new();
topo.add_tip(NodeId(1));
topo.add_tip(NodeId(2));
topo.connect(NodeId(1), NodeId(2), 1.0);
assert_eq!(topo.nodes.len(), 2);
assert_eq!(topo.edges.len(), 1);
assert_eq!(topo.active_tips.len(), 2);
}
#[test]
fn test_shortest_path_simple() {
let mut topo = Topology::new();
topo.add_tip(NodeId(1));
topo.add_tip(NodeId(2));
topo.add_tip(NodeId(3));
topo.connect(NodeId(1), NodeId(2), 1.0);
topo.connect(NodeId(2), NodeId(3), 1.0);
let path = topo.shortest_path(NodeId(1), NodeId(3));
assert_eq!(path, Some(vec![NodeId(1), NodeId(2), NodeId(3)]));
}
#[test]
fn test_shortest_path_same_node() {
let mut topo = Topology::new();
topo.add_tip(NodeId(1));
let path = topo.shortest_path(NodeId(1), NodeId(1));
assert_eq!(path, Some(vec![NodeId(1)]));
}
#[test]
fn test_shortest_path_no_path() {
let mut topo = Topology::new();
topo.add_tip(NodeId(1));
topo.add_tip(NodeId(2));
let path = topo.shortest_path(NodeId(1), NodeId(2));
assert_eq!(path, None);
}
#[test]
fn test_shortest_path_prefers_lower_latency() {
let mut topo = Topology::new();
topo.add_tip(NodeId(1));
topo.add_tip(NodeId(2));
topo.add_tip(NodeId(3));
topo.edges.push(Edge {
source: NodeId(1),
target: NodeId(3),
capacity: 0.1,
latency: 10.0,
});
topo.connect(NodeId(1), NodeId(2), 1.0); topo.connect(NodeId(2), NodeId(3), 1.0);
let path = topo.shortest_path(NodeId(1), NodeId(3));
assert_eq!(path, Some(vec![NodeId(1), NodeId(2), NodeId(3)]));
}
#[test]
fn test_anastomosis() {
let mut topo = Topology::new();
topo.add_tip(NodeId(1));
topo.add_tip(NodeId(2));
let fused = topo.fuse_tips(NodeId(1), NodeId(2));
assert!(fused.is_some());
assert_eq!(fused, Some(NodeId(1)));
assert_eq!(topo.active_tips.len(), 1);
assert!(topo.active_tips.contains(&NodeId(1)));
assert!(!topo.active_tips.contains(&NodeId(2)));
assert_eq!(topo.edges.len(), 2); }
#[test]
fn test_anastomosis_requires_active_tips() {
let mut topo = Topology::new();
topo.add_tip(NodeId(1));
topo.add_node(NodeId(2));
let fused = topo.fuse_tips(NodeId(1), NodeId(2));
assert!(fused.is_none());
}
#[test]
fn test_branch_tip() {
let mut topo = Topology::new();
topo.add_tip(NodeId(1));
let (left, right) = topo.branch_tip(NodeId(1), NodeId(2), NodeId(3), 1.0);
assert_eq!(left, NodeId(2));
assert_eq!(right, NodeId(3));
assert_eq!(topo.nodes.len(), 3);
assert_eq!(topo.edges.len(), 2);
assert!(!topo.active_tips.contains(&NodeId(1)));
assert!(topo.active_tips.contains(&NodeId(2)));
assert!(topo.active_tips.contains(&NodeId(3)));
}
#[test]
fn test_neighbors() {
let mut topo = Topology::new();
topo.add_tip(NodeId(1));
topo.add_tip(NodeId(2));
topo.add_tip(NodeId(3));
topo.connect(NodeId(1), NodeId(2), 1.0);
topo.connect(NodeId(1), NodeId(3), 1.0);
let neighbors = topo.neighbors(NodeId(1));
assert_eq!(neighbors.len(), 2);
assert!(neighbors.contains(&NodeId(2)));
assert!(neighbors.contains(&NodeId(3)));
}
#[test]
fn test_prune() {
let mut topo = Topology::new();
topo.add_tip(NodeId(1));
topo.add_tip(NodeId(2));
topo.add_tip(NodeId(3));
topo.connect(NodeId(1), NodeId(2), 0.5);
topo.connect(NodeId(2), NodeId(3), 1.5);
topo.prune(1.0);
assert_eq!(topo.edges.len(), 1);
assert_eq!(topo.edges[0].source, NodeId(2));
assert_eq!(topo.edges[0].target, NodeId(3));
}
#[test]
fn test_metrics() {
let mut topo = Topology::new();
topo.add_tip(NodeId(1));
topo.add_tip(NodeId(2));
topo.add_tip(NodeId(3));
topo.connect(NodeId(1), NodeId(2), 1.0);
topo.connect(NodeId(2), NodeId(3), 2.0);
let metrics = topo.metrics();
assert_eq!(metrics.node_count, 3);
assert_eq!(metrics.edge_count, 2);
assert_eq!(metrics.active_tip_count, 3);
assert!((metrics.avg_capacity - 1.5).abs() < 0.001);
}
#[test]
fn test_bidirectional_connection() {
let mut topo = Topology::new();
topo.add_tip(NodeId(1));
topo.add_tip(NodeId(2));
topo.connect_bidirectional(NodeId(1), NodeId(2), 1.0);
assert_eq!(topo.edges.len(), 2);
let path1 = topo.shortest_path(NodeId(1), NodeId(2));
let path2 = topo.shortest_path(NodeId(2), NodeId(1));
assert!(path1.is_some());
assert!(path2.is_some());
}
}