Skip to main content

Graph

Struct Graph 

Source
pub struct Graph { /* private fields */ }
Expand description

Graph structure using CSR (Compressed Sparse Row) for cache efficiency.

Memory layout inspired by Combinatorial BLAS (Buluc et al. 2009):

  • Adjacency stored as two flat vectors (CSR format)
  • Node labels stored separately (accessed rarely)
  • String→NodeId mapping via HashMap (build-time only)

§Performance

  • Memory: 50-70% reduction vs HashMap (no pointer overhead)
  • Cache misses: 3-5x fewer (sequential access pattern)
  • SIMD-friendly: Neighbor iteration can use vectorization

Implementations§

Source§

impl Graph

Source

pub fn new(is_directed: bool) -> Graph

Create empty graph.

§Arguments
  • is_directed - Whether the graph is directed
§Examples
use aprender::graph::Graph;

let g = Graph::new(false); // undirected
assert_eq!(g.num_nodes(), 0);
Source

pub fn num_nodes(&self) -> usize

Get number of nodes in graph.

Source

pub fn num_edges(&self) -> usize

Get number of edges in graph.

Source

pub fn is_directed(&self) -> bool

Check if graph is directed.

Source

pub fn neighbors(&self, v: usize) -> &[usize]

Get neighbors of node v in O(degree(v)) time with perfect cache locality.

§Arguments
  • v - Node ID
§Returns

Slice of neighbor node IDs

§Examples
use aprender::graph::Graph;

let g = Graph::from_edges(&[(0, 1), (1, 2)], false);
assert_eq!(g.neighbors(1), &[0, 2]);
Source

pub fn from_edges(edges: &[(usize, usize)], is_directed: bool) -> Graph

Build graph from edge list.

This is the primary construction method. Automatically detects the number of nodes from the edge list and builds CSR representation.

§Arguments
  • edges - Slice of (source, target) tuples
  • is_directed - Whether the graph is directed
§Examples
use aprender::graph::Graph;

let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], true);
assert_eq!(g.num_nodes(), 3);
assert_eq!(g.num_edges(), 3);
Source

pub fn from_weighted_edges( edges: &[(usize, usize, f64)], is_directed: bool, ) -> Graph

Build weighted graph from edge list with weights.

§Arguments
  • edges - Slice of (source, target, weight) tuples
  • is_directed - Whether the graph is directed
§Examples
use aprender::graph::Graph;

let g = Graph::from_weighted_edges(&[(0, 1, 1.0), (1, 2, 2.5)], false);
assert_eq!(g.num_nodes(), 3);
assert_eq!(g.num_edges(), 2);
Source

pub fn modularity(&self, communities: &[Vec<usize>]) -> f64

Compute modularity of a community partition.

Modularity Q measures the density of edges within communities compared to a random graph. Ranges from -0.5 to 1.0 (higher is better).

Formula: Q = (1/2m) Σ[A_ij - k_i*k_j/2m] δ(c_i, c_j) where:

  • m = total edges
  • A_ij = adjacency matrix
  • k_i = degree of node i
  • δ(c_i, c_j) = 1 if nodes i,j in same community, 0 otherwise
§Arguments
  • communities - Vector of communities, each community is a vector of node IDs
§Returns

Modularity score Q ∈ [-0.5, 1.0]

Source

pub fn louvain(&self) -> Vec<Vec<usize>>

Detect communities using the Louvain algorithm.

The Louvain method is a greedy modularity optimization algorithm that:

  1. Starts with each node in its own community
  2. Iteratively moves nodes to communities that maximize modularity gain
  3. Aggregates the graph by treating communities as super-nodes
  4. Repeats until modularity converges
§Performance
  • Time: O(m·log n) typical, where m = edges, n = nodes
  • Space: O(n + m)
§Returns

Vector of communities, each community is a vector of node IDs

§Examples
use aprender::graph::Graph;

// Two triangles connected by one edge
let g = Graph::from_edges(&[
    (0, 1), (1, 2), (2, 0),  // Triangle 1
    (3, 4), (4, 5), (5, 3),  // Triangle 2
    (2, 3),                   // Connection
], false);

let communities = g.louvain();
assert_eq!(communities.len(), 2);  // Two communities detected
Source§

impl Graph

Source

pub fn density(&self) -> f64

Compute graph density.

Density is the ratio of actual edges to possible edges. For undirected: d = 2m / (n(n-1)) For directed: d = m / (n(n-1))

§Returns

Density in [0, 1] where 1 is a complete graph

§Examples
use aprender::graph::Graph;

// Complete graph K4 has density 1.0
let g = Graph::from_edges(&[(0,1), (0,2), (0,3), (1,2), (1,3), (2,3)], false);
assert!((g.density() - 1.0).abs() < 1e-6);
Source

pub fn diameter(&self) -> Option<usize>

Compute graph diameter (longest shortest path).

Returns None if graph is disconnected.

§Returns

Some(diameter) if connected, None otherwise

§Performance

O(n·(n + m)) - runs BFS from all nodes

§Examples
use aprender::graph::Graph;

// Path graph 0--1--2--3 has diameter 3
let g = Graph::from_edges(&[(0,1), (1,2), (2,3)], false);
assert_eq!(g.diameter(), Some(3));
Source

pub fn clustering_coefficient(&self) -> f64

Compute global clustering coefficient.

Measures the probability that two neighbors of a node are connected. Formula: C = 3 × triangles / triads

§Returns

Clustering coefficient in [0, 1]

§Performance

O(n·d²) where d = average degree

§Examples
use aprender::graph::Graph;

// Triangle has clustering coefficient 1.0
let g = Graph::from_edges(&[(0,1), (1,2), (2,0)], false);
assert!((g.clustering_coefficient() - 1.0).abs() < 1e-6);
Source

pub fn assortativity(&self) -> f64

Compute degree assortativity coefficient.

Measures correlation between degrees of connected nodes. Positive: high-degree nodes connect to high-degree nodes Negative: high-degree nodes connect to low-degree nodes

§Returns

Assortativity coefficient in [-1, 1]

§Performance

O(m) where m = edges

§Examples
use aprender::graph::Graph;

// Star graph has negative assortativity
let g = Graph::from_edges(&[(0,1), (0,2), (0,3)], false);
assert!(g.assortativity() < 0.0);
Source

pub fn shortest_path(&self, source: usize, target: usize) -> Option<Vec<usize>>

Compute shortest path between two nodes using BFS.

Finds the shortest path (minimum number of hops) from source to target using breadth-first search. Works for both directed and undirected graphs.

§Algorithm

Uses BFS with predecessor tracking (Pohl 1971, bidirectional variant).

§Arguments
  • source - Starting node ID
  • target - Destination node ID
§Returns
  • Some(path) - Shortest path as vector of node IDs from source to target
  • None - No path exists between source and target
§Complexity
  • Time: O(n + m) where n = nodes, m = edges
  • Space: O(n) for predecessor tracking
§Examples
use aprender::graph::Graph;

let edges = vec![(0, 1), (1, 2), (2, 3), (0, 3)];
let g = Graph::from_edges(&edges, false);

// Shortest path from 0 to 3
let path = g.shortest_path(0, 3).expect("path from 0 to 3 should exist");
assert_eq!(path.len(), 2); // 0 -> 3 (direct edge)
assert_eq!(path[0], 0);
assert_eq!(path[1], 3);

// Path 0 to 2
let path = g.shortest_path(0, 2).expect("path from 0 to 2 should exist");
assert!(path.len() <= 3); // Either 0->1->2 or 0->3->2
Source§

impl Graph

Source

pub fn dijkstra( &self, source: usize, target: usize, ) -> Option<(Vec<usize>, f64)>

Compute shortest path using Dijkstra’s algorithm for weighted graphs.

Finds the shortest path from source to target using Dijkstra’s algorithm with a binary heap priority queue. Handles both weighted and unweighted graphs.

§Algorithm

Uses Dijkstra’s algorithm (1959) with priority queue for O((n+m) log n) complexity.

§Arguments
  • source - Starting node ID
  • target - Destination node ID
§Returns
  • Some((path, distance)) - Shortest path and total distance
  • None - No path exists
§Panics

Panics if graph contains negative edge weights.

§Complexity
  • Time: O((n + m) log n)
  • Space: O(n) for distance tracking and priority queue
§Examples
use aprender::graph::Graph;

let g = Graph::from_weighted_edges(&[(0, 1, 1.0), (1, 2, 2.0), (0, 2, 5.0)], false);
let (path, dist) = g.dijkstra(0, 2).expect("dijkstra path should exist");
assert_eq!(dist, 3.0); // 0->1->2 is shorter than 0->2
Source

pub fn all_pairs_shortest_paths(&self) -> Vec<Vec<Option<usize>>>

Compute all-pairs shortest paths using repeated BFS.

Computes the shortest path distance between all pairs of nodes. Uses BFS for unweighted graphs (O(nm)) which is faster than Floyd-Warshall (O(n³)) for sparse graphs.

§Algorithm

Runs BFS from each node (Floyd 1962 for weighted, BFS for unweighted).

§Returns

Distance matrix where [i][j] = shortest distance from node i to node j. Returns None if nodes are not connected.

§Complexity
  • Time: O(n·(n + m)) for unweighted graphs
  • Space: O(n²) for distance matrix
§Examples
use aprender::graph::Graph;

let g = Graph::from_edges(&[(0, 1), (1, 2)], false);
let dist = g.all_pairs_shortest_paths();

assert_eq!(dist[0][0], Some(0));
assert_eq!(dist[0][1], Some(1));
assert_eq!(dist[0][2], Some(2));
Source

pub fn a_star<F>( &self, source: usize, target: usize, heuristic: F, ) -> Option<Vec<usize>>
where F: Fn(usize) -> f64,

Compute shortest path using A* search algorithm with heuristic.

Finds the shortest path from source to target using A* algorithm with a user-provided heuristic function. The heuristic must be admissible (never overestimate) for optimality guarantees.

§Algorithm

Uses A* search (Hart et al. 1968) with f(n) = g(n) + h(n) where:

  • g(n) = actual cost from source to n
  • h(n) = estimated cost from n to target (heuristic)
§Arguments
  • source - Starting node ID
  • target - Destination node ID
  • heuristic - Function mapping NodeId to estimated distance to target
§Returns
  • Some(path) - Shortest path as vector of node IDs
  • None - No path exists
§Complexity
  • Time: O((n + m) log n) with admissible heuristic
  • Space: O(n) for tracking
§Examples
use aprender::graph::Graph;

let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false);

// Manhattan distance heuristic (example)
let heuristic = |node: usize| (3 - node) as f64;

let path = g.a_star(0, 3, heuristic).expect("a_star path should exist");
assert_eq!(path, vec![0, 1, 2, 3]);
Source

pub fn dfs(&self, source: usize) -> Option<Vec<usize>>

Depth-First Search (DFS) traversal starting from a given node.

Returns nodes in the order they were visited (pre-order traversal). Only visits nodes reachable from the source node.

§Arguments
  • source - Starting node for traversal
§Returns

Vector of visited nodes in DFS order, or None if source is invalid

§Time Complexity

O(n + m) where n = number of nodes, m = number of edges

§Examples
use aprender::graph::Graph;

let g = Graph::from_edges(&[(0, 1), (1, 2), (0, 3)], false);
let visited = g.dfs(0).expect("valid source");
assert_eq!(visited.len(), 4); // All nodes reachable
assert_eq!(visited[0], 0); // Starts at source
Source§

impl Graph

Source

pub fn connected_components(&self) -> Vec<usize>

Find connected components using Union-Find algorithm.

Returns a vector where each index is a node ID and the value is its component ID. Nodes in the same component have the same component ID.

For directed graphs, this finds weakly connected components (ignores edge direction).

§Returns

Vector mapping each node to its component ID (0-indexed)

§Time Complexity

O(m·α(n)) where α is the inverse Ackermann function (effectively constant)

§Examples
use aprender::graph::Graph;

// Two disconnected components: (0,1) and (2,3)
let g = Graph::from_edges(&[(0, 1), (2, 3)], false);
let components = g.connected_components();

assert_eq!(components[0], components[1]); // Same component
assert_ne!(components[0], components[2]); // Different components
Source

pub fn strongly_connected_components(&self) -> Vec<usize>

Find strongly connected components using Tarjan’s algorithm.

A strongly connected component (SCC) is a maximal set of vertices where every vertex is reachable from every other vertex in the set.

Only meaningful for directed graphs. For undirected graphs, use connected_components().

§Returns

Vector mapping each node to its SCC ID (0-indexed)

§Time Complexity

O(n + m) - single-pass DFS

§Examples
use aprender::graph::Graph;

// Directed cycle: 0 -> 1 -> 2 -> 0
let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], true);
let sccs = g.strongly_connected_components();

// All nodes in same SCC
assert_eq!(sccs[0], sccs[1]);
assert_eq!(sccs[1], sccs[2]);
Source

pub fn topological_sort(&self) -> Option<Vec<usize>>

Topological sort for directed acyclic graphs (DAGs).

Returns a linear ordering of vertices where for every directed edge (u,v), u appears before v in the ordering.

Only valid for DAGs. If the graph contains a cycle, returns None.

§Returns

Some(Vec<NodeId>) with nodes in topological order, or None if graph has cycles

§Time Complexity

O(n + m) - DFS-based approach

§Examples
use aprender::graph::Graph;

// DAG: 0 -> 1 -> 2
let g = Graph::from_edges(&[(0, 1), (1, 2)], true);
let order = g.topological_sort().expect("DAG should have topological order");

// 0 comes before 1, 1 comes before 2
assert!(order.iter().position(|&x| x == 0) < order.iter().position(|&x| x == 1));
assert!(order.iter().position(|&x| x == 1) < order.iter().position(|&x| x == 2));
Source

pub fn common_neighbors(&self, u: usize, v: usize) -> Option<usize>

Count common neighbors between two nodes (link prediction metric).

Returns the number of neighbors shared by both nodes u and v. Used for link prediction: nodes with many common neighbors are more likely to form a connection.

§Arguments
  • u - First node
  • v - Second node
§Returns

Number of common neighbors, or None if either node is invalid

§Time Complexity

O(min(deg(u), deg(v))) - intersection of neighbor sets

§Examples
use aprender::graph::Graph;

// Triangle: 0-1, 0-2, 1-2
let g = Graph::from_edges(&[(0, 1), (0, 2), (1, 2)], false);

// Nodes 1 and 2 share neighbor 0
assert_eq!(g.common_neighbors(1, 2), Some(1));
Source

pub fn adamic_adar_index(&self, u: usize, v: usize) -> Option<f64>

Adamic-Adar index for link prediction between two nodes.

Computes a weighted measure of common neighbors, where neighbors with fewer connections are weighted more heavily. This captures the intuition that sharing a rare neighbor is more significant than sharing a common one.

Formula: AA(u,v) = Σ 1/log(deg(z)) for all common neighbors z

§Arguments
  • u - First node
  • v - Second node
§Returns

Adamic-Adar index score, or None if either node is invalid

§Time Complexity

O(min(deg(u), deg(v))) - intersection of neighbor sets

§Examples
use aprender::graph::Graph;

// Graph with shared neighbors
let g = Graph::from_edges(&[(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)], false);

// Adamic-Adar index for nodes 1 and 2
let aa = g.adamic_adar_index(1, 2).expect("valid nodes");
assert!(aa > 0.0);
Source§

impl Graph

Source

pub fn label_propagation( &self, max_iter: usize, seed: Option<u64>, ) -> Vec<usize>

Label propagation algorithm for community detection.

Iteratively assigns each node the most common label among its neighbors. Nodes with the same final label belong to the same community.

§Arguments
  • max_iter - Maximum number of iterations (default: 100)
  • seed - Random seed for deterministic tie-breaking (optional)
§Returns

Vector mapping each node to its community label (0-indexed)

§Time Complexity

O(max_iter · m) where m = number of edges

§Examples
use aprender::graph::Graph;

// Graph with two communities: (0,1,2) and (3,4,5)
let g = Graph::from_edges(
    &[(0, 1), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3), (2, 3)],
    false
);

let communities = g.label_propagation(100, Some(42));
// Nodes in same community have same label
assert_eq!(communities[0], communities[1]);

Trait Implementations§

Source§

impl Debug for Graph

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>

Formats the value using the given formatter. Read more
Source§

impl GraphCentrality for Graph

Source§

fn degree_centrality(&self) -> HashMap<usize, f64>

Compute degree centrality for all nodes. Read more
Source§

fn pagerank( &self, damping: f64, max_iter: usize, tol: f64, ) -> Result<Vec<f64>, String>

Compute PageRank using power iteration with Kahan summation. Read more
Source§

fn betweenness_centrality(&self) -> Vec<f64>

Compute betweenness centrality using parallel Brandes’ algorithm. Read more
Source§

fn closeness_centrality(&self) -> Vec<f64>

Compute closeness centrality for all nodes. Read more
Source§

fn eigenvector_centrality( &self, max_iter: usize, tol: f64, ) -> Result<Vec<f64>, String>

Compute eigenvector centrality using power iteration. Read more
Source§

fn katz_centrality( &self, alpha: f64, max_iter: usize, tol: f64, ) -> Result<Vec<f64>, String>

Compute Katz centrality with attenuation factor. Read more
Source§

fn harmonic_centrality(&self) -> Vec<f64>

Compute harmonic centrality for all nodes. Read more

Auto Trait Implementations§

§

impl Freeze for Graph

§

impl RefUnwindSafe for Graph

§

impl Send for Graph

§

impl Sync for Graph

§

impl Unpin for Graph

§

impl UnsafeUnpin for Graph

§

impl UnwindSafe for Graph

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> Downcast<T> for T

Source§

fn downcast(&self) -> &T

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> Upcast<T> for T

Source§

fn upcast(&self) -> Option<&T>

Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WasmNotSend for T
where T: Send,

Source§

impl<T> WasmNotSendSync for T

Source§

impl<T> WasmNotSync for T
where T: Sync,