Skip to main content

legume_numeric/matrix/
graph.rs

1//! Shared graph APIs.
2//!
3//! A lightweight `WeightedGraph` trait + a handful of algorithms that
4//! operate generically over anything that implements it. Individual
5//! graph types (kNN graph, gene-pair graph, Leiden network) live in
6//! their own modules/crates and opt in via a single `impl` block.
7
8use std::collections::VecDeque;
9
10/// Undirected weighted graph over contiguous node ids `0..num_nodes()`.
11///
12/// `neighbors_with_weight` yields each edge incident to `node` as
13/// `(other_node, weight)`. The trait is deliberately boxed-iterator to
14/// keep object safety and let implementations pick whatever internal
15/// storage they already have (CSC, adjacency list, petgraph, etc.).
16pub trait WeightedGraph {
17    fn num_nodes(&self) -> usize;
18    fn num_edges(&self) -> usize;
19    fn neighbors_with_weight<'a>(
20        &'a self,
21        node: usize,
22    ) -> Box<dyn Iterator<Item = (usize, f32)> + 'a>;
23
24    fn degree(&self, node: usize) -> usize {
25        self.neighbors_with_weight(node).count()
26    }
27
28    fn weighted_degree(&self, node: usize) -> f32 {
29        self.neighbors_with_weight(node).map(|(_, w)| w).sum()
30    }
31}
32
33/// Generic symmetric adjacency-list graph. Useful as a target when adapting
34/// an edge-list representation (e.g. `GenePairGraph`) to algorithms that
35/// consume `WeightedGraph`.
36pub struct AdjListGraph {
37    adj: Vec<Vec<(usize, f32)>>,
38    num_edges: usize,
39}
40
41impl AdjListGraph {
42    /// Build from a canonical (u ≤ v) edge list. Each edge is expanded to
43    /// both directions internally; self-loops are dropped.
44    pub fn from_edges(n_nodes: usize, edges: &[(usize, usize, f32)]) -> Self {
45        let mut adj = vec![Vec::new(); n_nodes];
46        let mut num_edges = 0usize;
47        for &(u, v, w) in edges {
48            if u == v {
49                continue;
50            }
51            adj[u].push((v, w));
52            adj[v].push((u, w));
53            num_edges += 1;
54        }
55        Self { adj, num_edges }
56    }
57
58    /// Build from unweighted canonical edges; each edge gets weight 1.0.
59    pub fn from_unweighted_edges(n_nodes: usize, edges: &[(usize, usize)]) -> Self {
60        let weighted: Vec<(usize, usize, f32)> = edges.iter().map(|&(u, v)| (u, v, 1.0)).collect();
61        Self::from_edges(n_nodes, &weighted)
62    }
63}
64
65impl WeightedGraph for AdjListGraph {
66    fn num_nodes(&self) -> usize {
67        self.adj.len()
68    }
69
70    fn num_edges(&self) -> usize {
71        self.num_edges
72    }
73
74    fn neighbors_with_weight<'a>(
75        &'a self,
76        node: usize,
77    ) -> Box<dyn Iterator<Item = (usize, f32)> + 'a> {
78        Box::new(self.adj[node].iter().copied())
79    }
80}
81
82/// Per-node component id via BFS. Unreached nodes (isolated) get their own id.
83pub fn connected_components<G: WeightedGraph + ?Sized>(g: &G) -> Vec<usize> {
84    let n = g.num_nodes();
85    let mut label = vec![usize::MAX; n];
86    let mut queue: VecDeque<usize> = VecDeque::new();
87    let mut next_id = 0usize;
88
89    for root in 0..n {
90        if label[root] != usize::MAX {
91            continue;
92        }
93        label[root] = next_id;
94        queue.push_back(root);
95        while let Some(u) = queue.pop_front() {
96            for (v, _) in g.neighbors_with_weight(u) {
97                if label[v] == usize::MAX {
98                    label[v] = next_id;
99                    queue.push_back(v);
100                }
101            }
102        }
103        next_id += 1;
104    }
105    label
106}
107
108/// Count of connected components.
109pub fn num_connected_components<G: WeightedGraph + ?Sized>(g: &G) -> usize {
110    connected_components(g)
111        .into_iter()
112        .max()
113        .map(|m| m + 1)
114        .unwrap_or(0)
115}
116
117/// Weighted degree per node.
118pub fn weighted_degrees<G: WeightedGraph + ?Sized>(g: &G) -> Vec<f32> {
119    (0..g.num_nodes()).map(|i| g.weighted_degree(i)).collect()
120}
121
122/// Sum of all edge weights (each undirected edge counted once). Assumes
123/// `neighbors_with_weight(u)` yields `v > u` edges too (symmetric listing).
124pub fn total_edge_weight<G: WeightedGraph + ?Sized>(g: &G) -> f32 {
125    let mut s = 0f32;
126    for u in 0..g.num_nodes() {
127        for (v, w) in g.neighbors_with_weight(u) {
128            if u < v {
129                s += w;
130            }
131        }
132    }
133    s
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    struct TinyGraph {
141        n: usize,
142        adj: Vec<Vec<(usize, f32)>>,
143    }
144
145    impl WeightedGraph for TinyGraph {
146        fn num_nodes(&self) -> usize {
147            self.n
148        }
149        fn num_edges(&self) -> usize {
150            self.adj.iter().map(|v| v.len()).sum::<usize>() / 2
151        }
152        fn neighbors_with_weight<'a>(
153            &'a self,
154            node: usize,
155        ) -> Box<dyn Iterator<Item = (usize, f32)> + 'a> {
156            Box::new(self.adj[node].iter().copied())
157        }
158    }
159
160    fn two_components() -> TinyGraph {
161        // 0 -- 1,   2 -- 3 -- 4,   5 isolated
162        let mut adj = vec![vec![]; 6];
163        adj[0].push((1, 1.0));
164        adj[1].push((0, 1.0));
165        adj[2].push((3, 2.0));
166        adj[3].push((2, 2.0));
167        adj[3].push((4, 3.0));
168        adj[4].push((3, 3.0));
169        TinyGraph { n: 6, adj }
170    }
171
172    #[test]
173    fn connected_components_identifies_three_groups() {
174        let g = two_components();
175        let label = connected_components(&g);
176        assert_eq!(label[0], label[1]);
177        assert_eq!(label[2], label[3]);
178        assert_eq!(label[3], label[4]);
179        assert_ne!(label[0], label[2]);
180        assert_ne!(label[0], label[5]);
181        assert_ne!(label[2], label[5]);
182        assert_eq!(num_connected_components(&g), 3);
183    }
184
185    #[test]
186    fn weighted_degree_and_total_weight() {
187        let g = two_components();
188        assert_eq!(g.degree(3), 2);
189        assert_eq!(g.weighted_degree(3), 5.0);
190        assert_eq!(weighted_degrees(&g), vec![1.0, 1.0, 2.0, 5.0, 3.0, 0.0]);
191        assert_eq!(total_edge_weight(&g), 6.0);
192    }
193}