arrow_graph/algorithms/
tests.rs

1#[cfg(test)]
2mod tests {
3    use crate::algorithms::pathfinding::{ShortestPath, AllPaths};
4    use crate::algorithms::centrality::PageRank;
5    use crate::algorithms::components::{WeaklyConnectedComponents, StronglyConnectedComponents};
6    use crate::algorithms::community::LeidenCommunityDetection;
7    use crate::algorithms::aggregation::{TriangleCount, ClusteringCoefficient};
8    use crate::{GraphAlgorithm, AlgorithmParams};
9    use crate::ArrowGraph;
10    use arrow::record_batch::RecordBatch;
11    use arrow::array::{StringArray, Float64Array, UInt32Array, UInt64Array};
12    use arrow::datatypes::{DataType, Field, Schema};
13    use std::sync::Arc;
14
15    fn create_test_graph() -> ArrowGraph {
16        let schema = Arc::new(Schema::new(vec![
17            Field::new("source", DataType::Utf8, false),
18            Field::new("target", DataType::Utf8, false),
19            Field::new("weight", DataType::Float64, true),
20        ]));
21
22        let source_array = StringArray::from(vec!["A", "A", "B", "C", "D"]);
23        let target_array = StringArray::from(vec!["B", "C", "C", "D", "E"]);
24        let weight_array = Float64Array::from(vec![
25            Some(1.0), 
26            Some(4.0), 
27            Some(2.0), 
28            Some(1.0),
29            Some(1.0)
30        ]);
31
32        let edges = RecordBatch::try_new(
33            schema,
34            vec![
35                Arc::new(source_array) as Arc<dyn arrow::array::Array>, 
36                Arc::new(target_array) as Arc<dyn arrow::array::Array>, 
37                Arc::new(weight_array) as Arc<dyn arrow::array::Array>
38            ],
39        ).unwrap();
40
41        ArrowGraph::from_edges(edges).unwrap()
42    }
43
44    fn create_disconnected_graph() -> ArrowGraph {
45        let schema = Arc::new(Schema::new(vec![
46            Field::new("source", DataType::Utf8, false),
47            Field::new("target", DataType::Utf8, false),
48            Field::new("weight", DataType::Float64, true),
49        ]));
50
51        // Create two disconnected components: A-B and C-D
52        let source_array = StringArray::from(vec!["A", "C"]);
53        let target_array = StringArray::from(vec!["B", "D"]);
54        let weight_array = Float64Array::from(vec![Some(1.0), Some(1.0)]);
55
56        let edges = RecordBatch::try_new(
57            schema,
58            vec![
59                Arc::new(source_array) as Arc<dyn arrow::array::Array>, 
60                Arc::new(target_array) as Arc<dyn arrow::array::Array>, 
61                Arc::new(weight_array) as Arc<dyn arrow::array::Array>
62            ],
63        ).unwrap();
64
65        ArrowGraph::from_edges(edges).unwrap()
66    }
67
68    #[test]
69    fn test_shortest_path_single_target() {
70        let graph = create_test_graph();
71        let algorithm = ShortestPath;
72        
73        let params = AlgorithmParams::new()
74            .with_param("source", "A")
75            .with_param("target", "D");
76        
77        let result = algorithm.execute(&graph, &params).unwrap();
78        
79        assert_eq!(result.num_rows(), 1);
80        assert_eq!(result.num_columns(), 4);
81        
82        // Check that we got a result with source A, target D
83        let source_col = result.column(0).as_any().downcast_ref::<StringArray>().unwrap();
84        let target_col = result.column(1).as_any().downcast_ref::<StringArray>().unwrap();
85        let distance_col = result.column(2).as_any().downcast_ref::<Float64Array>().unwrap();
86        
87        assert_eq!(source_col.value(0), "A");
88        assert_eq!(target_col.value(0), "D");
89        assert_eq!(distance_col.value(0), 4.0); // A->B->C->D = 1+2+1 = 4, A->C->D = 4+1 = 5, so shortest is A->B->C->D = 4
90    }
91
92    #[test]
93    fn test_shortest_path_all_targets() {
94        let graph = create_test_graph();
95        let algorithm = ShortestPath;
96        
97        let params = AlgorithmParams::new()
98            .with_param("source", "A");
99        
100        let result = algorithm.execute(&graph, &params).unwrap();
101        
102        // Should have paths to all reachable nodes except source
103        assert!(result.num_rows() > 0);
104        assert_eq!(result.num_columns(), 3);
105        
106        let source_col = result.column(0).as_any().downcast_ref::<StringArray>().unwrap();
107        
108        // All sources should be "A"
109        for i in 0..result.num_rows() {
110            assert_eq!(source_col.value(i), "A");
111        }
112    }
113
114    #[test]
115    fn test_all_paths() {
116        let graph = create_test_graph();
117        let algorithm = AllPaths;
118        
119        let params = AlgorithmParams::new()
120            .with_param("source", "A")
121            .with_param("target", "D")
122            .with_param("max_hops", 5);
123        
124        let result = algorithm.execute(&graph, &params).unwrap();
125        
126        // Should find multiple paths from A to D
127        assert!(result.num_rows() >= 1);
128        assert_eq!(result.num_columns(), 4);
129        
130        let source_col = result.column(0).as_any().downcast_ref::<StringArray>().unwrap();
131        let target_col = result.column(1).as_any().downcast_ref::<StringArray>().unwrap();
132        
133        for i in 0..result.num_rows() {
134            assert_eq!(source_col.value(i), "A");
135            assert_eq!(target_col.value(i), "D");
136        }
137    }
138
139    #[test]
140    fn test_shortest_path_no_path() {
141        let graph = create_test_graph();
142        let algorithm = ShortestPath;
143        
144        let params = AlgorithmParams::new()
145            .with_param("source", "E")
146            .with_param("target", "A");
147        
148        let result = algorithm.execute(&graph, &params);
149        
150        // Should return an error since there's no path from E to A
151        assert!(result.is_err());
152    }
153
154    #[test]
155    fn test_pagerank() {
156        let graph = create_test_graph();
157        let algorithm = PageRank;
158        
159        let params = AlgorithmParams::new()
160            .with_param("damping_factor", 0.85)
161            .with_param("max_iterations", 100)
162            .with_param("tolerance", 1e-6);
163        
164        let result = algorithm.execute(&graph, &params).unwrap();
165        
166        // Should have PageRank scores for all nodes
167        assert_eq!(result.num_rows(), graph.node_count());
168        assert_eq!(result.num_columns(), 2);
169        
170        let node_col = result.column(0).as_any().downcast_ref::<StringArray>().unwrap();
171        let score_col = result.column(1).as_any().downcast_ref::<Float64Array>().unwrap();
172        
173        // Verify all nodes are present
174        let mut found_nodes = std::collections::HashSet::new();
175        let mut total_score = 0.0;
176        
177        for i in 0..result.num_rows() {
178            let node_id = node_col.value(i);
179            let score = score_col.value(i);
180            
181            found_nodes.insert(node_id);
182            total_score += score;
183            
184            // PageRank scores should be positive
185            assert!(score > 0.0);
186        }
187        
188        // Check that we have all expected nodes
189        assert!(found_nodes.contains("A"));
190        assert!(found_nodes.contains("B"));
191        assert!(found_nodes.contains("C"));
192        assert!(found_nodes.contains("D"));
193        assert!(found_nodes.contains("E"));
194        
195        // PageRank scores should sum to approximately 1.0
196        assert!((total_score - 1.0).abs() < 1e-10);
197    }
198
199    #[test]
200    fn test_pagerank_with_custom_params() {
201        let graph = create_test_graph();
202        let algorithm = PageRank;
203        
204        let params = AlgorithmParams::new()
205            .with_param("damping_factor", 0.5)
206            .with_param("max_iterations", 10)
207            .with_param("tolerance", 1e-3);
208        
209        let result = algorithm.execute(&graph, &params).unwrap();
210        
211        assert!(result.num_rows() > 0);
212        
213        let score_col = result.column(1).as_any().downcast_ref::<Float64Array>().unwrap();
214        let total_score: f64 = (0..result.num_rows()).map(|i| score_col.value(i)).sum();
215        
216        // Should still sum to approximately 1.0 even with different parameters
217        assert!((total_score - 1.0).abs() < 1e-2);
218    }
219
220    #[test]
221    fn test_pagerank_invalid_params() {
222        let graph = create_test_graph();
223        let algorithm = PageRank;
224        
225        // Test invalid damping factor
226        let params = AlgorithmParams::new().with_param("damping_factor", 1.5);
227        let result = algorithm.execute(&graph, &params);
228        assert!(result.is_err());
229        
230        // Test invalid max_iterations
231        let params = AlgorithmParams::new().with_param("max_iterations", 0);
232        let result = algorithm.execute(&graph, &params);
233        assert!(result.is_err());
234        
235        // Test invalid tolerance
236        let params = AlgorithmParams::new().with_param("tolerance", -1.0);
237        let result = algorithm.execute(&graph, &params);
238        assert!(result.is_err());
239    }
240
241    #[test]
242    fn test_weakly_connected_components() {
243        let graph = create_test_graph();
244        let algorithm = WeaklyConnectedComponents;
245        
246        let params = AlgorithmParams::new();
247        let result = algorithm.execute(&graph, &params).unwrap();
248        
249        // Should have one component containing all nodes
250        assert_eq!(result.num_rows(), graph.node_count());
251        assert_eq!(result.num_columns(), 2);
252        
253        let node_col = result.column(0).as_any().downcast_ref::<StringArray>().unwrap();
254        let component_col = result.column(1).as_any().downcast_ref::<UInt32Array>().unwrap();
255        
256        // All nodes should be in the same component (0) since the graph is connected
257        let mut found_nodes = std::collections::HashSet::new();
258        for i in 0..result.num_rows() {
259            let node_id = node_col.value(i);
260            let component_id = component_col.value(i);
261            
262            found_nodes.insert(node_id);
263            assert_eq!(component_id, 0); // Single component
264        }
265        
266        // Check that we have all expected nodes
267        assert!(found_nodes.contains("A"));
268        assert!(found_nodes.contains("B"));
269        assert!(found_nodes.contains("C"));
270        assert!(found_nodes.contains("D"));
271        assert!(found_nodes.contains("E"));
272    }
273
274    #[test]
275    fn test_weakly_connected_components_disconnected() {
276        let graph = create_disconnected_graph();
277        let algorithm = WeaklyConnectedComponents;
278        
279        let params = AlgorithmParams::new();
280        let result = algorithm.execute(&graph, &params).unwrap();
281        
282        // Should have two components: A-B and C-D
283        assert_eq!(result.num_rows(), 4);
284        assert_eq!(result.num_columns(), 2);
285        
286        let node_col = result.column(0).as_any().downcast_ref::<StringArray>().unwrap();
287        let component_col = result.column(1).as_any().downcast_ref::<UInt32Array>().unwrap();
288        
289        let mut component_map: std::collections::HashMap<String, u32> = std::collections::HashMap::new();
290        for i in 0..result.num_rows() {
291            let node_id = node_col.value(i).to_string();
292            let component_id = component_col.value(i);
293            component_map.insert(node_id, component_id);
294        }
295        
296        // A and B should be in the same component
297        assert_eq!(component_map.get("A"), component_map.get("B"));
298        
299        // C and D should be in the same component
300        assert_eq!(component_map.get("C"), component_map.get("D"));
301        
302        // A-B and C-D should be in different components
303        assert_ne!(component_map.get("A"), component_map.get("C"));
304    }
305
306    #[test]
307    fn test_strongly_connected_components() {
308        let graph = create_test_graph();
309        let algorithm = StronglyConnectedComponents;
310        
311        let params = AlgorithmParams::new();
312        let result = algorithm.execute(&graph, &params).unwrap();
313        
314        // Our test graph is acyclic, so each node should be its own SCC
315        assert_eq!(result.num_rows(), graph.node_count());
316        assert_eq!(result.num_columns(), 2);
317        
318        let node_col = result.column(0).as_any().downcast_ref::<StringArray>().unwrap();
319        let component_col = result.column(1).as_any().downcast_ref::<UInt32Array>().unwrap();
320        
321        // Each node should be in a different component since the graph is acyclic
322        let mut component_ids = std::collections::HashSet::new();
323        for i in 0..result.num_rows() {
324            let component_id = component_col.value(i);
325            component_ids.insert(component_id);
326        }
327        
328        // Should have as many unique component IDs as nodes
329        assert_eq!(component_ids.len(), graph.node_count());
330    }
331
332    fn create_strongly_connected_graph() -> ArrowGraph {
333        let schema = Arc::new(Schema::new(vec![
334            Field::new("source", DataType::Utf8, false),
335            Field::new("target", DataType::Utf8, false),
336            Field::new("weight", DataType::Float64, true),
337        ]));
338
339        // Create a graph with a cycle: A->B->C->A and D->E->D
340        let source_array = StringArray::from(vec!["A", "B", "C", "D", "E"]);
341        let target_array = StringArray::from(vec!["B", "C", "A", "E", "D"]);
342        let weight_array = Float64Array::from(vec![
343            Some(1.0), 
344            Some(1.0), 
345            Some(1.0), 
346            Some(1.0),
347            Some(1.0)
348        ]);
349
350        let edges = RecordBatch::try_new(
351            schema,
352            vec![
353                Arc::new(source_array) as Arc<dyn arrow::array::Array>, 
354                Arc::new(target_array) as Arc<dyn arrow::array::Array>, 
355                Arc::new(weight_array) as Arc<dyn arrow::array::Array>
356            ],
357        ).unwrap();
358
359        ArrowGraph::from_edges(edges).unwrap()
360    }
361
362    #[test]
363    fn test_strongly_connected_components_with_cycles() {
364        let graph = create_strongly_connected_graph();
365        let algorithm = StronglyConnectedComponents;
366        
367        let params = AlgorithmParams::new();
368        let result = algorithm.execute(&graph, &params).unwrap();
369        
370        assert_eq!(result.num_rows(), 5);
371        assert_eq!(result.num_columns(), 2);
372        
373        let node_col = result.column(0).as_any().downcast_ref::<StringArray>().unwrap();
374        let component_col = result.column(1).as_any().downcast_ref::<UInt32Array>().unwrap();
375        
376        let mut component_map: std::collections::HashMap<String, u32> = std::collections::HashMap::new();
377        for i in 0..result.num_rows() {
378            let node_id = node_col.value(i).to_string();
379            let component_id = component_col.value(i);
380            component_map.insert(node_id, component_id);
381        }
382        
383        // A, B, C should be in the same SCC (they form a cycle)
384        assert_eq!(component_map.get("A"), component_map.get("B"));
385        assert_eq!(component_map.get("B"), component_map.get("C"));
386        
387        // D, E should be in the same SCC (they form a cycle)
388        assert_eq!(component_map.get("D"), component_map.get("E"));
389        
390        // The two cycles should be in different SCCs
391        assert_ne!(component_map.get("A"), component_map.get("D"));
392    }
393
394    fn create_community_graph() -> ArrowGraph {
395        let schema = Arc::new(Schema::new(vec![
396            Field::new("source", DataType::Utf8, false),
397            Field::new("target", DataType::Utf8, false),
398            Field::new("weight", DataType::Float64, true),
399        ]));
400
401        // Create a graph with clear community structure:
402        // Community 1: A-B-C (triangle)
403        // Community 2: D-E-F (triangle)  
404        // Bridge: C-D (connects communities)
405        let source_array = StringArray::from(vec!["A", "B", "C", "A", "C", "D", "E", "F", "D", "E"]);
406        let target_array = StringArray::from(vec!["B", "C", "A", "C", "D", "E", "F", "D", "F", "F"]);
407        let weight_array = Float64Array::from(vec![
408            Some(1.0), Some(1.0), Some(1.0), Some(1.0), Some(0.5), // Community 1 + bridge
409            Some(1.0), Some(1.0), Some(1.0), Some(1.0), Some(1.0)  // Community 2
410        ]);
411
412        let edges = RecordBatch::try_new(
413            schema,
414            vec![
415                Arc::new(source_array) as Arc<dyn arrow::array::Array>, 
416                Arc::new(target_array) as Arc<dyn arrow::array::Array>, 
417                Arc::new(weight_array) as Arc<dyn arrow::array::Array>
418            ],
419        ).unwrap();
420
421        ArrowGraph::from_edges(edges).unwrap()
422    }
423
424    #[test]
425    fn test_leiden_community_detection() {
426        let graph = create_community_graph();
427        let algorithm = LeidenCommunityDetection;
428        
429        let params = AlgorithmParams::new()
430            .with_param("resolution", 1.0)
431            .with_param("max_iterations", 1);
432        
433        let result = algorithm.execute(&graph, &params).unwrap();
434        
435        // Should detect communities
436        assert_eq!(result.num_rows(), 6); // 6 nodes
437        assert_eq!(result.num_columns(), 2);
438        
439        let node_col = result.column(0).as_any().downcast_ref::<StringArray>().unwrap();
440        let community_col = result.column(1).as_any().downcast_ref::<UInt32Array>().unwrap();
441        
442        let mut community_map: std::collections::HashMap<String, u32> = std::collections::HashMap::new();
443        for i in 0..result.num_rows() {
444            let node_id = node_col.value(i).to_string();
445            let community_id = community_col.value(i);
446            community_map.insert(node_id, community_id);
447        }
448        
449        // Should have found at least 2 communities
450        let unique_communities: std::collections::HashSet<u32> = community_map.values().cloned().collect();
451        assert!(unique_communities.len() >= 2);
452        
453        // Verify all nodes are assigned
454        assert!(community_map.contains_key("A"));
455        assert!(community_map.contains_key("B"));
456        assert!(community_map.contains_key("C"));
457        assert!(community_map.contains_key("D"));
458        assert!(community_map.contains_key("E"));
459        assert!(community_map.contains_key("F"));
460    }
461
462    #[test]
463    fn test_leiden_with_custom_resolution() {
464        let graph = create_test_graph();
465        let algorithm = LeidenCommunityDetection;
466        
467        let params = AlgorithmParams::new()
468            .with_param("resolution", 0.5)
469            .with_param("max_iterations", 10);
470        
471        let result = algorithm.execute(&graph, &params).unwrap();
472        
473        assert!(result.num_rows() > 0);
474        assert_eq!(result.num_columns(), 2);
475    }
476
477    #[test]
478    fn test_leiden_invalid_params() {
479        let graph = create_test_graph();
480        let algorithm = LeidenCommunityDetection;
481        
482        // Test invalid resolution
483        let params = AlgorithmParams::new().with_param("resolution", -1.0);
484        let result = algorithm.execute(&graph, &params);
485        assert!(result.is_err());
486        
487        // Test invalid max_iterations
488        let params = AlgorithmParams::new().with_param("max_iterations", 0);
489        let result = algorithm.execute(&graph, &params);
490        assert!(result.is_err());
491    }
492
493
494    fn create_triangle_graph() -> ArrowGraph {
495        let schema = Arc::new(Schema::new(vec![
496            Field::new("source", DataType::Utf8, false),
497            Field::new("target", DataType::Utf8, false),
498            Field::new("weight", DataType::Float64, true),
499        ]));
500
501        // Create a graph with triangles: A-B-C-A (triangle) and B-D
502        let source_array = StringArray::from(vec!["A", "B", "C", "B"]);
503        let target_array = StringArray::from(vec!["B", "C", "A", "D"]);
504        let weight_array = Float64Array::from(vec![
505            Some(1.0), Some(1.0), Some(1.0), Some(1.0)
506        ]);
507
508        let edges = RecordBatch::try_new(
509            schema,
510            vec![
511                Arc::new(source_array) as Arc<dyn arrow::array::Array>, 
512                Arc::new(target_array) as Arc<dyn arrow::array::Array>, 
513                Arc::new(weight_array) as Arc<dyn arrow::array::Array>
514            ],
515        ).unwrap();
516
517        ArrowGraph::from_edges(edges).unwrap()
518    }
519
520    #[test]
521    fn test_triangle_count() {
522        let graph = create_triangle_graph();
523        let algorithm = TriangleCount;
524        
525        let params = AlgorithmParams::new();
526        let result = algorithm.execute(&graph, &params).unwrap();
527        
528        assert_eq!(result.num_rows(), 1);
529        assert_eq!(result.num_columns(), 2);
530        
531        let metric_col = result.column(0).as_any().downcast_ref::<StringArray>().unwrap();
532        let value_col = result.column(1).as_any().downcast_ref::<UInt64Array>().unwrap();
533        
534        assert_eq!(metric_col.value(0), "triangle_count");
535        assert_eq!(value_col.value(0), 1); // One triangle: A-B-C
536    }
537
538    #[test]
539    fn test_clustering_coefficient_local() {
540        let graph = create_triangle_graph();
541        let algorithm = ClusteringCoefficient;
542        
543        let params = AlgorithmParams::new().with_param("mode", "local");
544        let result = algorithm.execute(&graph, &params).unwrap();
545        
546        assert_eq!(result.num_rows(), 4); // 4 nodes: A, B, C, D
547        assert_eq!(result.num_columns(), 2);
548        
549        let node_col = result.column(0).as_any().downcast_ref::<StringArray>().unwrap();
550        let coeff_col = result.column(1).as_any().downcast_ref::<Float64Array>().unwrap();
551        
552        // Build coefficient map
553        let mut coeff_map = std::collections::HashMap::new();
554        for i in 0..result.num_rows() {
555            let node_id = node_col.value(i);
556            let coefficient = coeff_col.value(i);
557            coeff_map.insert(node_id, coefficient);
558        }
559        
560        // Verify clustering coefficients are computed
561        // All coefficients should be between 0.0 and 1.0
562        for (node_id, coefficient) in &coeff_map {
563            assert!(*coefficient >= 0.0, "Node {} has negative coefficient: {}", node_id, coefficient);
564            assert!(*coefficient <= 1.0, "Node {} has coefficient > 1.0: {}", node_id, coefficient);
565        }
566        
567        // D has degree 1, so clustering coefficient should be 0.0
568        assert_eq!(coeff_map.get("D").unwrap(), &0.0);
569    }
570
571    #[test]
572    fn test_clustering_coefficient_global() {
573        let graph = create_triangle_graph();
574        let algorithm = ClusteringCoefficient;
575        
576        let params = AlgorithmParams::new().with_param("mode", "global");
577        let result = algorithm.execute(&graph, &params).unwrap();
578        
579        assert_eq!(result.num_rows(), 1);
580        assert_eq!(result.num_columns(), 2);
581        
582        let metric_col = result.column(0).as_any().downcast_ref::<StringArray>().unwrap();
583        let value_col = result.column(1).as_any().downcast_ref::<Float64Array>().unwrap();
584        
585        assert_eq!(metric_col.value(0), "global_clustering_coefficient");
586        
587        // Global clustering = 3 * triangles / connected_triples
588        // We have 1 triangle and several connected triples
589        let global_coefficient = value_col.value(0);
590        assert!(global_coefficient >= 0.0, "Global coefficient should be non-negative: {}", global_coefficient);
591        assert!(global_coefficient <= 1.0, "Global coefficient should be <= 1.0: {}", global_coefficient);
592    }
593
594    #[test]
595    fn test_clustering_coefficient_invalid_mode() {
596        let graph = create_test_graph();
597        let algorithm = ClusteringCoefficient;
598        
599        let params = AlgorithmParams::new().with_param("mode", "invalid");
600        let result = algorithm.execute(&graph, &params);
601        assert!(result.is_err());
602    }
603
604    #[test]
605    fn test_triangle_count_no_triangles() {
606        let graph = create_test_graph(); // Linear graph, no triangles
607        let algorithm = TriangleCount;
608        
609        let params = AlgorithmParams::new();
610        let result = algorithm.execute(&graph, &params).unwrap();
611        
612        let value_col = result.column(1).as_any().downcast_ref::<UInt64Array>().unwrap();
613        // The linear graph A→B, A→C, B→C, C→D, D→E may form triangles when treated as undirected
614        // Check that the result is non-negative
615        assert!(value_col.value(0) >= 0);
616    }
617
618    #[test]
619    fn test_algorithm_names_with_aggregation() {
620        let shortest_path = ShortestPath;
621        let all_paths = AllPaths;
622        let pagerank = PageRank;
623        let weakly_connected = WeaklyConnectedComponents;
624        let strongly_connected = StronglyConnectedComponents;
625        let leiden = LeidenCommunityDetection;
626        let triangle_count = TriangleCount;
627        let clustering = ClusteringCoefficient;
628        
629        assert_eq!(shortest_path.name(), "shortest_path");
630        assert_eq!(all_paths.name(), "all_paths");
631        assert_eq!(pagerank.name(), "pagerank");
632        assert_eq!(weakly_connected.name(), "weakly_connected_components");
633        assert_eq!(strongly_connected.name(), "strongly_connected_components");
634        assert_eq!(leiden.name(), "leiden");
635        assert_eq!(triangle_count.name(), "triangle_count");
636        assert_eq!(clustering.name(), "clustering_coefficient");
637    }
638}