rust_igraph/algorithms/properties/
assortativity_weighted.rs1use crate::core::graph::EdgeId;
29use crate::core::{Graph, IgraphError, IgraphResult};
30
31pub fn assortativity_degree_weighted(graph: &Graph, weights: &[f64]) -> IgraphResult<Option<f64>> {
52 if graph.is_directed() {
53 return Err(IgraphError::Unsupported(
54 "directed weighted assortativity is PR-006c; not yet ported",
55 ));
56 }
57 let m = graph.ecount();
58 if m == 0 {
59 return Ok(None);
60 }
61 if weights.len() != m {
62 return Err(IgraphError::InvalidArgument(format!(
63 "weights vector size ({}) differs from edge count ({})",
64 weights.len(),
65 m
66 )));
67 }
68 for (e, &w) in weights.iter().enumerate() {
69 if w.is_nan() || w < 0.0 || !w.is_finite() {
70 return Err(IgraphError::InvalidArgument(format!(
71 "weight at edge {e} must be non-negative and finite (got {w})"
72 )));
73 }
74 }
75
76 let n = graph.vcount();
79 let n_us = n as usize;
80 let mut strength = vec![0.0_f64; n_us];
81
82 let m_u = u32::try_from(m).map_err(|_| IgraphError::Internal("ecount overflows u32"))?;
83 for e in 0..m_u {
84 let (u, v) = graph.edge(e as EdgeId)?;
85 let edge_w = weights[e as usize];
86 if u == v {
87 strength[u as usize] += 2.0 * edge_w;
88 } else {
89 strength[u as usize] += edge_w;
90 strength[v as usize] += edge_w;
91 }
92 }
93
94 let mut num1 = 0.0_f64;
95 let mut num2 = 0.0_f64;
96 let mut den1 = 0.0_f64;
97 let mut total_w = 0.0_f64;
98
99 for e in 0..m_u {
100 let (u, v) = graph.edge(e as EdgeId)?;
101 let edge_w = weights[e as usize];
102 let su = strength[u as usize];
103 let sv = strength[v as usize];
104 num1 += edge_w * (su * sv);
105 num2 += edge_w * (su + sv);
106 den1 += edge_w * (su * su + sv * sv);
107 total_w += edge_w;
108 }
109
110 if total_w == 0.0 {
111 return Ok(None);
112 }
113
114 num1 /= total_w;
115 den1 /= 2.0 * total_w;
116 num2 /= 2.0 * total_w;
117 num2 *= num2;
118
119 let denom = den1 - num2;
120 if denom == 0.0 {
121 return Ok(None);
122 }
123 Ok(Some((num1 - num2) / denom))
124}
125
126pub fn assortativity_degree_directed_weighted(
165 graph: &Graph,
166 weights: &[f64],
167) -> IgraphResult<Option<f64>> {
168 if !graph.is_directed() {
169 return assortativity_degree_weighted(graph, weights);
170 }
171 let m = graph.ecount();
172 if m == 0 {
173 return Ok(None);
174 }
175 if weights.len() != m {
176 return Err(IgraphError::InvalidArgument(format!(
177 "weights vector size ({}) differs from edge count ({})",
178 weights.len(),
179 m
180 )));
181 }
182 for (e, &w) in weights.iter().enumerate() {
183 if w.is_nan() || w < 0.0 || !w.is_finite() {
184 return Err(IgraphError::InvalidArgument(format!(
185 "weight at edge {e} must be non-negative and finite (got {w})"
186 )));
187 }
188 }
189
190 let n = graph.vcount();
191 let n_us = n as usize;
192 let mut out_strength = vec![0.0_f64; n_us];
193 let mut in_strength = vec![0.0_f64; n_us];
194
195 let m_u = u32::try_from(m).map_err(|_| IgraphError::Internal("ecount overflows u32"))?;
196 for e in 0..m_u {
197 let (u, v) = graph.edge(e as EdgeId)?;
198 let edge_w = weights[e as usize];
199 out_strength[u as usize] += edge_w;
200 in_strength[v as usize] += edge_w;
201 }
202
203 let mut num1 = 0.0_f64;
204 let mut num2 = 0.0_f64;
205 let mut num3 = 0.0_f64;
206 let mut den1 = 0.0_f64;
207 let mut den2 = 0.0_f64;
208 let mut total_w = 0.0_f64;
209
210 for e in 0..m_u {
211 let (u, v) = graph.edge(e as EdgeId)?;
212 let edge_w = weights[e as usize];
213 let so = out_strength[u as usize];
214 let si = in_strength[v as usize];
215 num1 += edge_w * so * si;
216 num2 += edge_w * so;
217 num3 += edge_w * si;
218 den1 += edge_w * so * so;
219 den2 += edge_w * si * si;
220 total_w += edge_w;
221 }
222
223 if total_w == 0.0 {
224 return Ok(None);
225 }
226 let num = num1 - num2 * num3 / total_w;
227 let var_from = den1 - num2 * num2 / total_w;
228 let var_to = den2 - num3 * num3 / total_w;
229 if var_from <= 0.0 || var_to <= 0.0 {
230 return Ok(None);
231 }
232 let den = var_from.sqrt() * var_to.sqrt();
233 if den == 0.0 {
234 return Ok(None);
235 }
236 Ok(Some(num / den))
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 fn close(a: f64, b: f64, tol: f64) {
244 assert!((a - b).abs() < tol, "{a} vs {b}");
245 }
246
247 #[test]
248 fn empty_graph_is_none() {
249 let g = Graph::with_vertices(0);
250 assert_eq!(assortativity_degree_weighted(&g, &[]).unwrap(), None);
251 }
252
253 #[test]
254 fn no_edges_is_none() {
255 let g = Graph::with_vertices(5);
256 assert_eq!(assortativity_degree_weighted(&g, &[]).unwrap(), None);
257 }
258
259 #[test]
260 fn unit_weights_match_unweighted_path_3() {
261 let mut g = Graph::with_vertices(3);
263 g.add_edge(0, 1).unwrap();
264 g.add_edge(1, 2).unwrap();
265 let r = assortativity_degree_weighted(&g, &[1.0, 1.0])
266 .unwrap()
267 .unwrap();
268 close(r, -1.0, 1e-12);
269 }
270
271 #[test]
272 fn unit_weights_match_unweighted_diamond() {
273 let mut g = Graph::with_vertices(4);
275 for &(u, v) in &[(0u32, 1), (0, 2), (1, 2), (1, 3), (2, 3)] {
276 g.add_edge(u, v).unwrap();
277 }
278 let weights = vec![1.0; 5];
279 let r = assortativity_degree_weighted(&g, &weights)
280 .unwrap()
281 .unwrap();
282 close(r, -2.0 / 3.0, 1e-12);
283 }
284
285 #[test]
286 fn k4_regular_unit_weights_returns_none() {
287 let mut g = Graph::with_vertices(4);
289 for u in 0..4u32 {
290 for v in (u + 1)..4 {
291 g.add_edge(u, v).unwrap();
292 }
293 }
294 assert_eq!(assortativity_degree_weighted(&g, &[1.0; 6]).unwrap(), None);
295 }
296
297 #[test]
298 fn weighted_path_breaks_perfect_disassortativity() {
299 let mut g = Graph::with_vertices(3);
307 g.add_edge(0, 1).unwrap();
308 g.add_edge(1, 2).unwrap();
309 let r = assortativity_degree_weighted(&g, &[1.0, 4.0])
310 .unwrap()
311 .unwrap();
312 close(r, -0.64 / 1.36, 1e-12);
313 }
314
315 #[test]
316 fn weights_size_mismatch_errors() {
317 let mut g = Graph::with_vertices(2);
318 g.add_edge(0, 1).unwrap();
319 assert!(assortativity_degree_weighted(&g, &[]).is_err());
320 }
321
322 #[test]
323 fn negative_weight_errors() {
324 let mut g = Graph::with_vertices(2);
325 g.add_edge(0, 1).unwrap();
326 assert!(assortativity_degree_weighted(&g, &[-1.0]).is_err());
327 }
328
329 #[test]
330 fn nan_weight_errors() {
331 let mut g = Graph::with_vertices(2);
332 g.add_edge(0, 1).unwrap();
333 assert!(assortativity_degree_weighted(&g, &[f64::NAN]).is_err());
334 }
335
336 #[test]
337 fn directed_returns_unsupported() {
338 let mut g = Graph::new(2, true).unwrap();
339 g.add_edge(0, 1).unwrap();
340 assert!(assortativity_degree_weighted(&g, &[1.0]).is_err());
341 }
342
343 #[test]
344 fn zero_total_weight_returns_none() {
345 let mut g = Graph::with_vertices(3);
346 g.add_edge(0, 1).unwrap();
347 g.add_edge(1, 2).unwrap();
348 assert_eq!(
350 assortativity_degree_weighted(&g, &[0.0, 0.0]).unwrap(),
351 None
352 );
353 }
354
355 #[test]
358 fn directed_weighted_3_cycle_uniform_returns_none() {
359 let mut g = Graph::new(3, true).unwrap();
360 g.add_edge(0, 1).unwrap();
361 g.add_edge(1, 2).unwrap();
362 g.add_edge(2, 0).unwrap();
363 assert_eq!(
364 assortativity_degree_directed_weighted(&g, &[1.0, 1.0, 1.0]).unwrap(),
365 None
366 );
367 }
368
369 #[test]
370 fn directed_weighted_unit_weights_match_unweighted_directed() {
371 let mut g = Graph::new(5, true).unwrap();
374 g.add_edge(0, 1).unwrap();
375 g.add_edge(1, 2).unwrap();
376 g.add_edge(2, 3).unwrap();
377 g.add_edge(3, 4).unwrap();
378 let unweighted =
379 crate::algorithms::properties::assortativity::assortativity_degree_directed(&g)
380 .unwrap();
381 let weighted = assortativity_degree_directed_weighted(&g, &[1.0; 4]).unwrap();
382 match (unweighted, weighted) {
383 (Some(u), Some(w)) => close(u, w, 1e-12),
384 (None, None) => {}
385 _ => panic!("disagreement between unweighted={unweighted:?} weighted={weighted:?}"),
386 }
387 }
388
389 #[test]
390 fn directed_weighted_undirected_routes_to_undirected_weighted() {
391 let mut g = Graph::with_vertices(3);
394 g.add_edge(0, 1).unwrap();
395 g.add_edge(1, 2).unwrap();
396 let r1 = assortativity_degree_directed_weighted(&g, &[1.0, 1.0]).unwrap();
397 let r2 = assortativity_degree_weighted(&g, &[1.0, 1.0]).unwrap();
398 assert_eq!(r1, r2);
399 }
400
401 #[test]
402 fn directed_weighted_empty_no_edges_is_none() {
403 let g = Graph::new(3, true).unwrap();
404 assert_eq!(
405 assortativity_degree_directed_weighted(&g, &[]).unwrap(),
406 None
407 );
408 }
409
410 #[test]
411 fn directed_weighted_negative_weight_errors() {
412 let mut g = Graph::new(2, true).unwrap();
413 g.add_edge(0, 1).unwrap();
414 assert!(assortativity_degree_directed_weighted(&g, &[-1.0]).is_err());
415 }
416
417 #[test]
418 fn directed_weighted_size_mismatch_errors() {
419 let mut g = Graph::new(2, true).unwrap();
420 g.add_edge(0, 1).unwrap();
421 assert!(assortativity_degree_directed_weighted(&g, &[]).is_err());
422 assert!(assortativity_degree_directed_weighted(&g, &[1.0, 2.0]).is_err());
423 }
424}