census.rs
10.3 kB · rust · 347 lines
1use super::models::{Branch, Network, Node};2use crate::core::error::Result;3use serde::{Deserialize, Serialize};4use std::collections::HashSet;5use std::f64::consts::LN_2;67const RUNGS: u32 = 32;89/// Sums the straight-line lengths of every branch.10pub fn total_length(network: &Network) -> f64 {11 network12 .branches13 .iter()14 .map(|b| {15 let a = &network.nodes[b.parent].position;16 let c = &network.nodes[b.child].position;17 a.iter()18 .zip(c)19 .map(|(x, y)| (x - y) * (x - y))20 .sum::<f64>()21 .sqrt()22 })23 .sum()24}2526/// What a node's degree makes it.27#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]28pub enum Role {29 /// A node no branch touches.30 Alone,31 /// A node exactly one branch touches.32 Tip,33 /// A node exactly two branches pass through.34 Through,35 /// A node three or more branches meet at.36 Junction,37}3839/// Tags every node by its degree, indexed like the node list.40///41/// ```42/// let mut net = mrlyrs::math::graph::Network::new(2);43/// net.add_node(vec![0.0, 0.0]).unwrap();44/// net.add_node(vec![1.0, 0.0]).unwrap();45/// net.add_branch(0, 1, 1.0).unwrap();46/// assert_eq!(mrlyrs::math::graph::roles(&net).unwrap(), vec![mrlyrs::math::graph::Role::Tip; 2]);47/// ```48///49/// # Errors50///51/// Errors when a branch names a node the network does not hold.52pub fn roles(network: &Network) -> Result<Vec<Role>> {53 Ok(network54 .degree()?55 .iter()56 .map(|&d| match d {57 0 => Role::Alone,58 1 => Role::Tip,59 2 => Role::Through,60 _ => Role::Junction,61 })62 .collect())63}6465/// Counts the nodes of degree one.66///67/// # Errors68///69/// Errors when a branch names a node the network does not hold.70pub fn tips(network: &Network) -> Result<usize> {71 Ok(roles(network)?.iter().filter(|&&r| r == Role::Tip).count())72}7374/// Counts the nodes of degree three or more.75///76/// # Errors77///78/// Errors when a branch names a node the network does not hold.79pub fn junctions(network: &Network) -> Result<usize> {80 Ok(roles(network)?81 .iter()82 .filter(|&&r| r == Role::Junction)83 .count())84}8586/// Counts the connected components of the network.87///88/// # Errors89///90/// Errors when a branch names a node the network does not hold.91pub fn components(network: &Network) -> Result<usize> {92 let n = network.nodes.len();93 if n == 0 {94 return Ok(0);95 }96 let adjacency = network.adjacency()?;97 let mut seen = vec![false; n];98 let mut count = 0;99 for start in 0..n {100 if seen[start] {101 continue;102 }103 count += 1;104 let mut stack = vec![start];105 seen[start] = true;106 while let Some(current) = stack.pop() {107 for &neighbor in &adjacency[¤t] {108 if !seen[neighbor] {109 seen[neighbor] = true;110 stack.push(neighbor);111 }112 }113 }114 }115 Ok(count)116}117118/// Extracts the largest connected piece as a network of its own, branches re-indexed.119///120/// Ties go to the piece whose lowest node index comes first. An empty network comes back empty.121///122/// ```123/// let mut net = mrlyrs::math::graph::Network::new(1);124/// for i in 0..3 { net.add_node(vec![i as f64]).unwrap(); }125/// net.add_branch(0, 1, 1.0).unwrap();126/// assert_eq!(mrlyrs::math::graph::largest_component(&net).unwrap().nodes.len(), 2);127/// ```128///129/// # Errors130///131/// Errors when a branch names a node the network does not hold.132pub fn largest_component(network: &Network) -> Result<Network> {133 let n = network.nodes.len();134 let adjacency = network.adjacency()?;135 let mut label = vec![usize::MAX; n];136 let mut sizes: Vec<usize> = Vec::new();137 for start in 0..n {138 if label[start] != usize::MAX {139 continue;140 }141 let piece = sizes.len();142 let mut size = 0;143 let mut stack = vec![start];144 label[start] = piece;145 while let Some(current) = stack.pop() {146 size += 1;147 for &neighbor in &adjacency[¤t] {148 if label[neighbor] == usize::MAX {149 label[neighbor] = piece;150 stack.push(neighbor);151 }152 }153 }154 sizes.push(size);155 }156 let mut best = 0;157 for (piece, &size) in sizes.iter().enumerate() {158 if size > sizes[best] {159 best = piece;160 }161 }162 let mut giant = Network::new(network.dim);163 if sizes.is_empty() {164 return Ok(giant);165 }166 let mut index_of = vec![usize::MAX; n];167 for (old, node) in network.nodes.iter().enumerate() {168 if label[old] != best {169 continue;170 }171 let index = giant.nodes.len();172 index_of[old] = index;173 giant.nodes.push(Node {174 position: node.position.clone(),175 index,176 });177 }178 for branch in &network.branches {179 if label[branch.parent] != best {180 continue;181 }182 giant.branches.push(Branch {183 parent: index_of[branch.parent],184 child: index_of[branch.child],185 radius: branch.radius,186 });187 }188 Ok(giant)189}190191/// Estimates the box-counting dimension of the node cloud over a ladder of halving boxes.192pub fn fractal_dimension(network: &Network) -> f64 {193 let positions: Vec<&Vec<f64>> = network.nodes.iter().map(|n| &n.position).collect();194 if positions.len() < 2 {195 return 0.0;196 }197 let dim = network.dim;198 let mins: Vec<f64> = (0..dim)199 .map(|a| positions.iter().map(|p| p[a]).fold(f64::MAX, f64::min))200 .collect();201 let maxs: Vec<f64> = (0..dim)202 .map(|a| positions.iter().map(|p| p[a]).fold(f64::MIN, f64::max))203 .collect();204 let extent = mins205 .iter()206 .zip(&maxs)207 .map(|(lo, hi)| hi - lo)208 .fold(0.0, f64::max);209 if extent == 0.0 {210 return 0.0;211 }212 let distinct: HashSet<Vec<u64>> = positions213 .iter()214 .map(|p| p.iter().map(|v| v.to_bits()).collect())215 .collect();216 let mut scales: Vec<f64> = Vec::new();217 let mut log_count: Vec<f64> = Vec::new();218 for k in 0..=RUNGS {219 let split = f64::from_bits(((1023 + k) as u64) << 52);220 let last = (1i64 << k) - 1;221 let mut boxes: HashSet<Vec<i64>> = HashSet::new();222 for p in &positions {223 let key: Vec<i64> = (0..dim)224 .map(|a| ((((p[a] - mins[a]) * split) / extent).floor() as i64).min(last))225 .collect();226 boxes.insert(key);227 }228 scales.push(k as f64);229 log_count.push(f64::ln(boxes.len() as f64));230 if boxes.len() == distinct.len() {231 break;232 }233 }234 let n = scales.len() as f64;235 let mean_x: f64 = scales.iter().sum::<f64>() / n;236 let mean_y: f64 = log_count.iter().sum::<f64>() / n;237 let cov: f64 = scales238 .iter()239 .zip(&log_count)240 .map(|(x, y)| (x - mean_x) * (y - mean_y))241 .sum();242 let var: f64 = scales.iter().map(|x| (x - mean_x) * (x - mean_x)).sum();243 cov / (var * LN_2)244}245246/// The measurements of one network.247#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]248pub struct Census {249 /// The node count.250 pub nodes: usize,251 /// The branch count.252 pub branches: usize,253 /// The count of degree-one nodes.254 pub tips: usize,255 /// The count of nodes of degree three or more.256 pub junctions: usize,257 /// The connected component count.258 pub components: usize,259 /// The summed branch length.260 pub total_length: f64,261 /// The box-counting dimension estimate.262 pub fractal_dimension: f64,263}264265/// Takes the full census of a network.266///267/// ```268/// let mut net = mrlyrs::math::graph::Network::new(2);269/// net.add_node(vec![0.0, 0.0]).unwrap();270/// net.add_node(vec![3.0, 4.0]).unwrap();271/// assert_eq!(mrlyrs::math::graph::census(&net).unwrap().components, 2);272/// ```273///274/// # Errors275///276/// Errors when a branch names a node the network does not hold.277pub fn census(network: &Network) -> Result<Census> {278 Ok(Census {279 nodes: network.nodes.len(),280 branches: network.branches.len(),281 tips: tips(network)?,282 junctions: junctions(network)?,283 components: components(network)?,284 total_length: total_length(network),285 fractal_dimension: fractal_dimension(network),286 })287}288289#[cfg(test)]290mod tests {291 use super::*;292 use crate::math::atoms;293 use crate::math::graph::extract::core_graph;294 #[test]295 fn carpet_census() {296 let network = core_graph(&atoms::carpet_2d(3)).unwrap();297 let c = census(&network).unwrap();298 assert_eq!(c.nodes, 8);299 assert_eq!(c.branches, 8);300 assert_eq!(c.components, 1);301 assert!((c.total_length - 8.0).abs() < 1e-9);302 }303 #[test]304 fn carpet_census_holds_its_pinned_counts() {305 let network = core_graph(&atoms::carpet_2d(3).fractal(4)).unwrap();306 let c = census(&network).unwrap();307 assert_eq!(c.nodes, 4096);308 assert_eq!(c.branches, 6424);309 assert_eq!(c.tips, 0);310 assert_eq!(c.junctions, 3596);311 assert_eq!(c.components, 1);312 assert!((c.total_length - 6424.0).abs() < 1e-9);313 }314 #[test]315 fn the_two_node_ladder_reads_exactly_one() {316 let mut net = Network::new(2);317 net.add_node(vec![0.0, 0.0]).unwrap();318 net.add_node(vec![3.0, 4.0]).unwrap();319 assert_eq!(fractal_dimension(&net), 1.0);320 net.add_node(vec![0.0, 0.0]).unwrap();321 assert_eq!(fractal_dimension(&net), 1.0);322 }323 #[test]324 fn the_carpet_dimension_holds_its_pinned_value() {325 let network = core_graph(&atoms::carpet_2d(3).fractal(4)).unwrap();326 let d = census(&network).unwrap().fractal_dimension;327 assert!((0.0..=3.0).contains(&d), "dimension {d}");328 assert!((d - 1.787589465914211).abs() < 1e-12, "dimension {d}");329 }330331 #[test]332 fn refuses_a_branch_past_the_nodes() {333 let mut net = Network::new(1);334 net.add_node(vec![0.0]).unwrap();335 net.branches.push(Branch {336 parent: 0,337 child: 4,338 radius: 1.0,339 });340 assert!(roles(&net).is_err());341 assert!(tips(&net).is_err());342 assert!(junctions(&net).is_err());343 assert!(components(&net).is_err());344 assert!(largest_component(&net).is_err());345 assert!(census(&net).is_err());346 }347}