census.rs
9.0 kB · rust · 305 lines
1use super::models::{Branch, Network, Node};2use mrlycore::logs;3use std::collections::HashSet;4use std::f64::consts::LN_2;56const RUNGS: u32 = 32;78/// Sums the straight-line lengths of every branch.9pub fn total_length(network: &Network) -> f64 {10 network11 .branches12 .iter()13 .map(|b| {14 let a = &network.nodes[b.parent].position;15 let c = &network.nodes[b.child].position;16 a.iter()17 .zip(c)18 .map(|(x, y)| (x - y) * (x - y))19 .sum::<f64>()20 .sqrt()21 })22 .sum()23}2425/// What a node's degree makes it.26#[derive(Clone, Copy, Debug, PartialEq)]27pub enum Role {28 /// A node no branch touches.29 Alone,30 /// A node exactly one branch touches.31 Tip,32 /// A node exactly two branches pass through.33 Through,34 /// A node three or more branches meet at.35 Junction,36}3738/// Tags every node by its degree, indexed like the node list.39///40/// ```41/// let mut net = mrlynum::graph::Network::new(2);42/// net.add_node(vec![0.0, 0.0]).unwrap();43/// net.add_node(vec![1.0, 0.0]).unwrap();44/// net.add_branch(0, 1, 1.0).unwrap();45/// assert_eq!(mrlynum::graph::roles(&net), vec![mrlynum::graph::Role::Tip; 2]);46/// ```47pub fn roles(network: &Network) -> Vec<Role> {48 network49 .degree()50 .iter()51 .map(|&d| match d {52 0 => Role::Alone,53 1 => Role::Tip,54 2 => Role::Through,55 _ => Role::Junction,56 })57 .collect()58}5960/// Counts the nodes of degree one.61pub fn tips(network: &Network) -> usize {62 roles(network).iter().filter(|&&r| r == Role::Tip).count()63}6465/// Counts the nodes of degree three or more.66pub fn junctions(network: &Network) -> usize {67 roles(network)68 .iter()69 .filter(|&&r| r == Role::Junction)70 .count()71}7273/// Counts the connected components of the network.74pub fn components(network: &Network) -> usize {75 let n = network.nodes.len();76 if n == 0 {77 return 0;78 }79 let adjacency = network.adjacency();80 let mut seen = vec![false; n];81 let mut count = 0;82 for start in 0..n {83 if seen[start] {84 continue;85 }86 count += 1;87 let mut stack = vec![start];88 seen[start] = true;89 while let Some(current) = stack.pop() {90 for &neighbor in &adjacency[¤t] {91 if !seen[neighbor] {92 seen[neighbor] = true;93 stack.push(neighbor);94 }95 }96 }97 }98 count99}100101/// Extracts the largest connected piece as a network of its own, branches re-indexed.102///103/// Ties go to the piece whose lowest node index comes first. An empty network comes back empty.104///105/// ```106/// let mut net = mrlynum::graph::Network::new(1);107/// for i in 0..3 { net.add_node(vec![i as f64]).unwrap(); }108/// net.add_branch(0, 1, 1.0).unwrap();109/// assert_eq!(mrlynum::graph::largest_component(&net).nodes.len(), 2);110/// ```111pub fn largest_component(network: &Network) -> Network {112 let n = network.nodes.len();113 let adjacency = network.adjacency();114 let mut label = vec![usize::MAX; n];115 let mut sizes: Vec<usize> = Vec::new();116 for start in 0..n {117 if label[start] != usize::MAX {118 continue;119 }120 let piece = sizes.len();121 let mut size = 0;122 let mut stack = vec![start];123 label[start] = piece;124 while let Some(current) = stack.pop() {125 size += 1;126 for &neighbor in &adjacency[¤t] {127 if label[neighbor] == usize::MAX {128 label[neighbor] = piece;129 stack.push(neighbor);130 }131 }132 }133 sizes.push(size);134 }135 let mut best = 0;136 for (piece, &size) in sizes.iter().enumerate() {137 if size > sizes[best] {138 best = piece;139 }140 }141 let mut giant = Network::new(network.dim);142 if sizes.is_empty() {143 return giant;144 }145 let mut index_of = vec![usize::MAX; n];146 for (old, node) in network.nodes.iter().enumerate() {147 if label[old] != best {148 continue;149 }150 let index = giant.nodes.len();151 index_of[old] = index;152 giant.nodes.push(Node {153 position: node.position.clone(),154 index,155 });156 }157 for branch in &network.branches {158 if label[branch.parent] != best {159 continue;160 }161 giant.branches.push(Branch {162 parent: index_of[branch.parent],163 child: index_of[branch.child],164 radius: branch.radius,165 });166 }167 giant168}169170/// Estimates the box-counting dimension of the node cloud over a ladder of halving boxes.171pub fn fractal_dimension(network: &Network) -> f64 {172 let positions: Vec<&Vec<f64>> = network.nodes.iter().map(|n| &n.position).collect();173 if positions.len() < 2 {174 return 0.0;175 }176 let dim = network.dim;177 let mins: Vec<f64> = (0..dim)178 .map(|a| positions.iter().map(|p| p[a]).fold(f64::MAX, f64::min))179 .collect();180 let maxs: Vec<f64> = (0..dim)181 .map(|a| positions.iter().map(|p| p[a]).fold(f64::MIN, f64::max))182 .collect();183 let extent = mins184 .iter()185 .zip(&maxs)186 .map(|(lo, hi)| hi - lo)187 .fold(0.0, f64::max);188 if extent == 0.0 {189 return 0.0;190 }191 let distinct: HashSet<Vec<u64>> = positions192 .iter()193 .map(|p| p.iter().map(|v| v.to_bits()).collect())194 .collect();195 let mut scales: Vec<f64> = Vec::new();196 let mut log_count: Vec<f64> = Vec::new();197 for k in 0..=RUNGS {198 let split = f64::from_bits(((1023 + k) as u64) << 52);199 let last = (1i64 << k) - 1;200 let mut boxes: HashSet<Vec<i64>> = HashSet::new();201 for p in &positions {202 let key: Vec<i64> = (0..dim)203 .map(|a| ((((p[a] - mins[a]) * split) / extent).floor() as i64).min(last))204 .collect();205 boxes.insert(key);206 }207 scales.push(k as f64);208 log_count.push(logs::ln(boxes.len() as f64));209 if boxes.len() == distinct.len() {210 break;211 }212 }213 let n = scales.len() as f64;214 let mean_x: f64 = scales.iter().sum::<f64>() / n;215 let mean_y: f64 = log_count.iter().sum::<f64>() / n;216 let cov: f64 = scales217 .iter()218 .zip(&log_count)219 .map(|(x, y)| (x - mean_x) * (y - mean_y))220 .sum();221 let var: f64 = scales.iter().map(|x| (x - mean_x) * (x - mean_x)).sum();222 cov / (var * LN_2)223}224225/// The measurements of one network.226#[derive(Clone, Debug, PartialEq)]227pub struct Census {228 /// The node count.229 pub nodes: usize,230 /// The branch count.231 pub branches: usize,232 /// The count of degree-one nodes.233 pub tips: usize,234 /// The count of nodes of degree three or more.235 pub junctions: usize,236 /// The connected component count.237 pub components: usize,238 /// The summed branch length.239 pub total_length: f64,240 /// The box-counting dimension estimate.241 pub fractal_dimension: f64,242}243244/// Takes the full census of a network.245///246/// ```247/// let mut net = mrlynum::graph::Network::new(2);248/// net.add_node(vec![0.0, 0.0]).unwrap();249/// net.add_node(vec![3.0, 4.0]).unwrap();250/// assert_eq!(mrlynum::graph::census(&net).components, 2);251/// ```252pub fn census(network: &Network) -> Census {253 Census {254 nodes: network.nodes.len(),255 branches: network.branches.len(),256 tips: tips(network),257 junctions: junctions(network),258 components: components(network),259 total_length: total_length(network),260 fractal_dimension: fractal_dimension(network),261 }262}263264#[cfg(test)]265mod tests {266 use super::*;267 use crate::graph::extract::core_graph;268 use mrlycore::atoms;269 #[test]270 fn carpet_census() {271 let network = core_graph(&atoms::carpet_2d(3)).unwrap();272 let c = census(&network);273 assert_eq!(c.nodes, 8);274 assert_eq!(c.branches, 8);275 assert_eq!(c.components, 1);276 assert!((c.total_length - 8.0).abs() < 1e-9);277 }278 #[test]279 fn carpet_census_holds_its_pinned_counts() {280 let network = core_graph(&atoms::carpet_2d(3).fractal(4)).unwrap();281 let c = census(&network);282 assert_eq!(c.nodes, 4096);283 assert_eq!(c.branches, 6424);284 assert_eq!(c.tips, 0);285 assert_eq!(c.junctions, 3596);286 assert_eq!(c.components, 1);287 assert!((c.total_length - 6424.0).abs() < 1e-9);288 }289 #[test]290 fn the_two_node_ladder_reads_exactly_one() {291 let mut net = Network::new(2);292 net.add_node(vec![0.0, 0.0]).unwrap();293 net.add_node(vec![3.0, 4.0]).unwrap();294 assert_eq!(fractal_dimension(&net), 1.0);295 net.add_node(vec![0.0, 0.0]).unwrap();296 assert_eq!(fractal_dimension(&net), 1.0);297 }298 #[test]299 fn the_carpet_dimension_holds_its_pinned_value() {300 let network = core_graph(&atoms::carpet_2d(3).fractal(4)).unwrap();301 let d = census(&network).fractal_dimension;302 assert!((0.0..=3.0).contains(&d), "dimension {d}");303 assert_eq!(d, 1.787589465914211);304 }305}