rust_igraph/algorithms/properties/
degree_correlation.rs1use crate::algorithms::properties::degree::DegreeMode;
12use crate::core::{Graph, IgraphResult};
13
14pub fn degree_correlation_vector(
51 graph: &Graph,
52 from_mode: DegreeMode,
53 to_mode: DegreeMode,
54 directed_neighbors: bool,
55 weights: Option<&[f64]>,
56) -> IgraphResult<Vec<f64>> {
57 let ecount = graph.ecount();
58
59 if graph.vcount() == 0 {
60 return Ok(Vec::new());
61 }
62
63 if let Some(w) = weights {
64 if w.len() != ecount {
65 return Err(crate::core::IgraphError::InvalidArgument(format!(
66 "degree_correlation_vector: weight vector length ({}) does not match edge count ({ecount})",
67 w.len()
68 )));
69 }
70 }
71
72 let (from_mode_eff, to_mode_eff, directed_eff) = if graph.is_directed() {
73 (from_mode, to_mode, directed_neighbors)
74 } else {
75 (DegreeMode::All, DegreeMode::All, false)
76 };
77
78 let deg_from = compute_degrees(graph, from_mode_eff)?;
79 let deg_to = compute_degrees(graph, to_mode_eff)?;
80
81 let max_from_deg = if ecount > 0 {
82 deg_from.iter().copied().max().unwrap_or(0) as usize
83 } else {
84 0
85 };
86
87 let mut knnk = vec![0.0_f64; max_from_deg + 1];
88 let mut weight_sums = vec![0.0_f64; max_from_deg + 1];
89
90 for eid in 0..ecount {
91 #[allow(clippy::cast_possible_truncation)]
92 let (from, to) = graph.edge(eid as u32)?;
93 let from_deg = deg_from[from as usize] as usize;
94 let to_deg = f64::from(deg_to[to as usize]);
95 let w = weights.map_or(1.0, |ws| ws[eid]);
96
97 weight_sums[from_deg] += w;
98 knnk[from_deg] += w * to_deg;
99
100 if !directed_eff {
101 let to_from_deg = deg_from[to as usize] as usize;
102 let from_to_deg = f64::from(deg_to[from as usize]);
103 weight_sums[to_from_deg] += w;
104 knnk[to_from_deg] += w * from_to_deg;
105 }
106 }
107
108 for i in 0..knnk.len() {
109 if weight_sums[i] > 0.0 {
110 knnk[i] /= weight_sums[i];
111 } else {
112 knnk[i] = f64::NAN;
113 }
114 }
115
116 Ok(knnk)
117}
118
119fn compute_degrees(graph: &Graph, mode: DegreeMode) -> IgraphResult<Vec<u32>> {
120 let n = graph.vcount() as usize;
121 let ecount = graph.ecount();
122 let mut deg = vec![0u32; n];
123
124 for eid in 0..ecount {
125 #[allow(clippy::cast_possible_truncation)]
126 let (from, to) = graph.edge(eid as u32)?;
127
128 match mode {
129 DegreeMode::Out => {
130 deg[from as usize] += 1;
131 }
132 DegreeMode::In => {
133 deg[to as usize] += 1;
134 }
135 DegreeMode::All => {
136 deg[from as usize] += 1;
137 if from == to {
138 deg[from as usize] += 1;
139 } else {
140 deg[to as usize] += 1;
141 }
142 }
143 }
144 }
145
146 Ok(deg)
147}
148
149#[cfg(test)]
150#[allow(clippy::float_cmp)]
151mod tests {
152 use super::*;
153
154 fn approx_eq(a: f64, b: f64) -> bool {
155 (a - b).abs() < 1e-10
156 }
157
158 #[test]
159 fn empty_graph() {
160 let g = Graph::with_vertices(0);
161 let knnk =
162 degree_correlation_vector(&g, DegreeMode::All, DegreeMode::All, false, None).unwrap();
163 assert!(knnk.is_empty());
164 }
165
166 #[test]
167 fn isolated_vertices() {
168 let g = Graph::with_vertices(5);
169 let knnk =
170 degree_correlation_vector(&g, DegreeMode::All, DegreeMode::All, false, None).unwrap();
171 assert_eq!(knnk.len(), 1); assert!(knnk[0].is_nan());
173 }
174
175 #[test]
176 fn single_edge() {
177 let mut g = Graph::with_vertices(2);
178 g.add_edge(0, 1).unwrap();
179 let knnk =
180 degree_correlation_vector(&g, DegreeMode::All, DegreeMode::All, false, None).unwrap();
181 assert_eq!(knnk.len(), 2);
183 assert!(knnk[0].is_nan());
184 assert!(approx_eq(knnk[1], 1.0));
185 }
186
187 #[test]
188 fn star_graph() {
189 let mut g = Graph::with_vertices(5);
190 g.add_edge(0, 1).unwrap();
191 g.add_edge(0, 2).unwrap();
192 g.add_edge(0, 3).unwrap();
193 g.add_edge(0, 4).unwrap();
194 let knnk =
195 degree_correlation_vector(&g, DegreeMode::All, DegreeMode::All, false, None).unwrap();
196 assert_eq!(knnk.len(), 5); assert!(knnk[0].is_nan());
198 assert!(approx_eq(knnk[1], 4.0)); assert!(knnk[2].is_nan());
200 assert!(knnk[3].is_nan());
201 assert!(approx_eq(knnk[4], 1.0)); }
203
204 #[test]
205 fn triangle() {
206 let mut g = Graph::with_vertices(3);
207 g.add_edge(0, 1).unwrap();
208 g.add_edge(1, 2).unwrap();
209 g.add_edge(2, 0).unwrap();
210 let knnk =
211 degree_correlation_vector(&g, DegreeMode::All, DegreeMode::All, false, None).unwrap();
212 assert!(approx_eq(knnk[2], 2.0));
214 }
215
216 #[test]
217 fn path_3() {
218 let mut g = Graph::with_vertices(3);
219 g.add_edge(0, 1).unwrap();
220 g.add_edge(1, 2).unwrap();
221 let knnk =
222 degree_correlation_vector(&g, DegreeMode::All, DegreeMode::All, false, None).unwrap();
223 assert!(approx_eq(knnk[1], 2.0));
226 assert!(approx_eq(knnk[2], 1.0));
227 }
228
229 #[test]
230 fn directed_out_in() {
231 let mut g = Graph::new(3, true).unwrap();
232 g.add_edge(0, 1).unwrap(); g.add_edge(0, 2).unwrap(); g.add_edge(1, 2).unwrap(); let knnk =
237 degree_correlation_vector(&g, DegreeMode::Out, DegreeMode::In, true, None).unwrap();
238 assert_eq!(knnk.len(), 3);
245 assert!(knnk[0].is_nan());
246 assert!(approx_eq(knnk[1], 2.0));
247 assert!(approx_eq(knnk[2], 1.5));
248 }
249
250 #[test]
251 fn directed_undirected_neighbors() {
252 let mut g = Graph::new(3, true).unwrap();
253 g.add_edge(0, 1).unwrap();
254 g.add_edge(1, 2).unwrap();
255 let knnk =
257 degree_correlation_vector(&g, DegreeMode::Out, DegreeMode::Out, false, None).unwrap();
258 assert!(approx_eq(knnk[0], 1.0));
265 assert!(approx_eq(knnk[1], 2.0 / 3.0));
266 }
267
268 #[test]
269 fn weighted() {
270 let mut g = Graph::with_vertices(3);
271 g.add_edge(0, 1).unwrap();
272 g.add_edge(1, 2).unwrap();
273 let weights = vec![2.0, 1.0];
274 let knnk =
275 degree_correlation_vector(&g, DegreeMode::All, DegreeMode::All, false, Some(&weights))
276 .unwrap();
277 assert!(approx_eq(knnk[1], 2.0));
285 assert!(approx_eq(knnk[2], 1.0));
286 }
287
288 #[test]
289 fn weight_length_mismatch() {
290 let mut g = Graph::with_vertices(2);
291 g.add_edge(0, 1).unwrap();
292 let bad_weights = vec![1.0, 2.0, 3.0];
293 let result = degree_correlation_vector(
294 &g,
295 DegreeMode::All,
296 DegreeMode::All,
297 false,
298 Some(&bad_weights),
299 );
300 assert!(result.is_err());
301 }
302
303 #[test]
304 fn self_loop_counted() {
305 let mut g = Graph::with_vertices(2);
306 g.add_edge(0, 0).unwrap(); g.add_edge(0, 1).unwrap();
308 let knnk =
309 degree_correlation_vector(&g, DegreeMode::All, DegreeMode::All, false, None).unwrap();
310 assert!(approx_eq(knnk[1], 3.0));
320 assert!(approx_eq(knnk[3], 7.0 / 3.0));
321 }
322
323 #[test]
324 fn complete_k4() {
325 let mut g = Graph::with_vertices(4);
326 g.add_edge(0, 1).unwrap();
327 g.add_edge(0, 2).unwrap();
328 g.add_edge(0, 3).unwrap();
329 g.add_edge(1, 2).unwrap();
330 g.add_edge(1, 3).unwrap();
331 g.add_edge(2, 3).unwrap();
332 let knnk =
333 degree_correlation_vector(&g, DegreeMode::All, DegreeMode::All, false, None).unwrap();
334 assert!(approx_eq(knnk[3], 3.0));
336 }
337}