1use crate::base::{DiGraph, EdgeWeight, Graph, Node};
7use crate::error::{GraphError, Result};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone)]
12pub struct WeightStatistics<E: EdgeWeight> {
13 pub min_weight: E,
15 pub max_weight: E,
17 pub total_weight: E,
19 pub edge_count: usize,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum NormalizationMethod {
26 MinMax,
28 ZScore,
30 L1,
32 L2,
34 Robust,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq)]
40pub enum WeightTransform {
41 Linear {
43 a: f64,
45 b: f64,
47 },
48 Logarithmic {
50 offset: f64,
52 },
53 Exponential,
55 Power {
57 power: f64,
59 },
60 Inverse,
62 SquareRoot,
64}
65
66#[derive(Debug, Clone)]
68pub struct MultiWeight<E: EdgeWeight> {
69 pub primary: E,
71 pub weights: HashMap<String, E>,
73}
74
75impl<E: EdgeWeight> MultiWeight<E> {
76 pub fn new(primary: E) -> Self {
78 Self {
79 primary,
80 weights: HashMap::new(),
81 }
82 }
83
84 pub fn add_weight(&mut self, name: String, weight: E) {
86 self.weights.insert(name, weight);
87 }
88
89 pub fn get_weight(&self, name: &str) -> Option<&E> {
91 self.weights.get(name)
92 }
93}
94
95pub trait WeightedOps<N: Node, E: EdgeWeight> {
97 fn weight_statistics(&self) -> Result<WeightStatistics<E>>;
99
100 fn filter_by_weight(&self, min_weight: Option<E>, maxweight: Option<E>) -> Result<Self>
102 where
103 Self: Sized;
104
105 fn edges_by_weight(&self, ascending: bool) -> Result<Vec<(N, N, E)>>;
107
108 fn subgraph_by_weight_range(&self, min_weight: E, maxweight: E) -> Result<Self>
110 where
111 Self: Sized;
112
113 fn normalize_weights(&self, method: NormalizationMethod) -> Result<Self>
115 where
116 Self: Sized;
117
118 fn transform_weights(&self, transform: WeightTransform) -> Result<Self>
120 where
121 Self: Sized;
122
123 fn weight_distribution(&self, bins: usize) -> Result<Vec<(E, usize)>>;
125
126 fn weighted_degree_centrality(&self) -> Result<HashMap<N, f64>>;
128
129 fn total_weight(&self) -> Result<E>;
131
132 fn average_weight(&self) -> Result<f64>;
134}
135
136impl<N: Node, E: EdgeWeight, Ix: petgraph::graph::IndexType> WeightedOps<N, E> for Graph<N, E, Ix>
137where
138 N: Clone + std::fmt::Debug,
139 E: Clone
140 + std::fmt::Debug
141 + Into<f64>
142 + From<f64>
143 + PartialOrd
144 + num_traits::Zero
145 + std::ops::Add<Output = E>
146 + std::ops::Div<f64, Output = E>
147 + std::ops::Mul<f64, Output = E>,
148{
149 fn weight_statistics(&self) -> Result<WeightStatistics<E>> {
150 let edges = self.edges();
151 if edges.is_empty() {
152 return Err(GraphError::InvalidGraph("No edges in graph".to_string()));
153 }
154
155 let mut min_weight = edges[0].weight.clone();
156 let mut max_weight = edges[0].weight.clone();
157 let mut total_weight = E::zero();
158
159 for edge in &edges {
160 if edge.weight < min_weight {
161 min_weight = edge.weight.clone();
162 }
163 if edge.weight > max_weight {
164 max_weight = edge.weight.clone();
165 }
166 total_weight = total_weight + edge.weight.clone();
167 }
168
169 Ok(WeightStatistics {
170 min_weight,
171 max_weight,
172 total_weight,
173 edge_count: edges.len(),
174 })
175 }
176
177 fn filter_by_weight(&self, min_weight: Option<E>, maxweight: Option<E>) -> Result<Self> {
178 let mut filtered_graph = Graph::new();
179
180 for node in self.nodes() {
182 filtered_graph.add_node(node.clone());
183 }
184
185 for edge in self.edges() {
187 let mut include = true;
188
189 if let Some(ref min) = min_weight {
190 if edge.weight < *min {
191 include = false;
192 }
193 }
194
195 if let Some(ref max) = maxweight {
196 if edge.weight > *max {
197 include = false;
198 }
199 }
200
201 if include {
202 filtered_graph.add_edge(
203 edge.source.clone(),
204 edge.target.clone(),
205 edge.weight.clone(),
206 )?;
207 }
208 }
209
210 Ok(filtered_graph)
211 }
212
213 fn edges_by_weight(&self, ascending: bool) -> Result<Vec<(N, N, E)>> {
214 let mut edges: Vec<_> = self
215 .edges()
216 .into_iter()
217 .map(|edge| (edge.source, edge.target, edge.weight))
218 .collect();
219
220 edges.sort_by(|a, b| {
221 if ascending {
222 a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal)
223 } else {
224 b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal)
225 }
226 });
227
228 Ok(edges)
229 }
230
231 fn subgraph_by_weight_range(&self, min_weight: E, maxweight: E) -> Result<Self> {
232 self.filter_by_weight(Some(min_weight), Some(maxweight))
233 }
234
235 fn normalize_weights(&self, method: NormalizationMethod) -> Result<Self> {
236 let edges = self.edges();
237 if edges.is_empty() {
238 return Ok(Graph::new());
239 }
240
241 let weights: Vec<f64> = edges.iter().map(|e| e.weight.clone().into()).collect();
242
243 let normalized_weights = match method {
244 NormalizationMethod::MinMax => {
245 let min_val = weights.iter().cloned().fold(f64::INFINITY, f64::min);
246 let max_val = weights.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
247 let range = max_val - min_val;
248 if range == 0.0 {
249 vec![0.5; weights.len()]
250 } else {
251 weights.iter().map(|w| (w - min_val) / range).collect()
252 }
253 }
254 NormalizationMethod::ZScore => {
255 let mean = weights.iter().sum::<f64>() / weights.len() as f64;
256 let variance =
257 weights.iter().map(|w| (w - mean).powi(2)).sum::<f64>() / weights.len() as f64;
258 let std_dev = variance.sqrt();
259 if std_dev == 0.0 {
260 vec![0.0; weights.len()]
261 } else {
262 weights.iter().map(|w| (w - mean) / std_dev).collect()
263 }
264 }
265 NormalizationMethod::L1 => {
266 let sum = weights.iter().sum::<f64>();
267 if sum == 0.0 {
268 weights.iter().map(|_| 1.0 / weights.len() as f64).collect()
269 } else {
270 weights.iter().map(|w| w / sum).collect()
271 }
272 }
273 NormalizationMethod::L2 => {
274 let norm = weights.iter().map(|w| w * w).sum::<f64>().sqrt();
275 if norm == 0.0 {
276 weights
277 .iter()
278 .map(|_| 1.0 / (weights.len() as f64).sqrt())
279 .collect()
280 } else {
281 weights.iter().map(|w| w / norm).collect()
282 }
283 }
284 NormalizationMethod::Robust => {
285 let mut sorted_weights = weights.clone();
286 sorted_weights.sort_by(|a, b| a.partial_cmp(b).unwrap());
287 let median = if sorted_weights.len() % 2 == 0 {
288 (sorted_weights[sorted_weights.len() / 2 - 1]
289 + sorted_weights[sorted_weights.len() / 2])
290 / 2.0
291 } else {
292 sorted_weights[sorted_weights.len() / 2]
293 };
294 let mad: Vec<f64> = sorted_weights.iter().map(|w| (w - median).abs()).collect();
295 let mut sorted_mad = mad.clone();
296 sorted_mad.sort_by(|a, b| a.partial_cmp(b).unwrap());
297 let mad_median = if sorted_mad.len() % 2 == 0 {
298 (sorted_mad[sorted_mad.len() / 2 - 1] + sorted_mad[sorted_mad.len() / 2]) / 2.0
299 } else {
300 sorted_mad[sorted_mad.len() / 2]
301 };
302 if mad_median == 0.0 {
303 vec![0.0; weights.len()]
304 } else {
305 weights.iter().map(|w| (w - median) / mad_median).collect()
306 }
307 }
308 };
309
310 let mut normalized_graph = Graph::new();
311
312 for node in self.nodes() {
314 normalized_graph.add_node(node.clone());
315 }
316
317 for (edge, &norm_weight) in edges.iter().zip(normalized_weights.iter()) {
319 normalized_graph.add_edge(
320 edge.source.clone(),
321 edge.target.clone(),
322 E::from(norm_weight),
323 )?;
324 }
325
326 Ok(normalized_graph)
327 }
328
329 fn transform_weights(&self, transform: WeightTransform) -> Result<Self> {
330 let mut transformed_graph = Graph::new();
331
332 for node in self.nodes() {
334 transformed_graph.add_node(node.clone());
335 }
336
337 for edge in self.edges() {
339 let weight_f64: f64 = edge.weight.clone().into();
340 let transformed_weight = match transform {
341 WeightTransform::Linear { a, b } => a * weight_f64 + b,
342 WeightTransform::Logarithmic { offset } => (weight_f64 + offset).ln(),
343 WeightTransform::Exponential => weight_f64.exp(),
344 WeightTransform::Power { power } => weight_f64.powf(power),
345 WeightTransform::Inverse => {
346 if weight_f64 == 0.0 {
347 return Err(GraphError::InvalidGraph(
348 "Cannot invert zero _weight".to_string(),
349 ));
350 }
351 1.0 / weight_f64
352 }
353 WeightTransform::SquareRoot => {
354 if weight_f64 < 0.0 {
355 return Err(GraphError::InvalidGraph(
356 "Cannot take square root of negative _weight".to_string(),
357 ));
358 }
359 weight_f64.sqrt()
360 }
361 };
362
363 transformed_graph.add_edge(
364 edge.source.clone(),
365 edge.target.clone(),
366 E::from(transformed_weight),
367 )?;
368 }
369
370 Ok(transformed_graph)
371 }
372
373 fn weight_distribution(&self, bins: usize) -> Result<Vec<(E, usize)>> {
374 let edges = self.edges();
375 if edges.is_empty() || bins == 0 {
376 return Ok(vec![]);
377 }
378
379 let weights: Vec<f64> = edges.iter().map(|e| e.weight.clone().into()).collect();
380 let min_weight = weights.iter().cloned().fold(f64::INFINITY, f64::min);
381 let max_weight = weights.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
382
383 if min_weight == max_weight {
384 return Ok(vec![(E::from(min_weight), weights.len())]);
385 }
386
387 let bin_width = (max_weight - min_weight) / bins as f64;
388 let mut histogram = vec![0; bins];
389 let mut bin_centers = Vec::new();
390
391 for i in 0..bins {
392 bin_centers.push(E::from(min_weight + (i as f64 + 0.5) * bin_width));
393 }
394
395 for weight in weights {
396 let bin_index = ((weight - min_weight) / bin_width) as usize;
397 let bin_index = bin_index.min(bins - 1);
398 histogram[bin_index] += 1;
399 }
400
401 Ok(bin_centers.into_iter().zip(histogram).collect())
402 }
403
404 fn weighted_degree_centrality(&self) -> Result<HashMap<N, f64>> {
405 let mut centrality = HashMap::new();
406
407 for node in self.nodes() {
408 let mut weighted_degree = 0.0;
409
410 if let Ok(neighbors) = self.neighbors(node) {
411 for neighbor in neighbors {
412 if let Ok(weight) = self.edge_weight(node, &neighbor) {
413 weighted_degree += weight.into();
414 }
415 }
416 }
417
418 centrality.insert(node.clone(), weighted_degree);
419 }
420
421 Ok(centrality)
422 }
423
424 fn total_weight(&self) -> Result<E> {
425 let mut total = E::zero();
426 for edge in self.edges() {
427 total = total + edge.weight.clone();
428 }
429 Ok(total)
430 }
431
432 fn average_weight(&self) -> Result<f64> {
433 let edges = self.edges();
434 if edges.is_empty() {
435 return Err(GraphError::InvalidGraph("No edges in graph".to_string()));
436 }
437
438 let total: f64 = edges.iter().map(|e| e.weight.clone().into()).sum();
439 Ok(total / edges.len() as f64)
440 }
441}
442
443impl<N: Node, E: EdgeWeight, Ix: petgraph::graph::IndexType> WeightedOps<N, E> for DiGraph<N, E, Ix>
444where
445 N: Clone + std::fmt::Debug,
446 E: Clone
447 + std::fmt::Debug
448 + Into<f64>
449 + From<f64>
450 + PartialOrd
451 + num_traits::Zero
452 + std::ops::Add<Output = E>
453 + std::ops::Div<f64, Output = E>
454 + std::ops::Mul<f64, Output = E>,
455{
456 fn weight_statistics(&self) -> Result<WeightStatistics<E>> {
457 let edges = self.edges();
458 if edges.is_empty() {
459 return Err(GraphError::InvalidGraph("No edges in graph".to_string()));
460 }
461
462 let mut min_weight = edges[0].weight.clone();
463 let mut max_weight = edges[0].weight.clone();
464 let mut total_weight = E::zero();
465
466 for edge in &edges {
467 if edge.weight < min_weight {
468 min_weight = edge.weight.clone();
469 }
470 if edge.weight > max_weight {
471 max_weight = edge.weight.clone();
472 }
473 total_weight = total_weight + edge.weight.clone();
474 }
475
476 Ok(WeightStatistics {
477 min_weight,
478 max_weight,
479 total_weight,
480 edge_count: edges.len(),
481 })
482 }
483
484 fn filter_by_weight(&self, min_weight: Option<E>, maxweight: Option<E>) -> Result<Self> {
485 let mut filtered_graph = DiGraph::new();
486
487 for node in self.nodes() {
489 filtered_graph.add_node(node.clone());
490 }
491
492 for edge in self.edges() {
494 let mut include = true;
495
496 if let Some(ref min) = min_weight {
497 if edge.weight < *min {
498 include = false;
499 }
500 }
501
502 if let Some(ref max) = maxweight {
503 if edge.weight > *max {
504 include = false;
505 }
506 }
507
508 if include {
509 filtered_graph.add_edge(
510 edge.source.clone(),
511 edge.target.clone(),
512 edge.weight.clone(),
513 )?;
514 }
515 }
516
517 Ok(filtered_graph)
518 }
519
520 fn edges_by_weight(&self, ascending: bool) -> Result<Vec<(N, N, E)>> {
521 let mut edges: Vec<_> = self
522 .edges()
523 .into_iter()
524 .map(|edge| (edge.source, edge.target, edge.weight))
525 .collect();
526
527 edges.sort_by(|a, b| {
528 if ascending {
529 a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal)
530 } else {
531 b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal)
532 }
533 });
534
535 Ok(edges)
536 }
537
538 fn subgraph_by_weight_range(&self, min_weight: E, maxweight: E) -> Result<Self> {
539 self.filter_by_weight(Some(min_weight), Some(maxweight))
540 }
541
542 fn normalize_weights(&self, method: NormalizationMethod) -> Result<Self> {
543 let edges = self.edges();
544 if edges.is_empty() {
545 return Ok(DiGraph::new());
546 }
547
548 let weights: Vec<f64> = edges.iter().map(|e| e.weight.clone().into()).collect();
549
550 let normalized_weights = match method {
551 NormalizationMethod::MinMax => {
552 let min_val = weights.iter().cloned().fold(f64::INFINITY, f64::min);
553 let max_val = weights.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
554 let range = max_val - min_val;
555 if range == 0.0 {
556 vec![0.5; weights.len()]
557 } else {
558 weights.iter().map(|w| (w - min_val) / range).collect()
559 }
560 }
561 NormalizationMethod::ZScore => {
562 let mean = weights.iter().sum::<f64>() / weights.len() as f64;
563 let variance =
564 weights.iter().map(|w| (w - mean).powi(2)).sum::<f64>() / weights.len() as f64;
565 let std_dev = variance.sqrt();
566 if std_dev == 0.0 {
567 vec![0.0; weights.len()]
568 } else {
569 weights.iter().map(|w| (w - mean) / std_dev).collect()
570 }
571 }
572 NormalizationMethod::L1 => {
573 let sum = weights.iter().sum::<f64>();
574 if sum == 0.0 {
575 weights.iter().map(|_| 1.0 / weights.len() as f64).collect()
576 } else {
577 weights.iter().map(|w| w / sum).collect()
578 }
579 }
580 NormalizationMethod::L2 => {
581 let norm = weights.iter().map(|w| w * w).sum::<f64>().sqrt();
582 if norm == 0.0 {
583 weights
584 .iter()
585 .map(|_| 1.0 / (weights.len() as f64).sqrt())
586 .collect()
587 } else {
588 weights.iter().map(|w| w / norm).collect()
589 }
590 }
591 NormalizationMethod::Robust => {
592 let mut sorted_weights = weights.clone();
593 sorted_weights.sort_by(|a, b| a.partial_cmp(b).unwrap());
594 let median = if sorted_weights.len() % 2 == 0 {
595 (sorted_weights[sorted_weights.len() / 2 - 1]
596 + sorted_weights[sorted_weights.len() / 2])
597 / 2.0
598 } else {
599 sorted_weights[sorted_weights.len() / 2]
600 };
601 let mad: Vec<f64> = sorted_weights.iter().map(|w| (w - median).abs()).collect();
602 let mut sorted_mad = mad.clone();
603 sorted_mad.sort_by(|a, b| a.partial_cmp(b).unwrap());
604 let mad_median = if sorted_mad.len() % 2 == 0 {
605 (sorted_mad[sorted_mad.len() / 2 - 1] + sorted_mad[sorted_mad.len() / 2]) / 2.0
606 } else {
607 sorted_mad[sorted_mad.len() / 2]
608 };
609 if mad_median == 0.0 {
610 vec![0.0; weights.len()]
611 } else {
612 weights.iter().map(|w| (w - median) / mad_median).collect()
613 }
614 }
615 };
616
617 let mut normalized_graph = DiGraph::new();
618
619 for node in self.nodes() {
621 normalized_graph.add_node(node.clone());
622 }
623
624 for (edge, &norm_weight) in edges.iter().zip(normalized_weights.iter()) {
626 normalized_graph.add_edge(
627 edge.source.clone(),
628 edge.target.clone(),
629 E::from(norm_weight),
630 )?;
631 }
632
633 Ok(normalized_graph)
634 }
635
636 fn transform_weights(&self, transform: WeightTransform) -> Result<Self> {
637 let mut transformed_graph = DiGraph::new();
638
639 for node in self.nodes() {
641 transformed_graph.add_node(node.clone());
642 }
643
644 for edge in self.edges() {
646 let weight_f64: f64 = edge.weight.clone().into();
647 let transformed_weight = match transform {
648 WeightTransform::Linear { a, b } => a * weight_f64 + b,
649 WeightTransform::Logarithmic { offset } => (weight_f64 + offset).ln(),
650 WeightTransform::Exponential => weight_f64.exp(),
651 WeightTransform::Power { power } => weight_f64.powf(power),
652 WeightTransform::Inverse => {
653 if weight_f64 == 0.0 {
654 return Err(GraphError::InvalidGraph(
655 "Cannot invert zero weight".to_string(),
656 ));
657 }
658 1.0 / weight_f64
659 }
660 WeightTransform::SquareRoot => {
661 if weight_f64 < 0.0 {
662 return Err(GraphError::InvalidGraph(
663 "Cannot take square root of negative weight".to_string(),
664 ));
665 }
666 weight_f64.sqrt()
667 }
668 };
669
670 transformed_graph.add_edge(
671 edge.source.clone(),
672 edge.target.clone(),
673 E::from(transformed_weight),
674 )?;
675 }
676
677 Ok(transformed_graph)
678 }
679
680 fn weight_distribution(&self, bins: usize) -> Result<Vec<(E, usize)>> {
681 let edges = self.edges();
682 if edges.is_empty() || bins == 0 {
683 return Ok(vec![]);
684 }
685
686 let weights: Vec<f64> = edges.iter().map(|e| e.weight.clone().into()).collect();
687 let min_weight = weights.iter().cloned().fold(f64::INFINITY, f64::min);
688 let max_weight = weights.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
689
690 if min_weight == max_weight {
691 return Ok(vec![(E::from(min_weight), weights.len())]);
692 }
693
694 let bin_width = (max_weight - min_weight) / bins as f64;
695 let mut histogram = vec![0; bins];
696 let mut bin_centers = Vec::new();
697
698 for i in 0..bins {
699 bin_centers.push(E::from(min_weight + (i as f64 + 0.5) * bin_width));
700 }
701
702 for weight in weights {
703 let bin_index = ((weight - min_weight) / bin_width) as usize;
704 let bin_index = bin_index.min(bins - 1);
705 histogram[bin_index] += 1;
706 }
707
708 Ok(bin_centers.into_iter().zip(histogram).collect())
709 }
710
711 fn weighted_degree_centrality(&self) -> Result<HashMap<N, f64>> {
712 let mut centrality = HashMap::new();
713
714 for node in self.nodes() {
715 let mut weighted_out_degree = 0.0;
716 let mut weighted_in_degree = 0.0;
717
718 if let Ok(successors) = self.successors(node) {
720 for successor in successors {
721 if let Ok(weight) = self.edge_weight(node, &successor) {
722 weighted_out_degree += weight.into();
723 }
724 }
725 }
726
727 if let Ok(predecessors) = self.predecessors(node) {
729 for predecessor in predecessors {
730 if let Ok(weight) = self.edge_weight(&predecessor, node) {
731 weighted_in_degree += weight.into();
732 }
733 }
734 }
735
736 centrality.insert(node.clone(), weighted_out_degree + weighted_in_degree);
737 }
738
739 Ok(centrality)
740 }
741
742 fn total_weight(&self) -> Result<E> {
743 let mut total = E::zero();
744 for edge in self.edges() {
745 total = total + edge.weight.clone();
746 }
747 Ok(total)
748 }
749
750 fn average_weight(&self) -> Result<f64> {
751 let edges = self.edges();
752 if edges.is_empty() {
753 return Err(GraphError::InvalidGraph("No edges in graph".to_string()));
754 }
755
756 let total: f64 = edges.iter().map(|e| e.weight.clone().into()).sum();
757 Ok(total / edges.len() as f64)
758 }
759}
760
761pub mod utils {
763 use super::*;
764
765 pub fn weight_threshold_filter<E: EdgeWeight + PartialOrd>(
767 threshold: E,
768 above: bool,
769 ) -> impl Fn(&E) -> bool {
770 move |weight| {
771 if above {
772 *weight >= threshold
773 } else {
774 *weight < threshold
775 }
776 }
777 }
778
779 pub fn merge_weighted_graphs<N, E>(
781 graph1: &Graph<N, E>,
782 graph2: &Graph<N, E>,
783 ) -> Result<Graph<N, E>>
784 where
785 N: Node + Clone + std::fmt::Debug,
786 E: EdgeWeight + Clone + std::fmt::Debug + num_traits::Zero + std::ops::Add<Output = E>,
787 {
788 let mut merged = Graph::new();
789
790 for node in graph1.nodes() {
792 merged.add_node(node.clone());
793 }
794 for node in graph2.nodes() {
795 merged.add_node(node.clone());
796 }
797
798 for edge in graph1.edges() {
800 merged.add_edge(
801 edge.source.clone(),
802 edge.target.clone(),
803 edge.weight.clone(),
804 )?;
805 }
806
807 for edge in graph2.edges() {
809 if merged.has_edge(&edge.source, &edge.target) {
810 let existing_weight = merged.edge_weight(&edge.source, &edge.target)?;
812 let _new_weight = existing_weight + edge.weight.clone();
813
814 merged.add_edge(
818 edge.source.clone(),
819 edge.target.clone(),
820 edge.weight.clone(),
821 )?;
822 } else {
823 merged.add_edge(
825 edge.source.clone(),
826 edge.target.clone(),
827 edge.weight.clone(),
828 )?;
829 }
830 }
831
832 Ok(merged)
833 }
834
835 pub fn weight_correlation<N, E>(graph1: &Graph<N, E>, graph2: &Graph<N, E>) -> Result<f64>
837 where
838 N: Node + Clone + std::fmt::Debug,
839 E: EdgeWeight + Clone + std::fmt::Debug + Into<f64>,
840 {
841 let edges1 = graph1.edges();
842 let edges2 = graph2.edges();
843
844 if edges1.len() != edges2.len() {
845 return Err(GraphError::InvalidGraph(
846 "Graphs must have same number of edges".to_string(),
847 ));
848 }
849
850 let weights1: Vec<f64> = edges1.iter().map(|e| e.weight.clone().into()).collect();
851 let weights2: Vec<f64> = edges2.iter().map(|e| e.weight.clone().into()).collect();
852
853 let mean1 = weights1.iter().sum::<f64>() / weights1.len() as f64;
854 let mean2 = weights2.iter().sum::<f64>() / weights2.len() as f64;
855
856 let numerator: f64 = weights1
857 .iter()
858 .zip(weights2.iter())
859 .map(|(w1, w2)| (w1 - mean1) * (w2 - mean2))
860 .sum();
861
862 let var1: f64 = weights1.iter().map(|w| (w - mean1).powi(2)).sum();
863 let var2: f64 = weights2.iter().map(|w| (w - mean2).powi(2)).sum();
864
865 let denominator = (var1 * var2).sqrt();
866
867 if denominator == 0.0 {
868 Ok(0.0)
869 } else {
870 Ok(numerator / denominator)
871 }
872 }
873}
874
875#[cfg(test)]
876mod tests {
877 use super::*;
878
879 #[test]
880 fn test_weight_statistics() {
881 let mut graph: Graph<i32, f64> = Graph::new();
882 graph.add_edge(1, 2, 1.0).unwrap();
883 graph.add_edge(2, 3, 2.0).unwrap();
884 graph.add_edge(3, 4, 3.0).unwrap();
885
886 let stats = graph.weight_statistics().unwrap();
887 assert_eq!(stats.min_weight, 1.0);
888 assert_eq!(stats.max_weight, 3.0);
889 assert_eq!(stats.total_weight, 6.0);
890 assert_eq!(stats.edge_count, 3);
891 }
892
893 #[test]
894 fn test_filter_by_weight() {
895 let mut graph: Graph<i32, f64> = Graph::new();
896 graph.add_edge(1, 2, 1.0).unwrap();
897 graph.add_edge(2, 3, 2.0).unwrap();
898 graph.add_edge(3, 4, 3.0).unwrap();
899
900 let filtered = graph.filter_by_weight(Some(2.0), None).unwrap();
901 assert_eq!(filtered.edge_count(), 2);
902 }
903
904 #[test]
905 fn test_normalize_weights() {
906 let mut graph: Graph<i32, f64> = Graph::new();
907 graph.add_edge(1, 2, 1.0).unwrap();
908 graph.add_edge(2, 3, 2.0).unwrap();
909 graph.add_edge(3, 4, 3.0).unwrap();
910
911 let normalized = graph
912 .normalize_weights(NormalizationMethod::MinMax)
913 .unwrap();
914 let edges = normalized.edges();
915
916 for edge in edges {
918 let weight = edge.weight;
919 assert!((0.0..=1.0).contains(&weight));
920 }
921 }
922
923 #[test]
924 fn test_transform_weights() {
925 let mut graph: Graph<i32, f64> = Graph::new();
926 graph.add_edge(1, 2, 1.0).unwrap();
927 graph.add_edge(2, 3, 2.0).unwrap();
928
929 let transformed = graph
930 .transform_weights(WeightTransform::Linear { a: 2.0, b: 1.0 })
931 .unwrap();
932 let edges = transformed.edges();
933
934 assert_eq!(edges[0].weight, 3.0); assert_eq!(edges[1].weight, 5.0); }
937
938 #[test]
939 fn test_weight_distribution() {
940 let mut graph: Graph<i32, f64> = Graph::new();
941 graph.add_edge(1, 2, 1.0).unwrap();
942 graph.add_edge(2, 3, 2.0).unwrap();
943 graph.add_edge(3, 4, 3.0).unwrap();
944
945 let distribution = graph.weight_distribution(3).unwrap();
946 assert_eq!(distribution.len(), 3);
947 }
948
949 #[test]
950 fn test_weighted_degree_centrality() {
951 let mut graph: Graph<i32, f64> = Graph::new();
952 graph.add_edge(1, 2, 1.0).unwrap();
953 graph.add_edge(2, 3, 2.0).unwrap();
954 graph.add_edge(2, 4, 3.0).unwrap();
955
956 let centrality = graph.weighted_degree_centrality().unwrap();
957 assert_eq!(centrality[&2], 6.0); }
959
960 #[test]
961 fn test_total_weight() {
962 let mut graph: Graph<i32, f64> = Graph::new();
963 graph.add_edge(1, 2, 1.5).unwrap();
964 graph.add_edge(2, 3, 2.5).unwrap();
965
966 let total = graph.total_weight().unwrap();
967 assert_eq!(total, 4.0);
968 }
969
970 #[test]
971 fn test_average_weight() {
972 let mut graph: Graph<i32, f64> = Graph::new();
973 graph.add_edge(1, 2, 1.0).unwrap();
974 graph.add_edge(2, 3, 3.0).unwrap();
975
976 let avg = graph.average_weight().unwrap();
977 assert_eq!(avg, 2.0);
978 }
979
980 #[test]
981 fn test_multi_weight() {
982 let mut multi_weight = MultiWeight::new(5.0);
983 multi_weight.add_weight("distance".to_string(), 10.0);
984 multi_weight.add_weight("time".to_string(), 15.0);
985
986 assert_eq!(multi_weight.primary, 5.0);
987 assert_eq!(multi_weight.get_weight("distance"), Some(&10.0));
988 assert_eq!(multi_weight.get_weight("time"), Some(&15.0));
989 assert_eq!(multi_weight.get_weight("cost"), None);
990 }
991
992 #[test]
993 fn test_edges_by_weight() {
994 let mut graph: Graph<i32, f64> = Graph::new();
995 graph.add_edge(1, 2, 3.0).unwrap();
996 graph.add_edge(2, 3, 1.0).unwrap();
997 graph.add_edge(3, 4, 2.0).unwrap();
998
999 let sorted_edges = graph.edges_by_weight(true).unwrap();
1000 assert_eq!(sorted_edges[0].2, 1.0);
1001 assert_eq!(sorted_edges[1].2, 2.0);
1002 assert_eq!(sorted_edges[2].2, 3.0);
1003
1004 let sorted_edges_desc = graph.edges_by_weight(false).unwrap();
1005 assert_eq!(sorted_edges_desc[0].2, 3.0);
1006 assert_eq!(sorted_edges_desc[1].2, 2.0);
1007 assert_eq!(sorted_edges_desc[2].2, 1.0);
1008 }
1009}