1use serde::{Deserialize, Serialize};
11use std::collections::{HashMap, HashSet};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct NetworkEvaluation {
16 pub node_count: usize,
18 pub edge_count: usize,
20 pub connectivity_ratio: f64,
22 pub power_law_alpha: Option<f64>,
24 pub clustering_coefficient: f64,
26 pub vendor_concentration: ConcentrationMetrics,
28 pub customer_concentration: ConcentrationMetrics,
30 pub avg_relationship_strength: f64,
32 pub strength_stats: StrengthStats,
34 pub cross_process_link_rate: f64,
36 pub passes: bool,
38 pub issues: Vec<String>,
40}
41
42impl Default for NetworkEvaluation {
43 fn default() -> Self {
44 Self {
45 node_count: 0,
46 edge_count: 0,
47 connectivity_ratio: 0.0,
48 power_law_alpha: None,
49 clustering_coefficient: 0.0,
50 vendor_concentration: ConcentrationMetrics::default(),
51 customer_concentration: ConcentrationMetrics::default(),
52 avg_relationship_strength: 0.0,
53 strength_stats: StrengthStats::default(),
54 cross_process_link_rate: 0.0,
55 passes: true,
56 issues: Vec::new(),
57 }
58 }
59}
60
61#[derive(Debug, Clone, Default, Serialize, Deserialize)]
63pub struct ConcentrationMetrics {
64 pub total_count: usize,
66 pub top_1_share: f64,
68 pub top_5_share: f64,
70 pub hhi: f64,
72 pub exceeds_limits: bool,
74}
75
76#[derive(Debug, Clone, Default, Serialize, Deserialize)]
78pub struct StrengthStats {
79 pub min: f64,
81 pub max: f64,
83 pub mean: f64,
85 pub std_dev: f64,
87 pub strong_count: usize,
89 pub moderate_count: usize,
91 pub weak_count: usize,
93 pub dormant_count: usize,
95}
96
97#[derive(Debug, Clone)]
99pub struct NetworkThresholds {
100 pub connectivity_min: f64,
102 pub power_law_alpha_min: f64,
104 pub power_law_alpha_max: f64,
105 pub clustering_min: f64,
107 pub clustering_max: f64,
108 pub max_single_vendor_concentration: f64,
110 pub max_top5_vendor_concentration: f64,
112 pub min_cross_process_link_rate: f64,
114}
115
116impl Default for NetworkThresholds {
117 fn default() -> Self {
118 Self {
119 connectivity_min: 0.95,
120 power_law_alpha_min: 2.0,
121 power_law_alpha_max: 3.0,
122 clustering_min: 0.10,
123 clustering_max: 0.50,
124 max_single_vendor_concentration: 0.15,
125 max_top5_vendor_concentration: 0.45,
126 min_cross_process_link_rate: 0.30,
127 }
128 }
129}
130
131#[derive(Debug, Clone)]
133pub struct NetworkEdge {
134 pub from_id: String,
136 pub to_id: String,
138 pub strength: f64,
140 pub volume: f64,
142}
143
144#[derive(Debug, Clone)]
146pub struct NetworkNode {
147 pub id: String,
149 pub node_type: String,
151 pub volume: f64,
153}
154
155pub struct NetworkEvaluator {
157 thresholds: NetworkThresholds,
158}
159
160impl NetworkEvaluator {
161 pub fn new() -> Self {
163 Self {
164 thresholds: NetworkThresholds::default(),
165 }
166 }
167
168 pub fn with_thresholds(thresholds: NetworkThresholds) -> Self {
170 Self { thresholds }
171 }
172
173 pub fn evaluate(
175 &self,
176 nodes: &[NetworkNode],
177 edges: &[NetworkEdge],
178 cross_process_links: usize,
179 potential_links: usize,
180 ) -> NetworkEvaluation {
181 let mut eval = NetworkEvaluation {
182 node_count: nodes.len(),
183 edge_count: edges.len(),
184 ..Default::default()
185 };
186
187 if nodes.is_empty() {
188 eval.issues.push("Empty graph".to_string());
189 eval.passes = false;
190 return eval;
191 }
192
193 eval.connectivity_ratio = self.calculate_connectivity(nodes, edges);
195
196 eval.power_law_alpha = self.estimate_power_law_alpha(nodes, edges);
198
199 eval.clustering_coefficient = self.calculate_clustering_coefficient(nodes, edges);
201
202 eval.vendor_concentration = self.calculate_concentration(nodes, "vendor");
204 eval.customer_concentration = self.calculate_concentration(nodes, "customer");
205
206 eval.strength_stats = self.calculate_strength_stats(edges);
208 eval.avg_relationship_strength = eval.strength_stats.mean;
209
210 eval.cross_process_link_rate = if potential_links > 0 {
212 cross_process_links as f64 / potential_links as f64
213 } else {
214 0.0
215 };
216
217 self.check_thresholds(&mut eval);
219
220 eval
221 }
222
223 fn calculate_connectivity(&self, nodes: &[NetworkNode], edges: &[NetworkEdge]) -> f64 {
225 if nodes.is_empty() {
226 return 0.0;
227 }
228
229 let mut adjacency: HashMap<&str, HashSet<&str>> = HashMap::new();
231 for node in nodes {
232 adjacency.insert(&node.id, HashSet::new());
233 }
234 for edge in edges {
235 if let Some(neighbors) = adjacency.get_mut(edge.from_id.as_str()) {
236 neighbors.insert(&edge.to_id);
237 }
238 if let Some(neighbors) = adjacency.get_mut(edge.to_id.as_str()) {
239 neighbors.insert(&edge.from_id);
240 }
241 }
242
243 let mut visited: HashSet<&str> = HashSet::new();
245 let mut largest_component = 0usize;
246
247 for node in nodes {
248 if visited.contains(node.id.as_str()) {
249 continue;
250 }
251
252 let mut component_size = 0;
253 let mut queue = vec![node.id.as_str()];
254
255 while let Some(current) = queue.pop() {
256 if visited.contains(current) {
257 continue;
258 }
259 visited.insert(current);
260 component_size += 1;
261
262 if let Some(neighbors) = adjacency.get(current) {
263 for neighbor in neighbors {
264 if !visited.contains(*neighbor) {
265 queue.push(neighbor);
266 }
267 }
268 }
269 }
270
271 largest_component = largest_component.max(component_size);
272 }
273
274 largest_component as f64 / nodes.len() as f64
275 }
276
277 fn estimate_power_law_alpha(
279 &self,
280 nodes: &[NetworkNode],
281 edges: &[NetworkEdge],
282 ) -> Option<f64> {
283 let mut degrees: HashMap<&str, usize> = HashMap::new();
285 for node in nodes {
286 degrees.insert(&node.id, 0);
287 }
288 for edge in edges {
289 *degrees.entry(&edge.from_id).or_insert(0) += 1;
290 *degrees.entry(&edge.to_id).or_insert(0) += 1;
291 }
292
293 let degree_values: Vec<f64> = degrees
294 .values()
295 .filter(|&&d| d > 0)
296 .map(|&d| d as f64)
297 .collect();
298
299 if degree_values.len() < 10 {
300 return None;
301 }
302
303 let x_min = degree_values.iter().cloned().fold(f64::INFINITY, f64::min);
306 if x_min <= 0.0 {
307 return None;
308 }
309
310 let sum_log: f64 = degree_values.iter().map(|x| (x / x_min).ln()).sum();
311
312 if sum_log <= 0.0 {
313 return None;
314 }
315
316 let alpha = 1.0 + degree_values.len() as f64 / sum_log;
317 Some(alpha)
318 }
319
320 fn calculate_clustering_coefficient(
322 &self,
323 nodes: &[NetworkNode],
324 edges: &[NetworkEdge],
325 ) -> f64 {
326 if nodes.len() < 3 {
327 return 0.0;
328 }
329
330 let mut neighbors: HashMap<&str, HashSet<&str>> = HashMap::new();
332 for node in nodes {
333 neighbors.insert(&node.id, HashSet::new());
334 }
335 for edge in edges {
336 if let Some(set) = neighbors.get_mut(edge.from_id.as_str()) {
337 set.insert(&edge.to_id);
338 }
339 if let Some(set) = neighbors.get_mut(edge.to_id.as_str()) {
340 set.insert(&edge.from_id);
341 }
342 }
343
344 let mut total_clustering = 0.0;
346 let mut valid_nodes = 0;
347
348 for node in nodes {
349 let node_neighbors = match neighbors.get(node.id.as_str()) {
350 Some(n) => n,
351 None => continue,
352 };
353
354 let k = node_neighbors.len();
355 if k < 2 {
356 continue;
357 }
358
359 let mut neighbor_edges = 0;
361 let neighbor_list: Vec<_> = node_neighbors.iter().collect();
362 for i in 0..neighbor_list.len() {
363 for j in (i + 1)..neighbor_list.len() {
364 if let Some(n_neighbors) = neighbors.get(*neighbor_list[i]) {
365 if n_neighbors.contains(*neighbor_list[j]) {
366 neighbor_edges += 1;
367 }
368 }
369 }
370 }
371
372 let max_edges = k * (k - 1) / 2;
373 if max_edges > 0 {
374 total_clustering += neighbor_edges as f64 / max_edges as f64;
375 valid_nodes += 1;
376 }
377 }
378
379 if valid_nodes > 0 {
380 total_clustering / valid_nodes as f64
381 } else {
382 0.0
383 }
384 }
385
386 fn calculate_concentration(
388 &self,
389 nodes: &[NetworkNode],
390 node_type: &str,
391 ) -> ConcentrationMetrics {
392 let type_nodes: Vec<_> = nodes
393 .iter()
394 .filter(|n| n.node_type.to_lowercase() == node_type.to_lowercase())
395 .collect();
396
397 if type_nodes.is_empty() {
398 return ConcentrationMetrics::default();
399 }
400
401 let total_volume: f64 = type_nodes.iter().map(|n| n.volume).sum();
402 if total_volume <= 0.0 {
403 return ConcentrationMetrics {
404 total_count: type_nodes.len(),
405 ..Default::default()
406 };
407 }
408
409 let mut volumes: Vec<f64> = type_nodes.iter().map(|n| n.volume).collect();
411 volumes.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
412
413 let top_1_share = volumes.first().map(|v| v / total_volume).unwrap_or(0.0);
414 let top_5_share: f64 = volumes.iter().take(5).sum::<f64>() / total_volume;
415
416 let hhi: f64 = volumes.iter().map(|v| (v / total_volume).powi(2)).sum();
418
419 let exceeds_limits = top_1_share > self.thresholds.max_single_vendor_concentration
420 || top_5_share > self.thresholds.max_top5_vendor_concentration;
421
422 ConcentrationMetrics {
423 total_count: type_nodes.len(),
424 top_1_share,
425 top_5_share,
426 hhi,
427 exceeds_limits,
428 }
429 }
430
431 fn calculate_strength_stats(&self, edges: &[NetworkEdge]) -> StrengthStats {
433 if edges.is_empty() {
434 return StrengthStats::default();
435 }
436
437 let strengths: Vec<f64> = edges.iter().map(|e| e.strength).collect();
438 let n = strengths.len() as f64;
439
440 let min = strengths.iter().cloned().fold(f64::INFINITY, f64::min);
441 let max = strengths.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
442 let mean = strengths.iter().sum::<f64>() / n;
443 let variance = strengths.iter().map(|s| (s - mean).powi(2)).sum::<f64>() / n;
444 let std_dev = variance.sqrt();
445
446 let strong_count = strengths.iter().filter(|&&s| s >= 0.7).count();
447 let moderate_count = strengths
448 .iter()
449 .filter(|&&s| (0.4..0.7).contains(&s))
450 .count();
451 let weak_count = strengths
452 .iter()
453 .filter(|&&s| (0.1..0.4).contains(&s))
454 .count();
455 let dormant_count = strengths.iter().filter(|&&s| s < 0.1).count();
456
457 StrengthStats {
458 min,
459 max,
460 mean,
461 std_dev,
462 strong_count,
463 moderate_count,
464 weak_count,
465 dormant_count,
466 }
467 }
468
469 fn check_thresholds(&self, eval: &mut NetworkEvaluation) {
471 eval.issues.clear();
472
473 if eval.connectivity_ratio < self.thresholds.connectivity_min {
475 eval.issues.push(format!(
476 "Connectivity ratio {:.2} < {:.2} (threshold)",
477 eval.connectivity_ratio, self.thresholds.connectivity_min
478 ));
479 }
480
481 if let Some(alpha) = eval.power_law_alpha {
483 if alpha < self.thresholds.power_law_alpha_min
484 || alpha > self.thresholds.power_law_alpha_max
485 {
486 eval.issues.push(format!(
487 "Power law alpha {:.2} not in range [{:.1}, {:.1}]",
488 alpha, self.thresholds.power_law_alpha_min, self.thresholds.power_law_alpha_max
489 ));
490 }
491 }
492
493 if eval.clustering_coefficient < self.thresholds.clustering_min
495 || eval.clustering_coefficient > self.thresholds.clustering_max
496 {
497 eval.issues.push(format!(
498 "Clustering coefficient {:.3} not in range [{:.2}, {:.2}]",
499 eval.clustering_coefficient,
500 self.thresholds.clustering_min,
501 self.thresholds.clustering_max
502 ));
503 }
504
505 if eval.vendor_concentration.exceeds_limits {
507 if eval.vendor_concentration.top_1_share
508 > self.thresholds.max_single_vendor_concentration
509 {
510 eval.issues.push(format!(
511 "Single vendor concentration {:.2}% > {:.0}% (limit)",
512 eval.vendor_concentration.top_1_share * 100.0,
513 self.thresholds.max_single_vendor_concentration * 100.0
514 ));
515 }
516 if eval.vendor_concentration.top_5_share > self.thresholds.max_top5_vendor_concentration
517 {
518 eval.issues.push(format!(
519 "Top 5 vendor concentration {:.2}% > {:.0}% (limit)",
520 eval.vendor_concentration.top_5_share * 100.0,
521 self.thresholds.max_top5_vendor_concentration * 100.0
522 ));
523 }
524 }
525
526 if eval.cross_process_link_rate < self.thresholds.min_cross_process_link_rate {
528 eval.issues.push(format!(
529 "Cross-process link rate {:.2}% < {:.0}% (threshold)",
530 eval.cross_process_link_rate * 100.0,
531 self.thresholds.min_cross_process_link_rate * 100.0
532 ));
533 }
534
535 eval.passes = eval.issues.is_empty();
536 }
537}
538
539impl Default for NetworkEvaluator {
540 fn default() -> Self {
541 Self::new()
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548
549 fn create_test_nodes() -> Vec<NetworkNode> {
550 vec![
551 NetworkNode {
552 id: "company".to_string(),
553 node_type: "company".to_string(),
554 volume: 1000000.0,
555 },
556 NetworkNode {
557 id: "vendor1".to_string(),
558 node_type: "vendor".to_string(),
559 volume: 100000.0,
560 },
561 NetworkNode {
562 id: "vendor2".to_string(),
563 node_type: "vendor".to_string(),
564 volume: 80000.0,
565 },
566 NetworkNode {
567 id: "vendor3".to_string(),
568 node_type: "vendor".to_string(),
569 volume: 60000.0,
570 },
571 NetworkNode {
572 id: "customer1".to_string(),
573 node_type: "customer".to_string(),
574 volume: 150000.0,
575 },
576 NetworkNode {
577 id: "customer2".to_string(),
578 node_type: "customer".to_string(),
579 volume: 120000.0,
580 },
581 ]
582 }
583
584 fn create_test_edges() -> Vec<NetworkEdge> {
585 vec![
586 NetworkEdge {
587 from_id: "company".to_string(),
588 to_id: "vendor1".to_string(),
589 strength: 0.8,
590 volume: 100000.0,
591 },
592 NetworkEdge {
593 from_id: "company".to_string(),
594 to_id: "vendor2".to_string(),
595 strength: 0.6,
596 volume: 80000.0,
597 },
598 NetworkEdge {
599 from_id: "company".to_string(),
600 to_id: "vendor3".to_string(),
601 strength: 0.4,
602 volume: 60000.0,
603 },
604 NetworkEdge {
605 from_id: "company".to_string(),
606 to_id: "customer1".to_string(),
607 strength: 0.9,
608 volume: 150000.0,
609 },
610 NetworkEdge {
611 from_id: "company".to_string(),
612 to_id: "customer2".to_string(),
613 strength: 0.7,
614 volume: 120000.0,
615 },
616 NetworkEdge {
618 from_id: "vendor1".to_string(),
619 to_id: "vendor2".to_string(),
620 strength: 0.3,
621 volume: 20000.0,
622 },
623 ]
624 }
625
626 #[test]
627 fn test_network_evaluation_basic() {
628 let nodes = create_test_nodes();
629 let edges = create_test_edges();
630
631 let evaluator = NetworkEvaluator::new();
632 let eval = evaluator.evaluate(&nodes, &edges, 10, 30);
633
634 assert_eq!(eval.node_count, 6);
635 assert_eq!(eval.edge_count, 6);
636 assert!(eval.connectivity_ratio > 0.0);
637 }
638
639 #[test]
640 fn test_connectivity_calculation() {
641 let nodes = create_test_nodes();
642 let edges = create_test_edges();
643
644 let evaluator = NetworkEvaluator::new();
645 let connectivity = evaluator.calculate_connectivity(&nodes, &edges);
646
647 assert_eq!(connectivity, 1.0);
649 }
650
651 #[test]
652 fn test_concentration_metrics() {
653 let nodes = create_test_nodes();
654
655 let evaluator = NetworkEvaluator::new();
656 let vendor_conc = evaluator.calculate_concentration(&nodes, "vendor");
657
658 assert_eq!(vendor_conc.total_count, 3);
659 assert!(vendor_conc.top_1_share > 0.0);
660 assert!(vendor_conc.top_5_share > 0.0);
661 assert!(vendor_conc.hhi > 0.0);
662 }
663
664 #[test]
665 fn test_strength_stats() {
666 let edges = create_test_edges();
667
668 let evaluator = NetworkEvaluator::new();
669 let stats = evaluator.calculate_strength_stats(&edges);
670
671 assert!(stats.min > 0.0);
672 assert!(stats.max <= 1.0);
673 assert!(stats.mean > 0.0);
674 assert!(stats.strong_count > 0); }
676
677 #[test]
678 fn test_empty_graph() {
679 let evaluator = NetworkEvaluator::new();
680 let eval = evaluator.evaluate(&[], &[], 0, 0);
681
682 assert!(!eval.passes);
683 assert!(!eval.issues.is_empty());
684 }
685}