Skip to main content

scirs2/
graph.rs

1//! Python bindings for scirs2-graph
2//!
3//! This module provides Python bindings for graph algorithms,
4//! including traversal, shortest paths, centrality, and community detection.
5
6use pyo3::exceptions::PyRuntimeError;
7use pyo3::prelude::*;
8use pyo3::types::{PyAny, PyDict, PyList, PyTuple};
9
10// NumPy types for Python array interface
11use scirs2_numpy::{IntoPyArray, PyArray2};
12
13// ndarray types from scirs2-core
14#[allow(unused_imports)]
15use scirs2_core::ndarray::{Array1, Array2};
16use scirs2_core::random::thread_rng;
17
18// Direct imports from scirs2-graph
19use scirs2_graph::{
20    articulation_points,
21    // Centrality
22    betweenness_centrality,
23    // Traversal
24    breadth_first_search,
25    bridges,
26    closeness_centrality,
27    // Connectivity
28    connected_components,
29    depth_first_search,
30    // Graph properties
31    diameter,
32    // Shortest paths
33    dijkstra_path,
34    floyd_warshall,
35    is_bipartite,
36    label_propagation_result,
37    // Community detection
38    louvain_communities_result,
39    // Spanning trees
40    minimum_spanning_tree,
41    modularity,
42    strongly_connected_components,
43    DiGraph,
44    // Graph types
45    Graph,
46};
47// Additional imports from submodules
48use scirs2_graph::generators::{
49    barabasi_albert_graph, complete_graph, cycle_graph, erdos_renyi_graph, path_graph, star_graph,
50    watts_strogatz_graph,
51};
52use scirs2_graph::measures::{clustering_coefficient, pagerank_centrality};
53
54// ========================================
55// GRAPH CREATION
56// ========================================
57
58/// Create an undirected graph from edge list
59#[pyfunction]
60fn graph_from_edges_py(
61    py: Python,
62    edges: &Bound<'_, PyList>,
63    num_nodes: Option<usize>,
64) -> PyResult<Py<PyAny>> {
65    let mut graph = Graph::<usize, f64, u32>::new();
66    let mut max_node = 0usize;
67
68    // Add edges
69    for edge in edges.iter() {
70        let tuple = edge.downcast::<PyTuple>()?;
71        let u: usize = tuple.get_item(0)?.extract()?;
72        let v: usize = tuple.get_item(1)?.extract()?;
73        let weight: f64 = if tuple.len() > 2 {
74            tuple.get_item(2)?.extract()?
75        } else {
76            1.0
77        };
78
79        max_node = max_node.max(u).max(v);
80        graph.add_edge(u, v, weight);
81    }
82
83    let n = num_nodes.unwrap_or(max_node + 1);
84
85    let edges_vec = graph.edges();
86    let dict = PyDict::new(py);
87    dict.set_item("num_nodes", n)?;
88    dict.set_item("num_edges", edges_vec.len())?;
89    dict.set_item("directed", false)?;
90
91    // Convert to adjacency list representation
92    let mut adj_list: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
93    for edge in &edges_vec {
94        adj_list[edge.source].push((edge.target, edge.weight));
95        adj_list[edge.target].push((edge.source, edge.weight)); // Undirected
96    }
97
98    let adj_py: Vec<Vec<(usize, f64)>> = adj_list;
99    dict.set_item("adjacency", adj_py)?;
100
101    Ok(dict.into())
102}
103
104/// Create a directed graph from edge list
105#[pyfunction]
106fn digraph_from_edges_py(
107    py: Python,
108    edges: &Bound<'_, PyList>,
109    num_nodes: Option<usize>,
110) -> PyResult<Py<PyAny>> {
111    let mut graph = DiGraph::<usize, f64, u32>::new();
112    let mut max_node = 0usize;
113
114    for edge in edges.iter() {
115        let tuple = edge.downcast::<PyTuple>()?;
116        let u: usize = tuple.get_item(0)?.extract()?;
117        let v: usize = tuple.get_item(1)?.extract()?;
118        let weight: f64 = if tuple.len() > 2 {
119            tuple.get_item(2)?.extract()?
120        } else {
121            1.0
122        };
123
124        max_node = max_node.max(u).max(v);
125        graph.add_edge(u, v, weight);
126    }
127
128    let n = num_nodes.unwrap_or(max_node + 1);
129
130    let edges_vec = graph.edges();
131    let dict = PyDict::new(py);
132    dict.set_item("num_nodes", n)?;
133    dict.set_item("num_edges", edges_vec.len())?;
134    dict.set_item("directed", true)?;
135
136    // Convert to adjacency list
137    let mut adj_list: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
138    for edge in &edges_vec {
139        adj_list[edge.source].push((edge.target, edge.weight));
140    }
141
142    dict.set_item("adjacency", adj_list)?;
143
144    Ok(dict.into())
145}
146
147// ========================================
148// GRAPH GENERATORS
149// ========================================
150
151/// Generate Erdős-Rényi random graph
152#[pyfunction]
153fn erdos_renyi_graph_py(py: Python, n: usize, p: f64) -> PyResult<Py<PyAny>> {
154    let mut rng = thread_rng();
155    let graph = erdos_renyi_graph(n, p, &mut rng)
156        .map_err(|e| PyRuntimeError::new_err(format!("Failed to generate graph: {}", e)))?;
157
158    let edges_vec = graph.edges();
159    let dict = PyDict::new(py);
160    dict.set_item("num_nodes", n)?;
161    dict.set_item("num_edges", edges_vec.len())?;
162    dict.set_item("directed", false)?;
163
164    // Extract edges
165    let edges: Vec<(usize, usize, f64)> = edges_vec
166        .iter()
167        .map(|e| (e.source, e.target, e.weight))
168        .collect();
169    dict.set_item("edges", edges)?;
170
171    Ok(dict.into())
172}
173
174/// Generate Barabási-Albert preferential attachment graph
175#[pyfunction]
176fn barabasi_albert_graph_py(py: Python, n: usize, m: usize) -> PyResult<Py<PyAny>> {
177    let mut rng = thread_rng();
178    let graph = barabasi_albert_graph(n, m, &mut rng)
179        .map_err(|e| PyRuntimeError::new_err(format!("Failed to generate graph: {}", e)))?;
180
181    let edges_vec = graph.edges();
182    let dict = PyDict::new(py);
183    dict.set_item("num_nodes", n)?;
184    dict.set_item("num_edges", edges_vec.len())?;
185    dict.set_item("directed", false)?;
186
187    let edges: Vec<(usize, usize, f64)> = edges_vec
188        .iter()
189        .map(|e| (e.source, e.target, e.weight))
190        .collect();
191    dict.set_item("edges", edges)?;
192
193    Ok(dict.into())
194}
195
196/// Generate Watts-Strogatz small-world graph
197#[pyfunction]
198fn watts_strogatz_graph_py(py: Python, n: usize, k: usize, p: f64) -> PyResult<Py<PyAny>> {
199    let mut rng = thread_rng();
200    let graph = watts_strogatz_graph(n, k, p, &mut rng)
201        .map_err(|e| PyRuntimeError::new_err(format!("Failed to generate graph: {}", e)))?;
202
203    let edges_vec = graph.edges();
204    let dict = PyDict::new(py);
205    dict.set_item("num_nodes", n)?;
206    dict.set_item("num_edges", edges_vec.len())?;
207    dict.set_item("directed", false)?;
208
209    let edges: Vec<(usize, usize, f64)> = edges_vec
210        .iter()
211        .map(|e| (e.source, e.target, e.weight))
212        .collect();
213    dict.set_item("edges", edges)?;
214
215    Ok(dict.into())
216}
217
218/// Generate complete graph
219#[pyfunction]
220fn complete_graph_py(py: Python, n: usize) -> PyResult<Py<PyAny>> {
221    let graph = complete_graph(n)
222        .map_err(|e| PyRuntimeError::new_err(format!("Failed to generate graph: {}", e)))?;
223
224    let edges_vec = graph.edges();
225    let dict = PyDict::new(py);
226    dict.set_item("num_nodes", n)?;
227    dict.set_item("num_edges", edges_vec.len())?;
228    dict.set_item("directed", false)?;
229
230    let edges: Vec<(usize, usize, f64)> = edges_vec
231        .iter()
232        .map(|e| (e.source, e.target, e.weight))
233        .collect();
234    dict.set_item("edges", edges)?;
235
236    Ok(dict.into())
237}
238
239/// Generate path graph
240#[pyfunction]
241fn path_graph_py(py: Python, n: usize) -> PyResult<Py<PyAny>> {
242    let graph = path_graph(n)
243        .map_err(|e| PyRuntimeError::new_err(format!("Failed to generate graph: {}", e)))?;
244
245    let edges_vec = graph.edges();
246    let dict = PyDict::new(py);
247    dict.set_item("num_nodes", n)?;
248    dict.set_item("num_edges", edges_vec.len())?;
249    dict.set_item("directed", false)?;
250
251    let edges: Vec<(usize, usize, f64)> = edges_vec
252        .iter()
253        .map(|e| (e.source, e.target, e.weight))
254        .collect();
255    dict.set_item("edges", edges)?;
256
257    Ok(dict.into())
258}
259
260/// Generate cycle graph
261#[pyfunction]
262fn cycle_graph_py(py: Python, n: usize) -> PyResult<Py<PyAny>> {
263    let graph = cycle_graph(n)
264        .map_err(|e| PyRuntimeError::new_err(format!("Failed to generate graph: {}", e)))?;
265
266    let edges_vec = graph.edges();
267    let dict = PyDict::new(py);
268    dict.set_item("num_nodes", n)?;
269    dict.set_item("num_edges", edges_vec.len())?;
270    dict.set_item("directed", false)?;
271
272    let edges: Vec<(usize, usize, f64)> = edges_vec
273        .iter()
274        .map(|e| (e.source, e.target, e.weight))
275        .collect();
276    dict.set_item("edges", edges)?;
277
278    Ok(dict.into())
279}
280
281/// Generate star graph
282#[pyfunction]
283fn star_graph_py(py: Python, n: usize) -> PyResult<Py<PyAny>> {
284    let graph = star_graph(n)
285        .map_err(|e| PyRuntimeError::new_err(format!("Failed to generate graph: {}", e)))?;
286
287    let edges_vec = graph.edges();
288    let dict = PyDict::new(py);
289    dict.set_item("num_nodes", n)?;
290    dict.set_item("num_edges", edges_vec.len())?;
291    dict.set_item("directed", false)?;
292
293    let edges: Vec<(usize, usize, f64)> = edges_vec
294        .iter()
295        .map(|e| (e.source, e.target, e.weight))
296        .collect();
297    dict.set_item("edges", edges)?;
298
299    Ok(dict.into())
300}
301
302// ========================================
303// TRAVERSAL ALGORITHMS
304// ========================================
305
306/// Breadth-first search from a source node
307#[pyfunction]
308fn bfs_py(
309    py: Python,
310    edges: &Bound<'_, PyList>,
311    source: usize,
312    num_nodes: Option<usize>,
313) -> PyResult<Py<PyAny>> {
314    let mut graph = Graph::<usize, f64, u32>::new();
315    let mut max_node = 0usize;
316
317    for edge in edges.iter() {
318        let tuple = edge.downcast::<PyTuple>()?;
319        let u: usize = tuple.get_item(0)?.extract()?;
320        let v: usize = tuple.get_item(1)?.extract()?;
321        max_node = max_node.max(u).max(v);
322        graph.add_edge(u, v, 1.0);
323    }
324
325    let _n = num_nodes.unwrap_or(max_node + 1);
326
327    let result = breadth_first_search(&graph, &source)
328        .map_err(|e| PyRuntimeError::new_err(format!("BFS failed: {}", e)))?;
329
330    let dict = PyDict::new(py);
331    dict.set_item("order", result)?;
332
333    Ok(dict.into())
334}
335
336/// Depth-first search from a source node
337#[pyfunction]
338fn dfs_py(
339    py: Python,
340    edges: &Bound<'_, PyList>,
341    source: usize,
342    num_nodes: Option<usize>,
343) -> PyResult<Py<PyAny>> {
344    let mut graph = Graph::<usize, f64, u32>::new();
345    let mut max_node = 0usize;
346
347    for edge in edges.iter() {
348        let tuple = edge.downcast::<PyTuple>()?;
349        let u: usize = tuple.get_item(0)?.extract()?;
350        let v: usize = tuple.get_item(1)?.extract()?;
351        max_node = max_node.max(u).max(v);
352        graph.add_edge(u, v, 1.0);
353    }
354
355    let _n = num_nodes.unwrap_or(max_node + 1);
356
357    let result = depth_first_search(&graph, &source)
358        .map_err(|e| PyRuntimeError::new_err(format!("DFS failed: {}", e)))?;
359
360    let dict = PyDict::new(py);
361    dict.set_item("order", result)?;
362
363    Ok(dict.into())
364}
365
366// ========================================
367// SHORTEST PATH ALGORITHMS
368// ========================================
369
370/// Dijkstra's shortest path algorithm
371#[pyfunction]
372fn dijkstra_py(
373    py: Python,
374    edges: &Bound<'_, PyList>,
375    source: usize,
376    target: usize,
377) -> PyResult<Py<PyAny>> {
378    let mut graph = Graph::<usize, f64, u32>::new();
379
380    for edge in edges.iter() {
381        let tuple: &Bound<'_, PyTuple> = edge.downcast()?;
382        let u: usize = tuple.get_item(0)?.extract()?;
383        let v: usize = tuple.get_item(1)?.extract()?;
384        let weight: f64 = if tuple.len() > 2 {
385            tuple.get_item(2)?.extract()?
386        } else {
387            1.0
388        };
389        graph.add_edge(u, v, weight);
390    }
391
392    let result = dijkstra_path(&graph, &source, &target)
393        .map_err(|e| PyRuntimeError::new_err(format!("Dijkstra failed: {}", e)))?;
394
395    let dict = PyDict::new(py);
396    if let Some(path) = result {
397        dict.set_item("path", path.nodes)?;
398        dict.set_item("distance", path.total_weight)?;
399    } else {
400        return Err(PyRuntimeError::new_err("No path found"));
401    }
402
403    Ok(dict.into())
404}
405
406/// Floyd-Warshall all-pairs shortest paths
407#[pyfunction]
408#[allow(unused_variables)]
409fn floyd_warshall_py(
410    py: Python,
411    edges: &Bound<'_, PyList>,
412    num_nodes: usize,
413) -> PyResult<Py<PyArray2<f64>>> {
414    let mut graph = Graph::<usize, f64, u32>::new();
415
416    for edge in edges.iter() {
417        let tuple: &Bound<'_, PyTuple> = edge.downcast()?;
418        let u: usize = tuple.get_item(0)?.extract()?;
419        let v: usize = tuple.get_item(1)?.extract()?;
420        let weight: f64 = if tuple.len() > 2 {
421            tuple.get_item(2)?.extract()?
422        } else {
423            1.0
424        };
425        graph.add_edge(u, v, weight);
426    }
427
428    let result = floyd_warshall(&graph)
429        .map_err(|e| PyRuntimeError::new_err(format!("Floyd-Warshall failed: {}", e)))?;
430
431    Ok(result.into_pyarray(py).unbind())
432}
433
434// ========================================
435// CONNECTIVITY
436// ========================================
437
438/// Find connected components
439#[pyfunction]
440#[allow(unused_variables)]
441fn connected_components_py(
442    py: Python,
443    edges: &Bound<'_, PyList>,
444    num_nodes: usize,
445) -> PyResult<Py<PyAny>> {
446    let mut graph = Graph::<usize, f64, u32>::new();
447
448    for edge in edges.iter() {
449        let tuple: &Bound<'_, PyTuple> = edge.downcast()?;
450        let u: usize = tuple.get_item(0)?.extract()?;
451        let v: usize = tuple.get_item(1)?.extract()?;
452        graph.add_edge(u, v, 1.0);
453    }
454
455    let components = connected_components(&graph);
456
457    let dict = PyDict::new(py);
458    dict.set_item("n_components", components.len())?;
459
460    // Convert components to list of lists (Component<N> = HashSet<N>)
461    let comp_list: Vec<Vec<usize>> = components
462        .into_iter()
463        .map(|c: std::collections::HashSet<usize>| c.into_iter().collect())
464        .collect();
465    dict.set_item("components", comp_list)?;
466
467    Ok(dict.into())
468}
469
470/// Find strongly connected components (directed graphs)
471#[pyfunction]
472#[allow(unused_variables)]
473fn strongly_connected_components_py(
474    py: Python,
475    edges: &Bound<'_, PyList>,
476    num_nodes: usize,
477) -> PyResult<Py<PyAny>> {
478    let mut graph = DiGraph::<usize, f64, u32>::new();
479
480    for edge in edges.iter() {
481        let tuple: &Bound<'_, PyTuple> = edge.downcast()?;
482        let u: usize = tuple.get_item(0)?.extract()?;
483        let v: usize = tuple.get_item(1)?.extract()?;
484        graph.add_edge(u, v, 1.0);
485    }
486
487    let components = strongly_connected_components(&graph);
488
489    let dict = PyDict::new(py);
490    dict.set_item("n_components", components.len())?;
491
492    let comp_list: Vec<Vec<usize>> = components
493        .into_iter()
494        .map(|c: std::collections::HashSet<usize>| c.into_iter().collect())
495        .collect();
496    dict.set_item("components", comp_list)?;
497
498    Ok(dict.into())
499}
500
501/// Find articulation points (cut vertices)
502#[pyfunction]
503#[allow(unused_variables)]
504fn articulation_points_py(
505    py: Python,
506    edges: &Bound<'_, PyList>,
507    num_nodes: usize,
508) -> PyResult<Py<PyAny>> {
509    let mut graph = Graph::<usize, f64, u32>::new();
510
511    for edge in edges.iter() {
512        let tuple = edge.downcast::<PyTuple>()?;
513        let u: usize = tuple.get_item(0)?.extract()?;
514        let v: usize = tuple.get_item(1)?.extract()?;
515        graph.add_edge(u, v, 1.0);
516    }
517
518    // articulation_points takes only graph, returns HashSet (not Result)
519    let points = articulation_points(&graph);
520
521    let dict = PyDict::new(py);
522    let points_vec: Vec<usize> = points.into_iter().collect();
523    dict.set_item("articulation_points", points_vec)?;
524
525    Ok(dict.into())
526}
527
528/// Find bridges (cut edges)
529#[pyfunction]
530#[allow(unused_variables)]
531fn bridges_py(py: Python, edges: &Bound<'_, PyList>, num_nodes: usize) -> PyResult<Py<PyAny>> {
532    let mut graph = Graph::<usize, f64, u32>::new();
533
534    for edge in edges.iter() {
535        let tuple = edge.downcast::<PyTuple>()?;
536        let u: usize = tuple.get_item(0)?.extract()?;
537        let v: usize = tuple.get_item(1)?.extract()?;
538        graph.add_edge(u, v, 1.0);
539    }
540
541    // bridges takes only graph, returns Vec<(N, N)> (not Result)
542    let bridge_list = bridges(&graph);
543
544    let dict = PyDict::new(py);
545    let bridges_vec: Vec<(usize, usize)> = bridge_list;
546    dict.set_item("bridges", bridges_vec)?;
547
548    Ok(dict.into())
549}
550
551/// Check if graph is bipartite
552#[pyfunction]
553#[allow(unused_variables)]
554fn is_bipartite_py(py: Python, edges: &Bound<'_, PyList>, num_nodes: usize) -> PyResult<Py<PyAny>> {
555    let mut graph = Graph::<usize, f64, u32>::new();
556
557    for edge in edges.iter() {
558        let tuple = edge.downcast::<PyTuple>()?;
559        let u: usize = tuple.get_item(0)?.extract()?;
560        let v: usize = tuple.get_item(1)?.extract()?;
561        graph.add_edge(u, v, 1.0);
562    }
563
564    // is_bipartite takes only graph, returns BipartiteResult (not Result)
565    let result = is_bipartite(&graph);
566
567    let dict = PyDict::new(py);
568    dict.set_item("is_bipartite", result.is_bipartite)?;
569
570    if result.is_bipartite {
571        // BipartiteResult has coloring: HashMap<N, u8> where 0 is left and 1 is right
572        let left: Vec<usize> = result
573            .coloring
574            .iter()
575            .filter(|(_, &color)| color == 0)
576            .map(|(&node, _)| node)
577            .collect();
578        let right: Vec<usize> = result
579            .coloring
580            .iter()
581            .filter(|(_, &color)| color == 1)
582            .map(|(&node, _)| node)
583            .collect();
584        dict.set_item("left", left)?;
585        dict.set_item("right", right)?;
586    }
587
588    Ok(dict.into())
589}
590
591// ========================================
592// CENTRALITY MEASURES
593// ========================================
594
595/// Calculate betweenness centrality
596#[pyfunction]
597#[pyo3(signature = (edges, num_nodes, normalized=true))]
598fn betweenness_centrality_py(
599    py: Python,
600    edges: &Bound<'_, PyList>,
601    num_nodes: usize,
602    normalized: bool,
603) -> PyResult<Py<PyAny>> {
604    let mut graph = Graph::<usize, f64, u32>::new();
605
606    for edge in edges.iter() {
607        let tuple = edge.downcast::<PyTuple>()?;
608        let u: usize = tuple.get_item(0)?.extract()?;
609        let v: usize = tuple.get_item(1)?.extract()?;
610        graph.add_edge(u, v, 1.0);
611    }
612
613    // betweenness_centrality takes (graph, normalized), returns HashMap (not Result)
614    let centrality = betweenness_centrality(&graph, normalized);
615
616    let dict = PyDict::new(py);
617    let centrality_vec: Vec<f64> = (0..num_nodes)
618        .map(|i| *centrality.get(&i).unwrap_or(&0.0))
619        .collect();
620    dict.set_item("centrality", centrality_vec)?;
621
622    Ok(dict.into())
623}
624
625/// Calculate closeness centrality
626#[pyfunction]
627#[pyo3(signature = (edges, num_nodes, normalized=true))]
628fn closeness_centrality_py(
629    py: Python,
630    edges: &Bound<'_, PyList>,
631    num_nodes: usize,
632    normalized: bool,
633) -> PyResult<Py<PyAny>> {
634    let mut graph = Graph::<usize, f64, u32>::new();
635
636    for edge in edges.iter() {
637        let tuple = edge.downcast::<PyTuple>()?;
638        let u: usize = tuple.get_item(0)?.extract()?;
639        let v: usize = tuple.get_item(1)?.extract()?;
640        graph.add_edge(u, v, 1.0);
641    }
642
643    // closeness_centrality takes (graph, normalized), returns HashMap (not Result)
644    let centrality = closeness_centrality(&graph, normalized);
645
646    let dict = PyDict::new(py);
647    let centrality_vec: Vec<f64> = (0..num_nodes)
648        .map(|i| *centrality.get(&i).unwrap_or(&0.0))
649        .collect();
650    dict.set_item("centrality", centrality_vec)?;
651
652    Ok(dict.into())
653}
654
655/// Calculate PageRank
656#[pyfunction]
657#[pyo3(signature = (edges, num_nodes, damping=0.85, _max_iter=100, tol=1e-6))]
658#[allow(unused_variables)]
659fn pagerank_py(
660    py: Python,
661    edges: &Bound<'_, PyList>,
662    num_nodes: usize,
663    damping: f64,
664    _max_iter: usize,
665    tol: f64,
666) -> PyResult<Py<PyAny>> {
667    let mut graph = Graph::<usize, f64, u32>::new();
668
669    for edge in edges.iter() {
670        let tuple = edge.downcast::<PyTuple>()?;
671        let u: usize = tuple.get_item(0)?.extract()?;
672        let v: usize = tuple.get_item(1)?.extract()?;
673        graph.add_edge(u, v, 1.0);
674    }
675
676    // pagerank_centrality takes (graph, damping, tolerance) - max_iter is hardcoded internally
677    let pr = pagerank_centrality(&graph, damping, tol)
678        .map_err(|e| PyRuntimeError::new_err(format!("PageRank failed: {}", e)))?;
679
680    let dict = PyDict::new(py);
681    let pr_vec: Vec<f64> = (0..num_nodes)
682        .map(|i| *pr.get(&i).unwrap_or(&0.0))
683        .collect();
684    dict.set_item("pagerank", pr_vec)?;
685
686    Ok(dict.into())
687}
688
689// ========================================
690// COMMUNITY DETECTION
691// ========================================
692
693/// Louvain community detection
694#[pyfunction]
695#[allow(unused_variables)]
696fn louvain_communities_py(
697    py: Python,
698    edges: &Bound<'_, PyList>,
699    num_nodes: usize,
700) -> PyResult<Py<PyAny>> {
701    let mut graph = Graph::<usize, f64, u32>::new();
702
703    for edge in edges.iter() {
704        let tuple = edge.downcast::<PyTuple>()?;
705        let u: usize = tuple.get_item(0)?.extract()?;
706        let v: usize = tuple.get_item(1)?.extract()?;
707        let weight: f64 = if tuple.len() > 2 {
708            tuple.get_item(2)?.extract()?
709        } else {
710            1.0
711        };
712        graph.add_edge(u, v, weight);
713    }
714
715    // louvain_communities_result takes only graph, returns CommunityResult (not Result)
716    let result = louvain_communities_result(&graph);
717
718    let dict = PyDict::new(py);
719    dict.set_item("n_communities", result.communities.len())?;
720    // CommunityResult uses quality_score instead of modularity
721    dict.set_item("modularity", result.quality_score)?;
722
723    // Convert communities (Vec<HashSet<N>>) to list of lists
724    let comm_list: Vec<Vec<usize>> = result
725        .communities
726        .into_iter()
727        .map(|c: std::collections::HashSet<usize>| c.into_iter().collect())
728        .collect();
729    dict.set_item("communities", comm_list)?;
730
731    Ok(dict.into())
732}
733
734/// Label propagation community detection
735#[pyfunction]
736#[pyo3(signature = (edges, num_nodes, max_iterations=100))]
737#[allow(unused_variables)]
738fn label_propagation_py(
739    py: Python,
740    edges: &Bound<'_, PyList>,
741    num_nodes: usize,
742    max_iterations: usize,
743) -> PyResult<Py<PyAny>> {
744    let mut graph = Graph::<usize, f64, u32>::new();
745
746    for edge in edges.iter() {
747        let tuple = edge.downcast::<PyTuple>()?;
748        let u: usize = tuple.get_item(0)?.extract()?;
749        let v: usize = tuple.get_item(1)?.extract()?;
750        graph.add_edge(u, v, 1.0);
751    }
752
753    // label_propagation_result takes graph and max_iterations, returns CommunityResult
754    let result = label_propagation_result(&graph, max_iterations);
755
756    let dict = PyDict::new(py);
757    dict.set_item("n_communities", result.communities.len())?;
758
759    let comm_list: Vec<Vec<usize>> = result
760        .communities
761        .into_iter()
762        .map(|c: std::collections::HashSet<usize>| c.into_iter().collect())
763        .collect();
764    dict.set_item("communities", comm_list)?;
765
766    Ok(dict.into())
767}
768
769/// Calculate modularity of a partition
770#[pyfunction]
771#[allow(unused_variables)]
772fn modularity_py(
773    edges: &Bound<'_, PyList>,
774    communities: &Bound<'_, PyList>,
775    num_nodes: usize,
776) -> PyResult<f64> {
777    let mut graph = Graph::<usize, f64, u32>::new();
778
779    for edge in edges.iter() {
780        let tuple = edge.downcast::<PyTuple>()?;
781        let u: usize = tuple.get_item(0)?.extract()?;
782        let v: usize = tuple.get_item(1)?.extract()?;
783        let weight: f64 = if tuple.len() > 2 {
784            tuple.get_item(2)?.extract()?
785        } else {
786            1.0
787        };
788        graph.add_edge(u, v, weight);
789    }
790
791    // Convert communities from Python - modularity expects HashMap<N, usize> (node -> community_id)
792    let mut comm_map: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
793    for (comm_id, comm) in communities.iter().enumerate() {
794        let nodes: Vec<usize> = comm.extract()?;
795        for node in nodes {
796            comm_map.insert(node, comm_id);
797        }
798    }
799
800    // modularity returns f64 directly (no Result wrapper)
801    Ok(modularity(&graph, &comm_map))
802}
803
804// ========================================
805// GRAPH PROPERTIES
806// ========================================
807
808/// Calculate graph diameter
809#[pyfunction]
810#[allow(unused_variables)]
811fn diameter_py(edges: &Bound<'_, PyList>, num_nodes: usize) -> PyResult<f64> {
812    let mut graph = Graph::<usize, f64, u32>::new();
813
814    for edge in edges.iter() {
815        let tuple = edge.downcast::<PyTuple>()?;
816        let u: usize = tuple.get_item(0)?.extract()?;
817        let v: usize = tuple.get_item(1)?.extract()?;
818        graph.add_edge(u, v, 1.0);
819    }
820
821    // diameter returns Option<f64>
822    diameter(&graph).ok_or_else(|| PyRuntimeError::new_err("Graph is not connected or empty"))
823}
824
825/// Calculate clustering coefficient
826#[pyfunction]
827fn clustering_coefficient_py(
828    py: Python,
829    edges: &Bound<'_, PyList>,
830    num_nodes: usize,
831) -> PyResult<Py<PyAny>> {
832    let mut graph = Graph::<usize, f64, u32>::new();
833
834    for edge in edges.iter() {
835        let tuple = edge.downcast::<PyTuple>()?;
836        let u: usize = tuple.get_item(0)?.extract()?;
837        let v: usize = tuple.get_item(1)?.extract()?;
838        graph.add_edge(u, v, 1.0);
839    }
840
841    // clustering_coefficient takes only graph, returns Result<HashMap<N, f64>>
842    let coeffs = clustering_coefficient(&graph)
843        .map_err(|e| PyRuntimeError::new_err(format!("Clustering coefficient failed: {}", e)))?;
844
845    let dict = PyDict::new(py);
846    let coeff_vec: Vec<f64> = (0..num_nodes)
847        .map(|i| *coeffs.get(&i).unwrap_or(&0.0))
848        .collect();
849
850    // Calculate average before moving coeff_vec
851    let avg: f64 = coeff_vec.iter().sum::<f64>() / num_nodes as f64;
852    dict.set_item("coefficients", coeff_vec)?;
853    dict.set_item("average", avg)?;
854
855    Ok(dict.into())
856}
857
858/// Calculate graph density
859#[pyfunction]
860fn graph_density_py(edges: &Bound<'_, PyList>, num_nodes: usize, directed: bool) -> PyResult<f64> {
861    let num_edges = edges.len();
862
863    if directed {
864        let max_edges = num_nodes * (num_nodes - 1);
865        Ok(num_edges as f64 / max_edges as f64)
866    } else {
867        let max_edges = num_nodes * (num_nodes - 1) / 2;
868        Ok(num_edges as f64 / max_edges as f64)
869    }
870}
871
872// ========================================
873// SPANNING TREES
874// ========================================
875
876/// Compute minimum spanning tree
877#[pyfunction]
878#[allow(unused_variables)]
879fn minimum_spanning_tree_py(
880    py: Python,
881    edges: &Bound<'_, PyList>,
882    num_nodes: usize,
883) -> PyResult<Py<PyAny>> {
884    let mut graph = Graph::<usize, f64, u32>::new();
885
886    for edge in edges.iter() {
887        let tuple = edge.downcast::<PyTuple>()?;
888        let u: usize = tuple.get_item(0)?.extract()?;
889        let v: usize = tuple.get_item(1)?.extract()?;
890        let weight: f64 = if tuple.len() > 2 {
891            tuple.get_item(2)?.extract()?
892        } else {
893            1.0
894        };
895        graph.add_edge(u, v, weight);
896    }
897
898    // minimum_spanning_tree returns Result<Vec<Edge<N, E>>>
899    let mst = minimum_spanning_tree(&graph)
900        .map_err(|e| PyRuntimeError::new_err(format!("MST failed: {}", e)))?;
901
902    let dict = PyDict::new(py);
903    // mst is a Vec<Edge>, where Edge has source, target, weight fields
904    let mst_edges: Vec<(usize, usize, f64)> =
905        mst.iter().map(|e| (e.source, e.target, e.weight)).collect();
906    dict.set_item("edges", mst_edges.clone())?;
907
908    let total_weight: f64 = mst_edges.iter().map(|(_, _, w)| *w).sum();
909    dict.set_item("total_weight", total_weight)?;
910
911    Ok(dict.into())
912}
913
914/// Python module registration
915pub fn register_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
916    // Graph creation
917    m.add_function(wrap_pyfunction!(graph_from_edges_py, m)?)?;
918    m.add_function(wrap_pyfunction!(digraph_from_edges_py, m)?)?;
919
920    // Graph generators
921    m.add_function(wrap_pyfunction!(erdos_renyi_graph_py, m)?)?;
922    m.add_function(wrap_pyfunction!(barabasi_albert_graph_py, m)?)?;
923    m.add_function(wrap_pyfunction!(watts_strogatz_graph_py, m)?)?;
924    m.add_function(wrap_pyfunction!(complete_graph_py, m)?)?;
925    m.add_function(wrap_pyfunction!(path_graph_py, m)?)?;
926    m.add_function(wrap_pyfunction!(cycle_graph_py, m)?)?;
927    m.add_function(wrap_pyfunction!(star_graph_py, m)?)?;
928
929    // Traversal
930    m.add_function(wrap_pyfunction!(bfs_py, m)?)?;
931    m.add_function(wrap_pyfunction!(dfs_py, m)?)?;
932
933    // Shortest paths
934    m.add_function(wrap_pyfunction!(dijkstra_py, m)?)?;
935    m.add_function(wrap_pyfunction!(floyd_warshall_py, m)?)?;
936
937    // Connectivity
938    m.add_function(wrap_pyfunction!(connected_components_py, m)?)?;
939    m.add_function(wrap_pyfunction!(strongly_connected_components_py, m)?)?;
940    m.add_function(wrap_pyfunction!(articulation_points_py, m)?)?;
941    m.add_function(wrap_pyfunction!(bridges_py, m)?)?;
942    m.add_function(wrap_pyfunction!(is_bipartite_py, m)?)?;
943
944    // Centrality
945    m.add_function(wrap_pyfunction!(betweenness_centrality_py, m)?)?;
946    m.add_function(wrap_pyfunction!(closeness_centrality_py, m)?)?;
947    m.add_function(wrap_pyfunction!(pagerank_py, m)?)?;
948
949    // Community detection
950    m.add_function(wrap_pyfunction!(louvain_communities_py, m)?)?;
951    m.add_function(wrap_pyfunction!(label_propagation_py, m)?)?;
952    m.add_function(wrap_pyfunction!(modularity_py, m)?)?;
953
954    // Graph properties
955    m.add_function(wrap_pyfunction!(diameter_py, m)?)?;
956    m.add_function(wrap_pyfunction!(clustering_coefficient_py, m)?)?;
957    m.add_function(wrap_pyfunction!(graph_density_py, m)?)?;
958
959    // Spanning trees
960    m.add_function(wrap_pyfunction!(minimum_spanning_tree_py, m)?)?;
961
962    Ok(())
963}