rust_igraph/algorithms/properties/
closeness_weighted.rs1use crate::algorithms::paths::dijkstra::dijkstra_distances;
20use crate::core::{Graph, IgraphResult};
21
22pub fn closeness_weighted(graph: &Graph, weights: &[f64]) -> IgraphResult<Vec<Option<f64>>> {
46 let n = graph.vcount();
47 let n_us = n as usize;
48 let mut out = Vec::with_capacity(n_us);
49 for v in 0..n {
50 let d = dijkstra_distances(graph, v, weights)?;
51 let mut sum_dist: f64 = 0.0;
52 let mut reach: u64 = 0;
53 let v_us = v as usize;
54 for (target, val) in d.iter().enumerate() {
55 if target == v_us {
56 continue;
57 }
58 if let Some(dist) = val {
59 sum_dist += *dist;
60 reach += 1;
61 }
62 }
63 if reach == 0 || sum_dist == 0.0 {
64 out.push(None);
69 } else {
70 #[allow(clippy::cast_precision_loss)]
71 let val = (reach as f64) / sum_dist;
72 out.push(Some(val));
73 }
74 }
75 Ok(out)
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 fn approx_eq(a: Option<f64>, b: Option<f64>, tol: f64) {
83 match (a, b) {
84 (Some(x), Some(y)) => {
85 assert!((x - y).abs() < tol, "{x} vs {y}");
86 }
87 (None, None) => {}
88 _ => panic!("{a:?} vs {b:?}"),
89 }
90 }
91
92 #[test]
93 fn empty_graph_yields_empty() {
94 let g = Graph::with_vertices(0);
95 assert!(closeness_weighted(&g, &[]).unwrap().is_empty());
96 }
97
98 #[test]
99 fn isolated_vertices_all_none() {
100 let g = Graph::with_vertices(3);
101 let c = closeness_weighted(&g, &[]).unwrap();
102 assert_eq!(c, vec![None, None, None]);
103 }
104
105 #[test]
106 fn singleton_is_none() {
107 let g = Graph::with_vertices(1);
108 assert_eq!(closeness_weighted(&g, &[]).unwrap(), vec![None]);
109 }
110
111 #[test]
112 fn star_with_uniform_weights_matches_unweighted() {
113 let mut g = Graph::with_vertices(4);
114 for v in 1..4 {
115 g.add_edge(0, v).unwrap();
116 }
117 let c = closeness_weighted(&g, &[1.0, 1.0, 1.0]).unwrap();
118 approx_eq(c[0], Some(1.0), 1e-12);
120 for &leaf_val in &c[1..4] {
122 approx_eq(leaf_val, Some(0.6), 1e-12);
123 }
124 }
125
126 #[test]
127 fn star_with_non_uniform_weights() {
128 let mut g = Graph::with_vertices(4);
129 g.add_edge(0, 1).unwrap();
130 g.add_edge(0, 2).unwrap();
131 g.add_edge(0, 3).unwrap();
132 let c = closeness_weighted(&g, &[1.0, 2.0, 3.0]).unwrap();
133 approx_eq(c[0], Some(0.5), 1e-12);
135 approx_eq(c[1], Some(3.0 / 8.0), 1e-12);
137 approx_eq(c[2], Some(0.3), 1e-12);
139 approx_eq(c[3], Some(0.25), 1e-12);
141 }
142
143 #[test]
144 fn directed_path_uses_out_edges() {
145 let mut g = Graph::new(3, true).unwrap();
146 g.add_edge(0, 1).unwrap();
147 g.add_edge(1, 2).unwrap();
148 let c = closeness_weighted(&g, &[1.0, 1.0]).unwrap();
149 approx_eq(c[0], Some(2.0 / 3.0), 1e-12);
151 approx_eq(c[1], Some(1.0), 1e-12);
153 assert_eq!(c[2], None);
155 }
156
157 #[test]
158 fn disconnected_components_within_component_only() {
159 let mut g = Graph::with_vertices(4);
160 g.add_edge(0, 1).unwrap();
161 g.add_edge(1, 2).unwrap();
162 let c = closeness_weighted(&g, &[1.0, 1.0]).unwrap();
163 approx_eq(c[0], Some(2.0 / 3.0), 1e-12);
164 approx_eq(c[1], Some(1.0), 1e-12);
165 approx_eq(c[2], Some(2.0 / 3.0), 1e-12);
166 assert_eq!(c[3], None);
167 }
168
169 #[test]
170 fn unit_weights_match_unweighted() {
171 let mut g = Graph::with_vertices(3);
173 g.add_edge(0, 1).unwrap();
174 g.add_edge(1, 2).unwrap();
175 g.add_edge(2, 0).unwrap();
176 let cw = closeness_weighted(&g, &[1.0; 3]).unwrap();
177 let cu = crate::closeness(&g).unwrap();
178 for (a, b) in cw.iter().zip(cu.iter()) {
179 approx_eq(*a, *b, 1e-12);
180 }
181 }
182
183 #[test]
184 fn weights_size_mismatch_errors() {
185 let mut g = Graph::with_vertices(2);
186 g.add_edge(0, 1).unwrap();
187 assert!(closeness_weighted(&g, &[]).is_err());
188 assert!(closeness_weighted(&g, &[1.0, 2.0]).is_err());
189 }
190
191 #[test]
192 fn negative_weight_errors() {
193 let mut g = Graph::with_vertices(2);
194 g.add_edge(0, 1).unwrap();
195 assert!(closeness_weighted(&g, &[-1.0]).is_err());
196 }
197}