rust_igraph/algorithms/properties/
structural_features.rs1#![allow(
9 clippy::cast_possible_truncation,
10 clippy::cast_precision_loss,
11 clippy::needless_range_loop
12)]
13
14use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
15
16#[derive(Debug, Clone, PartialEq)]
18pub struct StructuralFeatures {
19 pub num_vertices: usize,
21 pub num_features: usize,
23 pub features: Vec<f64>,
25 pub feature_names: Vec<&'static str>,
27}
28
29impl StructuralFeatures {
30 pub fn vertex_features(&self, v: usize) -> &[f64] {
32 let start = v * self.num_features;
33 &self.features[start..start + self.num_features]
34 }
35}
36
37pub fn structural_feature_vectors(graph: &Graph) -> IgraphResult<StructuralFeatures> {
62 if graph.is_directed() {
63 return Err(IgraphError::InvalidArgument(
64 "structural_feature_vectors is defined for undirected graphs only".to_string(),
65 ));
66 }
67
68 let nv = graph.vcount() as usize;
69 let num_features = 8;
70
71 let mut degrees = Vec::with_capacity(nv);
73 let mut neighbors_cache: Vec<Vec<VertexId>> = Vec::with_capacity(nv);
74 for v in 0..nv {
75 let vid = v as VertexId;
76 degrees.push(graph.degree(vid)?);
77 neighbors_cache.push(graph.neighbors(vid)?);
78 }
79
80 let mut features = vec![0.0_f64; nv * num_features];
81
82 for v in 0..nv {
83 let offset = v * num_features;
84 let deg = degrees[v];
85 let neighbors = &neighbors_cache[v];
86
87 features[offset] = deg as f64;
89
90 features[offset + 1] = (1.0 + deg as f64).log2();
92
93 features[offset + 2] = local_clustering(&neighbors_cache, v);
95
96 if deg > 0 {
98 let mut sum_nd = 0usize;
99 let mut min_nd = usize::MAX;
100 let mut max_nd = 0usize;
101 for &u in neighbors {
102 let nd = degrees[u as usize];
103 sum_nd += nd;
104 if nd < min_nd {
105 min_nd = nd;
106 }
107 if nd > max_nd {
108 max_nd = nd;
109 }
110 }
111 features[offset + 3] = sum_nd as f64 / deg as f64;
112 features[offset + 4] = min_nd as f64;
113 features[offset + 5] = max_nd as f64;
114 }
115
116 let tri = count_vertex_triangles(&neighbors_cache, v);
118 features[offset + 6] = tri as f64;
119
120 features[offset + 7] = square_clustering_coeff(&neighbors_cache, °rees, v);
122 }
123
124 Ok(StructuralFeatures {
125 num_vertices: nv,
126 num_features,
127 features,
128 feature_names: vec![
129 "degree",
130 "log_degree",
131 "clustering",
132 "avg_neighbor_degree",
133 "min_neighbor_degree",
134 "max_neighbor_degree",
135 "triangles",
136 "square_clustering",
137 ],
138 })
139}
140
141pub fn degree_profile(graph: &Graph) -> IgraphResult<Vec<f64>> {
160 let nv = graph.vcount() as usize;
161 let mut result = Vec::with_capacity(nv * 3);
162
163 for v in 0..nv {
164 let d = graph.degree(v as VertexId)? as f64;
165 result.push(d);
166 result.push(d * d);
167 result.push((d + 1.0).log2());
168 }
169
170 Ok(result)
171}
172
173fn local_clustering(neighbors_cache: &[Vec<VertexId>], v: usize) -> f64 {
176 let neighbors = &neighbors_cache[v];
177 let deg = neighbors.len();
178 if deg < 2 {
179 return 0.0;
180 }
181
182 let mut triangles = 0usize;
183 for i in 0..deg {
184 let ni = neighbors[i] as usize;
185 for j in (i + 1)..deg {
186 let nj = neighbors[j];
187 if neighbors_cache[ni].contains(&nj) {
188 triangles += 1;
189 }
190 }
191 }
192
193 let possible = deg * (deg - 1) / 2;
194 triangles as f64 / possible as f64
195}
196
197fn count_vertex_triangles(neighbors_cache: &[Vec<VertexId>], v: usize) -> usize {
198 let neighbors = &neighbors_cache[v];
199 let deg = neighbors.len();
200 let mut count = 0;
201
202 for i in 0..deg {
203 let ni = neighbors[i] as usize;
204 for j in (i + 1)..deg {
205 let nj = neighbors[j];
206 if neighbors_cache[ni].contains(&nj) {
207 count += 1;
208 }
209 }
210 }
211
212 count
213}
214
215fn square_clustering_coeff(neighbors_cache: &[Vec<VertexId>], degrees: &[usize], v: usize) -> f64 {
216 let neighbors = &neighbors_cache[v];
219 let deg = neighbors.len();
220 if deg < 2 {
221 return 0.0;
222 }
223
224 let mut squares = 0usize;
225 for i in 0..deg {
226 let u = neighbors[i] as usize;
227 for j in (i + 1)..deg {
228 let w = neighbors[j];
229 for &x in &neighbors_cache[u] {
231 if x != v as u32 && x != w && neighbors_cache[w as usize].contains(&x) {
232 squares += 1;
233 break; }
235 }
236 }
237 }
238
239 let possible = deg * (deg - 1) / 2;
241 if possible == 0 {
242 return 0.0;
243 }
244
245 let _ = degrees;
246 squares as f64 / possible as f64
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 fn triangle() -> Graph {
254 Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
255 }
256
257 fn path4() -> Graph {
258 Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
259 }
260
261 fn square_graph() -> Graph {
262 Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
263 }
264
265 #[test]
268 fn sf_triangle_features() {
269 let g = triangle();
270 let sf = structural_feature_vectors(&g).unwrap();
271 assert_eq!(sf.num_vertices, 3);
272 assert_eq!(sf.num_features, 8);
273 for v in 0..3 {
275 let f = sf.vertex_features(v);
276 assert!((f[0] - 2.0).abs() < 1e-10); assert!((f[2] - 1.0).abs() < 1e-10); assert!((f[3] - 2.0).abs() < 1e-10); assert!((f[6] - 1.0).abs() < 1e-10); }
281 }
282
283 #[test]
284 fn sf_path_features() {
285 let g = path4();
286 let sf = structural_feature_vectors(&g).unwrap();
287 let f0 = sf.vertex_features(0);
289 assert!((f0[0] - 1.0).abs() < 1e-10);
290 assert!(f0[2].abs() < 1e-10);
291 assert!(f0[6].abs() < 1e-10);
292
293 let f1 = sf.vertex_features(1);
295 assert!((f1[0] - 2.0).abs() < 1e-10);
296 assert!(f1[2].abs() < 1e-10);
297 }
298
299 #[test]
300 fn sf_empty_graph() {
301 let g = Graph::with_vertices(3);
302 let sf = structural_feature_vectors(&g).unwrap();
303 for v in 0..3 {
304 let f = sf.vertex_features(v);
305 assert!(f[0].abs() < 1e-10); }
307 }
308
309 #[test]
310 fn sf_directed_error() {
311 let g = Graph::from_edges(&[(0, 1)], true, Some(2)).unwrap();
312 assert!(structural_feature_vectors(&g).is_err());
313 }
314
315 #[test]
316 fn sf_feature_names() {
317 let g = triangle();
318 let sf = structural_feature_vectors(&g).unwrap();
319 assert_eq!(sf.feature_names.len(), 8);
320 assert_eq!(sf.feature_names[0], "degree");
321 assert_eq!(sf.feature_names[7], "square_clustering");
322 }
323
324 #[test]
325 fn sf_square_clustering() {
326 let g = square_graph();
327 let sf = structural_feature_vectors(&g).unwrap();
328 for v in 0..4 {
331 let f = sf.vertex_features(v);
332 assert!((f[7] - 1.0).abs() < 1e-10);
333 }
334 }
335
336 #[test]
339 fn dp_triangle() {
340 let g = triangle();
341 let prof = degree_profile(&g).unwrap();
342 assert_eq!(prof.len(), 9); assert!((prof[0] - 2.0).abs() < 1e-10);
345 assert!((prof[1] - 4.0).abs() < 1e-10);
346 assert!((prof[2] - 3.0_f64.log2()).abs() < 1e-10);
347 }
348
349 #[test]
350 fn dp_empty() {
351 let g = Graph::with_vertices(2);
352 let prof = degree_profile(&g).unwrap();
353 assert_eq!(prof.len(), 6);
354 assert!(prof[0].abs() < 1e-10);
356 assert!(prof[1].abs() < 1e-10);
357 assert!(prof[2].abs() < 1e-10);
359 }
360
361 #[test]
362 fn dp_star() {
363 let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false, Some(4)).unwrap();
364 let prof = degree_profile(&g).unwrap();
365 assert!((prof[0] - 3.0).abs() < 1e-10);
367 assert!((prof[1] - 9.0).abs() < 1e-10);
368 }
369}