Skip to content

Commit ec3bb9f

Browse files
committed
WIP
1 parent fb51cb0 commit ec3bb9f

27 files changed

+2347
-245
lines changed

.github/workflows/tests.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
name: Tests
1+
name: Run Tests
22

33
on:
44
workflow_dispatch: { } # Allow manual execution
55
workflow_call: # Make this workflow callable from other workflows
6+
push:
7+
tags:
8+
- 'v*' # Trigger on version tags
69

710
jobs:
811
build:

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,12 @@ binaries = []
4141
ctor = "0.2.9"
4242
tracing = "0.1.41"
4343
tracing-subscriber = "0.3.19"
44-
petgraph = { version = "0.7.1", features = ["graphmap", "stable_graph", "matrix_graph", "serde-1", "rayon"] }
4544
rand = "0.9.0"
4645
sprs = "0.11.3"
46+
ordered-float = "5.0.0"
47+
rayon = "1.10.0"
48+
nalgebra = "0.33.2"
49+
petgraph = { version = "0.7.1", features = ["graphmap", "stable_graph", "matrix_graph", "serde-1", "rayon"] }
4750

4851
[dev-dependencies]
4952
criterion = { version = "0.5", features = ["html_reports"] }

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ format: ## Format Rust files
2222
.PHONY: test
2323
test: format ## Run tests
2424
@echo "Running tests..."
25-
DEBUG_GRAPHINA=$(DEBUG_GRAPHINA) RUST_BACKTRACE=$(RUST_BACKTRACE) cargo test -- --nocapture
25+
DEBUG_GRAPHINA=$(DEBUG_GRAPHINA) RUST_LOG=debug RUST_BACKTRACE=$(RUST_BACKTRACE) cargo test -- --nocapture
2626

2727
.PHONY: coverage
2828
coverage: format ## Generate test coverage report

README.md

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Graphina
22

33
[![Tests](https://img.shields.io/github/actions/workflow/status/habedi/graphina/tests.yml?label=tests&style=popout-square&logo=github)](https://github.com/habedi/graphina/actions/workflows/tests.yml)
4+
[![Lint](https://img.shields.io/github/actions/workflow/status/habedi/graphina/lint.yml?label=lint&style=popout-square&logo=github)](https://github.com/habedi/graphina/actions/workflows/lint.yml)
45
[![Code Coverage](https://img.shields.io/codecov/c/github/habedi/graphina?style=popout-square&logo=codecov)](https://codecov.io/gh/habedi/graphina)
56
[![CodeFactor](https://img.shields.io/codefactor/grade/github/habedi/graphina?style=popout-square&logo=codefactor)](https://www.codefactor.io/repository/github/habedi/graphina)
67
[![Crates.io](https://img.shields.io/crates/v/graphina.svg?style=popout-square&color=fc8d62&logo=rust)](https://crates.io/crates/graphina)
@@ -23,35 +24,17 @@ set of ready-to-use algorithms for graph analysis and mining.
2324
2425
## Features
2526

26-
* **Graphs**:
27-
- [x] Directed and undirected graphs
28-
- [x] Weighted and unweighted graphs
29-
30-
* **IO**:
31-
- [x] Edge list
32-
- [x] Adjacency list
33-
- [ ] GraphML
34-
- [ ] GML
35-
- [ ] JSON
36-
37-
* **Generators**:
38-
- [x] Erdős–Rényi Graph
39-
- [x] Complete Graph
40-
- [x] Bipartite Graph
41-
- [x] Star Graph
42-
- [x] Cycle Graph
43-
- [x] Watts–Strogatz Graph
44-
- [x] Barabási–Albert Graph
45-
46-
* **Algorithms**:
47-
- [ ] Graph traversal
48-
- [ ] Shortest paths
49-
- [ ] Minimum spanning tree
50-
- [ ] Connected components
51-
- [ ] Clustering, partitioning, and community detection
52-
- [ ] Centrality
53-
- [ ] Graph matching
54-
- [ ] Graph visualization
27+
| Module | Feature | Status | Notes |
28+
|------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------|---------------------------------------------|
29+
| [**Types**](src/graph/types.rs) | - ✓ Directed and undirected graphs<br>- ✓ Weighted and unweighted graphs | <small>- ✓ Tested<br>- ☐ Benchmarked</small> | Core graph types |
30+
| [**IO**](src/graph/io.rs) | - ✓ Edge list<br>- ✓ Adjacency list<br>- ☐ GraphML<br>- ☐ GML<br>- ☐ JSON | <small>- ✓ Tested<br>- ☐ Benchmarked</small> | I/O routines for reading/writing graph data |
31+
| [**Generators**](src/graph/generators.rs) | - ✓ Erdős–Rényi graph<br>- ✓ Watts–Strogatz graph<br>- ✓ Barabási–Albert graph<br>- ✓ Complete graph<br>- ✓ Bipartite graph<br>- ✓ Star graph<br>- ✓ Cycle graph | <small>- ✓ Tested<br>- ☐ Benchmarked</small> | Generators for random and structured graphs |
32+
| [**Traversal**](src/traversal/) | - ✓ Breadth-first search<br>- ✓ Depth-first search<br>- ✓ Iterative deepening DFS<br>- ✓ Bidirectional search | <small>- ✓ Tested<br>- ☐ Benchmarked</small> | Graph traversal algorithms |
33+
| [**Paths**](src/paths/) | - ✓ Dijkstra's algorithm<br>- ✓ Bellman–Ford algorithm<br>- ✓ Floyd–Warshall algorithm<br>- ✓ Johnson's algorithm<br>- ✓ A* search algorithm<br>- ✓ Iterative deepening A* search algorithm | <small>- ✓ Tested<br>- ☐ Benchmarked</small> | Shortest paths algorithms |
34+
| [**Centrality**](src/centrality/algorithms.rs) | - ✓ Degree centrality<br>- ✓ Closeness centrality<br>- ✓ Betweenness centrality<br>- ✓ Eigenvector centrality<br>- ✓ PageRank<br>- ✓ Katz centrality<br>- ✓ Harmonic centrality | <small>- ✓ Tested<br>- ☐ Benchmarked</small> | Centrality measures |
35+
| [**MST**](src/mst/algorithms.rs) | - ✓ Borůvka’s algorithm<br>- ✓ Kruskal’s algorithm<br>- ✓ Prim’s algorithm | <small>- ✓ Tested<br>- ☐ Benchmarked</small> | Minimum spanning tree algorithms |
36+
| [**Community**](src/community/algorithms.rs) | - ✓ Label Propagation<br>- ✓ Louvain Method<br>- ✓ Girvan–Newman algorithm<br>- ✓ Spectral Clustering<br>- ✓ Personalized PageRank<br>- ✓ Infomap<br>- ✓ Connected components | <small>- ✓ Tested<br>- ☐ Benchmarked</small> | Community detection and clustering |
37+
| **Remaining Modules** | - ☐ Graph matching<br>- ☐ Graph visualization | <small>Planned</small> | Future work: additional graph algorithms |
5538

5639
## Installation
5740

src/centrality/algorithms.rs

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
// File: src/centrality/algorithms.rs
2+
3+
use crate::graph::types::{BaseGraph, NodeId};
4+
use crate::paths::algorithms::dijkstra;
5+
// Our dijkstra implementation
6+
use ordered_float::OrderedFloat;
7+
use std::collections::VecDeque;
8+
9+
/// ### Degree Centrality
10+
/// For each node, degree centrality is defined as the sum of its out-degree and in-degree.
11+
pub fn degree_centrality<A, W, Ty>(graph: &BaseGraph<A, W, Ty>) -> Vec<f64>
12+
where
13+
W: Copy,
14+
Ty: crate::graph::types::GraphConstructor<A, W>,
15+
{
16+
let n = graph.node_count();
17+
let mut degree = vec![0usize; n];
18+
// Out-degree: count neighbors for each node.
19+
for (node, _) in graph.nodes() {
20+
let outdeg = graph.neighbors(node).count();
21+
degree[node.index()] += outdeg;
22+
}
23+
// In-degree: count each appearance as target in edges.
24+
for (_u, v, _w) in graph.edges() {
25+
degree[v.index()] += 1;
26+
}
27+
degree.into_iter().map(|d| d as f64).collect()
28+
}
29+
30+
/// ### Closeness Centrality
31+
/// For each node, closeness centrality is defined as (n-1) divided by the sum of shortest-path
32+
/// distances from that node to all other nodes. (If a node cannot reach all others, its centrality is 0.)
33+
/// This implementation uses our dijkstra algorithm, so it requires the graph’s weight type to be
34+
/// `OrderedFloat<f64>`.
35+
pub fn closeness_centrality<A, Ty>(graph: &BaseGraph<A, OrderedFloat<f64>, Ty>) -> Vec<f64>
36+
where
37+
Ty: crate::graph::types::GraphConstructor<A, OrderedFloat<f64>>,
38+
{
39+
let n = graph.node_count();
40+
let mut closeness = vec![0.0; n];
41+
for (node, _) in graph.nodes() {
42+
let distances = dijkstra(graph, node);
43+
let sum: f64 = distances.iter().filter_map(|&d| d.map(|od| od.0)).sum();
44+
if sum > 0.0 {
45+
closeness[node.index()] = (n as f64 - 1.0) / sum;
46+
} else {
47+
closeness[node.index()] = 0.0;
48+
}
49+
}
50+
closeness
51+
}
52+
53+
/// ### Betweenness Centrality (Unweighted)
54+
/// Using Brandes’ algorithm (based on BFS).
55+
/// Note: This is an unweighted implementation.
56+
pub fn betweenness_centrality<A, Ty>(graph: &BaseGraph<A, f64, Ty>) -> Vec<f64>
57+
where
58+
Ty: crate::graph::types::GraphConstructor<A, f64>,
59+
{
60+
let n = graph.node_count();
61+
let mut bc = vec![0.0; n];
62+
63+
// For each source node s:
64+
for (s, _) in graph.nodes() {
65+
let mut stack = Vec::new();
66+
let mut pred: Vec<Vec<NodeId>> = vec![Vec::new(); n];
67+
let mut sigma = vec![0.0; n]; // Number of shortest paths from s.
68+
let mut dist = vec![-1.0; n]; // Distance from s (-1 means infinity).
69+
sigma[s.index()] = 1.0;
70+
dist[s.index()] = 0.0;
71+
let mut queue = VecDeque::new();
72+
queue.push_back(s);
73+
74+
while let Some(v) = queue.pop_front() {
75+
stack.push(v);
76+
for w in graph.neighbors(v) {
77+
if dist[w.index()] < 0.0 {
78+
dist[w.index()] = dist[v.index()] + 1.0;
79+
queue.push_back(w);
80+
}
81+
if dist[w.index()] == dist[v.index()] + 1.0 {
82+
sigma[w.index()] += sigma[v.index()];
83+
pred[w.index()].push(v);
84+
}
85+
}
86+
}
87+
88+
let mut delta = vec![0.0; n];
89+
while let Some(w) = stack.pop() {
90+
for &v in &pred[w.index()] {
91+
delta[v.index()] +=
92+
(sigma[v.index()] / sigma[w.index()]) * (1.0 + delta[w.index()]);
93+
}
94+
if w != s {
95+
bc[w.index()] += delta[w.index()];
96+
}
97+
}
98+
}
99+
bc
100+
}
101+
102+
/// ### Eigenvector Centrality
103+
/// Computes centrality via power iteration over the adjacency matrix.
104+
/// This implementation uses plain f64.
105+
pub fn eigenvector_centrality<A, Ty>(graph: &BaseGraph<A, f64, Ty>, iterations: usize) -> Vec<f64>
106+
where
107+
Ty: crate::graph::types::GraphConstructor<A, f64>,
108+
{
109+
let n = graph.node_count();
110+
let mut centrality = vec![1.0; n];
111+
for _ in 0..iterations {
112+
let mut next = vec![0.0; n];
113+
for (node, _) in graph.nodes() {
114+
for neighbor in graph.neighbors(node) {
115+
next[neighbor.index()] += centrality[node.index()];
116+
}
117+
}
118+
let norm = next.iter().map(|x| x * x).sum::<f64>().sqrt();
119+
if norm > 0.0 {
120+
for x in next.iter_mut() {
121+
*x /= norm;
122+
}
123+
}
124+
centrality = next;
125+
}
126+
centrality
127+
}
128+
129+
/// ### PageRank
130+
/// Computes PageRank scores using iterative updates with damping.
131+
pub fn pagerank<A, Ty>(graph: &BaseGraph<A, f64, Ty>, damping: f64, iterations: usize) -> Vec<f64>
132+
where
133+
Ty: crate::graph::types::GraphConstructor<A, f64>,
134+
{
135+
let n = graph.node_count();
136+
let mut rank = vec![1.0 / n as f64; n];
137+
let teleport = (1.0 - damping) / n as f64;
138+
139+
// Precompute out-degree for each node.
140+
let mut out_deg = vec![0usize; n];
141+
for (node, _) in graph.nodes() {
142+
out_deg[node.index()] = graph.neighbors(node).count();
143+
}
144+
145+
for _ in 0..iterations {
146+
let mut new_rank = vec![teleport; n];
147+
for (u, _) in graph.nodes() {
148+
let r = rank[u.index()];
149+
if out_deg[u.index()] > 0 {
150+
let share = damping * r / out_deg[u.index()] as f64;
151+
for v in graph.neighbors(u) {
152+
new_rank[v.index()] += share;
153+
}
154+
} else {
155+
// Distribute uniformly if no out-edges.
156+
for x in new_rank.iter_mut() {
157+
*x += damping * r / n as f64;
158+
}
159+
}
160+
}
161+
rank = new_rank;
162+
}
163+
rank
164+
}
165+
166+
/// ### Katz Centrality
167+
/// Computes centrality using the formula: x = alpha * A * x + beta.
168+
/// beta is a constant offset for each node.
169+
pub fn katz_centrality<A, Ty>(
170+
graph: &BaseGraph<A, f64, Ty>,
171+
alpha: f64,
172+
beta: f64,
173+
iterations: usize,
174+
) -> Vec<f64>
175+
where
176+
Ty: crate::graph::types::GraphConstructor<A, f64>,
177+
{
178+
let n = graph.node_count();
179+
let mut centrality = vec![beta; n];
180+
for _ in 0..iterations {
181+
let mut next = vec![beta; n];
182+
for (node, _) in graph.nodes() {
183+
for neighbor in graph.neighbors(node) {
184+
next[neighbor.index()] += alpha * centrality[node.index()];
185+
}
186+
}
187+
let norm: f64 = next.iter().map(|&x| x * x).sum::<f64>().sqrt();
188+
if norm > 0.0 {
189+
for x in next.iter_mut() {
190+
*x /= norm;
191+
}
192+
}
193+
centrality = next;
194+
}
195+
centrality
196+
}
197+
198+
/// ### Harmonic Centrality
199+
/// For each node, harmonic centrality is defined as the sum of the reciprocals of the shortest
200+
/// path distances to all other nodes (ignoring unreachable nodes). Uses dijkstra,
201+
/// so requires weights to be OrderedFloat<f64>.
202+
pub fn harmonic_centrality<A, Ty>(graph: &BaseGraph<A, OrderedFloat<f64>, Ty>) -> Vec<f64>
203+
where
204+
Ty: crate::graph::types::GraphConstructor<A, OrderedFloat<f64>>,
205+
{
206+
let n = graph.node_count();
207+
let mut centrality = vec![0.0; n];
208+
for (node, _) in graph.nodes() {
209+
let distances = dijkstra(graph, node);
210+
let sum: f64 = distances
211+
.iter()
212+
.filter_map(|&d| d.map(|od| od.0))
213+
.filter(|&d| d > 0.0)
214+
.map(|d| 1.0 / d)
215+
.sum();
216+
centrality[node.index()] = sum;
217+
}
218+
centrality
219+
}

src/centrality/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1+
pub mod algorithms;

0 commit comments

Comments
 (0)