use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
use std::collections::HashMap;
pub const NETWORK_TOPOLOGY_SEED_OFFSET: u64 = 8600;
#[derive(Debug, Clone)]
pub struct NetworkNode {
pub id: usize,
pub degree: usize,
pub is_hub: bool, pub is_bridge: bool, pub cluster_id: Option<usize>,
}
#[derive(Debug, Clone, Copy)]
pub struct NetworkEdge {
pub from: usize,
pub to: usize,
}
#[derive(Debug, Clone)]
pub struct NetworkTopology {
pub nodes: Vec<NetworkNode>,
pub edges: Vec<NetworkEdge>,
}
impl NetworkTopology {
pub fn degree_distribution(&self) -> HashMap<usize, usize> {
let mut dist = HashMap::new();
for node in &self.nodes {
*dist.entry(node.degree).or_insert(0) += 1;
}
dist
}
pub fn hub_count(&self, min_degree: usize) -> usize {
self.nodes.iter().filter(|n| n.degree >= min_degree).count()
}
}
pub struct NetworkTopologyGenerator {
rng: ChaCha8Rng,
}
impl NetworkTopologyGenerator {
pub fn new(seed: u64) -> Self {
Self {
rng: ChaCha8Rng::seed_from_u64(seed.wrapping_add(NETWORK_TOPOLOGY_SEED_OFFSET)),
}
}
pub fn generate_ba(
&mut self,
total_nodes: usize,
m: usize,
num_clusters: usize,
) -> NetworkTopology {
let total_nodes = total_nodes.max(m + 1);
let mut nodes: Vec<NetworkNode> = (0..total_nodes)
.map(|i| NetworkNode {
id: i,
degree: 0,
is_hub: false,
is_bridge: false,
cluster_id: None,
})
.collect();
let mut edges: Vec<NetworkEdge> = Vec::new();
if num_clusters > 1 {
for (i, node) in nodes.iter_mut().enumerate() {
node.cluster_id = Some(i % num_clusters);
}
}
for i in 0..=m {
for j in (i + 1)..=m {
edges.push(NetworkEdge { from: i, to: j });
nodes[i].degree += 1;
nodes[j].degree += 1;
}
}
for new_id in (m + 1)..total_nodes {
let mut targets: Vec<usize> = Vec::with_capacity(m);
while targets.len() < m {
let total_degree: usize = nodes[..new_id].iter().map(|n| n.degree).sum();
if total_degree == 0 {
let candidate = self.rng.random_range(0..new_id);
if !targets.contains(&candidate) {
targets.push(candidate);
}
continue;
}
let roll = self.rng.random_range(0..total_degree);
let mut cumulative = 0;
for (id, node) in nodes[..new_id].iter().enumerate() {
cumulative += node.degree;
if cumulative > roll && !targets.contains(&id) {
targets.push(id);
break;
}
}
}
for t in targets {
edges.push(NetworkEdge {
from: new_id,
to: t,
});
nodes[new_id].degree += 1;
nodes[t].degree += 1;
}
}
let mut degrees: Vec<(usize, usize)> = nodes.iter().map(|n| (n.id, n.degree)).collect();
degrees.sort_by(|a, b| b.1.cmp(&a.1));
let hub_count = (total_nodes / 10).max(1);
for (id, _) in degrees.iter().take(hub_count) {
nodes[*id].is_hub = true;
}
if num_clusters > 1 {
for edge in &edges {
let c1 = nodes[edge.from].cluster_id;
let c2 = nodes[edge.to].cluster_id;
if c1 != c2 && c1.is_some() && c2.is_some() {
nodes[edge.from].is_bridge = true;
nodes[edge.to].is_bridge = true;
}
}
}
NetworkTopology { nodes, edges }
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_ba_produces_power_law_like_degrees() {
let mut gen = NetworkTopologyGenerator::new(42);
let network = gen.generate_ba(100, 2, 1);
assert_eq!(network.nodes.len(), 100);
assert!(network.edges.len() >= 99); let max_degree = network.nodes.iter().map(|n| n.degree).max().unwrap();
let avg_degree: f64 =
network.nodes.iter().map(|n| n.degree as f64).sum::<f64>() / network.nodes.len() as f64;
assert!(
(max_degree as f64) > 3.0 * avg_degree,
"Max degree ({max_degree}) should exceed 3x average ({avg_degree:.1}) — power-law signature"
);
}
#[test]
fn test_hub_count_reasonable() {
let mut gen = NetworkTopologyGenerator::new(42);
let network = gen.generate_ba(100, 2, 1);
let hubs = network.hub_count(5);
assert!(
hubs > 0 && hubs < 30,
"Should have some but not too many hubs: {hubs}"
);
}
#[test]
fn test_clusters_produce_bridges() {
let mut gen = NetworkTopologyGenerator::new(42);
let network = gen.generate_ba(50, 2, 3); let bridges = network.nodes.iter().filter(|n| n.is_bridge).count();
assert!(
bridges > 0,
"Should have bridge nodes with multiple clusters"
);
}
#[test]
fn test_degree_distribution_has_tail() {
let mut gen = NetworkTopologyGenerator::new(42);
let network = gen.generate_ba(200, 3, 1);
let dist = network.degree_distribution();
let low_degree = dist
.iter()
.filter(|(d, _)| **d <= 3)
.map(|(_, c)| c)
.sum::<usize>();
let high_degree = dist
.iter()
.filter(|(d, _)| **d >= 10)
.map(|(_, c)| c)
.sum::<usize>();
assert!(
low_degree > high_degree,
"Should have more low-degree than high-degree nodes"
);
}
}