scirs2_graph/
weighted.rs

1//! Specialized weighted graph operations and analysis
2//!
3//! This module provides specialized APIs for working with weighted graphs,
4//! including weight statistics, filtering, normalization, and transformation operations.
5
6use crate::base::{DiGraph, EdgeWeight, Graph, Node};
7use crate::error::{GraphError, Result};
8use std::collections::HashMap;
9
10/// Weight statistics for a graph
11#[derive(Debug, Clone)]
12pub struct WeightStatistics<E: EdgeWeight> {
13    /// Minimum weight in the graph
14    pub min_weight: E,
15    /// Maximum weight in the graph
16    pub max_weight: E,
17    /// Sum of all weights
18    pub total_weight: E,
19    /// Number of edges
20    pub edge_count: usize,
21}
22
23/// Weight normalization methods
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum NormalizationMethod {
26    /// Min-max normalization to [0,1]
27    MinMax,
28    /// Z-score normalization (standardization)
29    ZScore,
30    /// L1 normalization (sum to 1)
31    L1,
32    /// L2 normalization (unit norm)
33    L2,
34    /// Robust normalization using median and MAD
35    Robust,
36}
37
38/// Weight transformation functions
39#[derive(Debug, Clone, Copy, PartialEq)]
40pub enum WeightTransform {
41    /// Linear transformation: ax + b
42    Linear {
43        /// Multiplier coefficient
44        a: f64,
45        /// Additive constant
46        b: f64,
47    },
48    /// Logarithmic transformation: log(x + offset)
49    Logarithmic {
50        /// Offset to add before taking logarithm
51        offset: f64,
52    },
53    /// Exponential transformation: exp(x)
54    Exponential,
55    /// Power transformation: x^power
56    Power {
57        /// Power exponent
58        power: f64,
59    },
60    /// Inverse transformation: 1/x
61    Inverse,
62    /// Square root transformation
63    SquareRoot,
64}
65
66/// Multi-weight edge for graphs with multiple edge attributes
67#[derive(Debug, Clone)]
68pub struct MultiWeight<E: EdgeWeight> {
69    /// Primary weight
70    pub primary: E,
71    /// Additional weights with names
72    pub weights: HashMap<String, E>,
73}
74
75impl<E: EdgeWeight> MultiWeight<E> {
76    /// Create a new multi-weight with just primary weight
77    pub fn new(primary: E) -> Self {
78        Self {
79            primary,
80            weights: HashMap::new(),
81        }
82    }
83
84    /// Add a named weight
85    pub fn add_weight(&mut self, name: String, weight: E) {
86        self.weights.insert(name, weight);
87    }
88
89    /// Get a named weight
90    pub fn get_weight(&self, name: &str) -> Option<&E> {
91        self.weights.get(name)
92    }
93}
94
95/// Weighted graph operations trait
96pub trait WeightedOps<N: Node, E: EdgeWeight> {
97    /// Calculate weight statistics for the graph
98    fn weight_statistics(&self) -> Result<WeightStatistics<E>>;
99
100    /// Filter edges by weight threshold
101    fn filter_by_weight(&self, min_weight: Option<E>, maxweight: Option<E>) -> Result<Self>
102    where
103        Self: Sized;
104
105    /// Get edges sorted by weight
106    fn edges_by_weight(&self, ascending: bool) -> Result<Vec<(N, N, E)>>;
107
108    /// Extract subgraph with edges in weight range
109    fn subgraph_by_weight_range(&self, min_weight: E, maxweight: E) -> Result<Self>
110    where
111        Self: Sized;
112
113    /// Normalize edge weights using specified method
114    fn normalize_weights(&self, method: NormalizationMethod) -> Result<Self>
115    where
116        Self: Sized;
117
118    /// Transform edge weights using specified transformation
119    fn transform_weights(&self, transform: WeightTransform) -> Result<Self>
120    where
121        Self: Sized;
122
123    /// Get weight distribution as histogram
124    fn weight_distribution(&self, bins: usize) -> Result<Vec<(E, usize)>>;
125
126    /// Calculate weight-based centrality measures
127    fn weighted_degree_centrality(&self) -> Result<HashMap<N, f64>>;
128
129    /// Get total weight of the graph
130    fn total_weight(&self) -> Result<E>;
131
132    /// Get average edge weight
133    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        // Add all nodes first
181        for node in self.nodes() {
182            filtered_graph.add_node(node.clone());
183        }
184
185        // Add edges that meet _weight criteria
186        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        // Add all nodes
313        for node in self.nodes() {
314            normalized_graph.add_node(node.clone());
315        }
316
317        // Add edges with normalized weights
318        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        // Add all nodes
333        for node in self.nodes() {
334            transformed_graph.add_node(node.clone());
335        }
336
337        // Transform and add edges
338        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        // Add all nodes first
488        for node in self.nodes() {
489            filtered_graph.add_node(node.clone());
490        }
491
492        // Add edges that meet _weight criteria
493        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        // Add all nodes
620        for node in self.nodes() {
621            normalized_graph.add_node(node.clone());
622        }
623
624        // Add edges with normalized weights
625        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        // Add all nodes
640        for node in self.nodes() {
641            transformed_graph.add_node(node.clone());
642        }
643
644        // Transform and add edges
645        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            // Calculate out-degree weight
719            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            // Calculate in-degree weight
728            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
761/// Additional utility functions for weighted graphs
762pub mod utils {
763    use super::*;
764
765    /// Create a weight threshold filter function
766    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    /// Combine two graphs by merging weights
780    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        // Add all nodes from both graphs
791        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        // Add edges from first graph
799        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        // Add or merge edges from second graph
808        for edge in graph2.edges() {
809            if merged.has_edge(&edge.source, &edge.target) {
810                // Edge exists, merge weights
811                let existing_weight = merged.edge_weight(&edge.source, &edge.target)?;
812                let _new_weight = existing_weight + edge.weight.clone();
813
814                // For now, we can't easily update edge weights in place
815                // This is a limitation that could be addressed with a more sophisticated merge
816                // For now, just add the edge with the original weight from the second graph
817                merged.add_edge(
818                    edge.source.clone(),
819                    edge.target.clone(),
820                    edge.weight.clone(),
821                )?;
822            } else {
823                // New edge
824                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    /// Calculate weight correlation between two graphs with same structure
836    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        // Check that weights are in [0, 1] range
917        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); // 2*1 + 1
935        assert_eq!(edges[1].weight, 5.0); // 2*2 + 1
936    }
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); // Node 2 has weighted degree 1+2+3=6
958    }
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}