pub mod centrality;
pub mod components;
pub mod core;
pub mod path;
pub mod traversal;
pub use core::{Edge, EdgeId, Graph, GraphBuilder, GraphError, GraphType, Node, NodeId};
pub use traversal::{
bfs, bfs_reachable, center, dfs, dfs_from, dfs_iterative, diameter, eccentricity, has_cycle,
nodes_at_distance, radius, shortest_path_bfs, topological_sort, BfsResult, DfsResult,
};
pub use centrality::{
betweenness_centrality, closeness_centrality, degree_centrality, eigenvector_centrality,
eigenvector_centrality_default, hits, hits_default, in_degree_centrality, katz_centrality,
katz_centrality_default, out_degree_centrality, pagerank, pagerank_default,
};
pub use path::{
all_shortest_paths, astar, bellman_ford, bellman_ford_default, dijkstra, dijkstra_default,
floyd_warshall, floyd_warshall_default, k_shortest_paths, AllPairsShortestPaths,
ShortestPathResult,
};
pub use components::{
connected_components, find_articulation_points, find_bridges, is_connected, label_propagation,
louvain, louvain_default, modularity, strongly_connected_components,
weakly_connected_components, ComponentResult,
};
use crate::{DataFrame, Series};
use std::collections::HashMap;
use std::fmt::Debug;
use std::hash::Hash;
pub fn from_edge_dataframe(
df: &DataFrame,
source_col: &str,
target_col: &str,
weight_col: Option<&str>,
directed: bool,
) -> Result<Graph<String, f64>, GraphError> {
let graph_type = if directed {
GraphType::Directed
} else {
GraphType::Undirected
};
let mut graph: Graph<String, f64> = Graph::new(graph_type);
let mut node_map: HashMap<String, NodeId> = HashMap::new();
let source_values = df.get_column_string_values(source_col).map_err(|e| {
GraphError::InvalidOperation(format!("Column '{}' error: {}", source_col, e))
})?;
let target_values = df.get_column_string_values(target_col).map_err(|e| {
GraphError::InvalidOperation(format!("Column '{}' error: {}", target_col, e))
})?;
let weight_values = weight_col.and_then(|col| df.get_column_numeric_values(col).ok());
let n_rows = source_values.len();
for i in 0..n_rows {
let source_val = &source_values[i];
let target_val = &target_values[i];
let source_id = if let Some(&id) = node_map.get(source_val) {
id
} else {
let id = graph.add_node(source_val.clone());
node_map.insert(source_val.clone(), id);
id
};
let target_id = if let Some(&id) = node_map.get(target_val) {
id
} else {
let id = graph.add_node(target_val.clone());
node_map.insert(target_val.clone(), id);
id
};
let weight = weight_values
.as_ref()
.and_then(|wv| if i < wv.len() { Some(wv[i]) } else { None });
graph.add_edge(source_id, target_id, weight)?;
}
Ok(graph)
}
pub fn to_edge_dataframe<N, W>(graph: &Graph<N, W>) -> Result<DataFrame, GraphError>
where
N: Clone + Debug + ToString,
W: Clone + Debug + ToString,
{
let mut sources: Vec<String> = Vec::new();
let mut targets: Vec<String> = Vec::new();
let mut weights: Vec<String> = Vec::new();
for (_, edge) in graph.edges() {
let source_node = graph
.get_node(edge.source)
.ok_or(GraphError::NodeNotFound(edge.source))?;
let target_node = graph
.get_node(edge.target)
.ok_or(GraphError::NodeNotFound(edge.target))?;
sources.push(source_node.data.to_string());
targets.push(target_node.data.to_string());
match &edge.weight {
Some(w) => weights.push(w.to_string()),
None => weights.push("".to_string()),
}
}
let source_series = Series::new(sources, Some("source".to_string()))
.map_err(|e| GraphError::InvalidOperation(e.to_string()))?;
let target_series = Series::new(targets, Some("target".to_string()))
.map_err(|e| GraphError::InvalidOperation(e.to_string()))?;
let mut df = DataFrame::new();
df.add_column("source".to_string(), source_series)
.map_err(|e| GraphError::InvalidOperation(e.to_string()))?;
df.add_column("target".to_string(), target_series)
.map_err(|e| GraphError::InvalidOperation(e.to_string()))?;
if weights.iter().any(|w| !w.is_empty()) {
let weight_series = Series::new(weights, Some("weight".to_string()))
.map_err(|e| GraphError::InvalidOperation(e.to_string()))?;
df.add_column("weight".to_string(), weight_series)
.map_err(|e| GraphError::InvalidOperation(e.to_string()))?;
}
Ok(df)
}
pub fn from_adjacency_matrix(
matrix: &[Vec<f64>],
node_labels: Option<Vec<String>>,
directed: bool,
) -> Result<Graph<String, f64>, GraphError> {
let n = matrix.len();
if n == 0 {
return Ok(Graph::new(if directed {
GraphType::Directed
} else {
GraphType::Undirected
}));
}
for row in matrix {
if row.len() != n {
return Err(GraphError::InvalidOperation(
"Adjacency matrix must be square".to_string(),
));
}
}
let graph_type = if directed {
GraphType::Directed
} else {
GraphType::Undirected
};
let mut graph: Graph<String, f64> = Graph::new(graph_type);
let labels = node_labels.unwrap_or_else(|| (0..n).map(|i| i.to_string()).collect());
let mut node_ids: Vec<NodeId> = Vec::with_capacity(n);
for label in labels.iter().take(n) {
node_ids.push(graph.add_node(label.clone()));
}
for i in 0..n {
let start_j = if directed { 0 } else { i };
for j in start_j..n {
let weight = matrix[i][j];
if weight != 0.0 && (directed || i != j) {
let edge_weight = if weight == 1.0 { None } else { Some(weight) };
graph.add_edge(node_ids[i], node_ids[j], edge_weight)?;
}
}
}
Ok(graph)
}
pub fn to_adjacency_matrix<N, W>(graph: &Graph<N, W>) -> (Vec<Vec<f64>>, HashMap<NodeId, usize>)
where
N: Clone + Debug,
W: Clone + Debug,
{
let nodes: Vec<NodeId> = graph.node_ids().collect();
let n = nodes.len();
let node_to_idx: HashMap<NodeId, usize> =
nodes.iter().enumerate().map(|(i, &id)| (id, i)).collect();
let mut matrix = vec![vec![0.0; n]; n];
for (_, edge) in graph.edges() {
let i = node_to_idx[&edge.source];
let j = node_to_idx[&edge.target];
let weight = 1.0;
matrix[i][j] = weight;
if graph.graph_type() == GraphType::Undirected {
matrix[j][i] = weight;
}
}
(matrix, node_to_idx)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_graph_creation() {
let mut graph: Graph<&str, f64> = Graph::new(GraphType::Undirected);
let a = graph.add_node("A");
let b = graph.add_node("B");
graph
.add_edge(a, b, Some(1.0))
.expect("operation should succeed");
assert_eq!(graph.node_count(), 2);
assert_eq!(graph.edge_count(), 1);
}
#[test]
fn test_builder_pattern() {
let graph: Graph<&str, f64> = GraphBuilder::undirected()
.add_node("a", "Alice")
.add_node("b", "Bob")
.add_edge("a", "b", Some(1.0))
.build();
assert_eq!(graph.node_count(), 2);
assert_eq!(graph.edge_count(), 1);
}
#[test]
fn test_adjacency_matrix_roundtrip() {
let matrix = vec![
vec![0.0, 1.0, 0.0],
vec![1.0, 0.0, 1.0],
vec![0.0, 1.0, 0.0],
];
let graph = from_adjacency_matrix(&matrix, None, false).expect("operation should succeed");
assert_eq!(graph.node_count(), 3);
assert_eq!(graph.edge_count(), 2);
let (result_matrix, node_to_idx) = to_adjacency_matrix(&graph);
assert_eq!(result_matrix.len(), 3);
assert_eq!(result_matrix[0].len(), 3);
let original_edges: f64 = matrix.iter().flat_map(|row| row.iter()).sum();
let result_edges: f64 = result_matrix.iter().flat_map(|row| row.iter()).sum();
assert_eq!(original_edges, result_edges);
for i in 0..3 {
for j in 0..3 {
assert_eq!(result_matrix[i][j], result_matrix[j][i]);
}
}
}
#[test]
fn test_to_edge_dataframe() {
let mut graph: Graph<&str, f64> = Graph::new(GraphType::Directed);
let a = graph.add_node("A");
let b = graph.add_node("B");
let c = graph.add_node("C");
graph
.add_edge(a, b, Some(1.0))
.expect("operation should succeed");
graph
.add_edge(b, c, Some(2.0))
.expect("operation should succeed");
let df = to_edge_dataframe(&graph).expect("operation should succeed");
assert_eq!(df.row_count(), 2);
assert!(df.column_names().contains(&"source".to_string()));
assert!(df.column_names().contains(&"target".to_string()));
}
}