Skip to main content

legume_numeric/leiden/
network.rs

1//! Undirected weighted graph used by the Leiden / Louvain clustering algorithms.
2
3use crate::leiden::Clustering;
4use rayon::prelude::{IndexedParallelIterator, ParallelIterator};
5use rayon::slice::ParallelSlice;
6use rustc_hash::FxHashMap as HashMap;
7
8/// Undirected weighted graph backing the Leiden / Louvain algorithms.
9///
10/// Node ids are dense `usize` in `0..nodes()`. Each undirected edge is stored
11/// twice (once per endpoint) so adjacency iteration is O(deg). Internal
12/// adjacency uses `u32` neighbour ids for compactness; callers see only `usize`.
13pub struct Network {
14    /// adj\[u\] holds (neighbour_id, edge_weight) for each undirected edge {u, v}.
15    /// Each undirected edge appears in both adj\[u\] and adj\[v\].
16    adj: Vec<Vec<(u32, f32)>>,
17    /// Node weights, parallel to `adj`.
18    node_weights: Vec<f32>,
19    /// Number of undirected edges (each pair counted once).
20    edge_count: usize,
21}
22
23/// Iterator over `(neighbour_id, edge_weight)` for all neighbours of a chosen node.
24pub struct NeighborAndWeightIter<'a> {
25    iter: std::slice::Iter<'a, (u32, f32)>,
26}
27
28impl Iterator for NeighborAndWeightIter<'_> {
29    type Item = (usize, f64);
30
31    fn next(&mut self) -> Option<Self::Item> {
32        self.iter.next().map(|&(n, w)| (n as usize, f64::from(w)))
33    }
34}
35
36/// Iterator over each undirected edge once, yielding `(source, target, weight)`
37/// with `target >= source`. Iteration order is by `source` ascending; within a
38/// source, in insertion order. Stable across calls.
39pub struct EdgeReferences<'a> {
40    adj: &'a [Vec<(u32, f32)>],
41    src: usize,
42    pos: usize,
43}
44
45impl Iterator for EdgeReferences<'_> {
46    type Item = (usize, usize, f32);
47
48    fn next(&mut self) -> Option<Self::Item> {
49        while self.src < self.adj.len() {
50            let edges = &self.adj[self.src];
51            while self.pos < edges.len() {
52                let (tgt32, w) = edges[self.pos];
53                self.pos += 1;
54                let tgt = tgt32 as usize;
55                if tgt >= self.src {
56                    return Some((self.src, tgt, w));
57                }
58            }
59            self.src += 1;
60            self.pos = 0;
61        }
62        None
63    }
64}
65
66impl Network {
67    /// Create a new empty network.
68    #[must_use]
69    pub fn new() -> Network {
70        Network::with_capacity(0)
71    }
72
73    /// Create a new empty network with capacity for `n_nodes` nodes.
74    #[must_use]
75    pub fn with_capacity(n_nodes: usize) -> Network {
76        Network {
77            adj: Vec::with_capacity(n_nodes),
78            node_weights: Vec::with_capacity(n_nodes),
79            edge_count: 0,
80        }
81    }
82
83    /// Every node at once, with `weight` and room for `degree` neighbours, so
84    /// the edges that follow never grow an adjacency list.
85    #[must_use]
86    pub fn with_nodes(node_weights: &[f32], degrees: &[usize]) -> Network {
87        assert_eq!(
88            node_weights.len(),
89            degrees.len(),
90            "one degree per node weight"
91        );
92        Network {
93            adj: degrees.iter().map(|&d| Vec::with_capacity(d)).collect(),
94            node_weights: node_weights.to_vec(),
95            edge_count: 0,
96        }
97    }
98
99    /// Append a node with `weight`. Returns its node id.
100    pub fn add_node(&mut self, weight: f32) -> usize {
101        let id = self.node_weights.len();
102        self.node_weights.push(weight);
103        self.adj.push(Vec::new());
104        id
105    }
106
107    /// Add an undirected edge between `source` and `target` with `weight`.
108    ///
109    /// # Panics
110    /// If `source` or `target` exceeds `u32::MAX`, or is out of range.
111    pub fn add_edge(&mut self, source: usize, target: usize, weight: f32) {
112        let s32 = u32::try_from(source).expect("node index exceeds u32::MAX");
113        let t32 = u32::try_from(target).expect("node index exceeds u32::MAX");
114        self.adj[source].push((t32, weight));
115        self.adj[target].push((s32, weight));
116        self.edge_count += 1;
117    }
118
119    /// Number of nodes.
120    #[must_use]
121    pub fn nodes(&self) -> usize {
122        self.node_weights.len()
123    }
124
125    /// Number of undirected edges (each pair counted once).
126    #[must_use]
127    pub fn edge_count(&self) -> usize {
128        self.edge_count
129    }
130
131    /// Get the weight of `node` as `f64`.
132    ///
133    /// # Panics
134    /// If `node >= nodes()`.
135    #[must_use]
136    pub fn weight(&self, node: usize) -> f64 {
137        f64::from(self.node_weights[node])
138    }
139
140    /// Mutable access to a node's weight.
141    ///
142    /// # Panics
143    /// If `node >= nodes()`.
144    pub fn node_weight_mut(&mut self, node: usize) -> &mut f32 {
145        &mut self.node_weights[node]
146    }
147
148    /// Iterator over `(neighbour_id, edge_weight)` for all neighbours of `node`.
149    ///
150    /// # Panics
151    /// If `node >= nodes()`.
152    #[must_use]
153    pub fn neighbors(&self, node: usize) -> NeighborAndWeightIter<'_> {
154        NeighborAndWeightIter {
155            iter: self.adj[node].iter(),
156        }
157    }
158
159    /// Iterator over each undirected edge once.
160    pub fn edge_references(&self) -> EdgeReferences<'_> {
161        EdgeReferences {
162            adj: &self.adj,
163            src: 0,
164            pos: 0,
165        }
166    }
167
168    /// Sum of all node weights.
169    #[must_use]
170    pub fn get_total_node_weight(&self) -> f64 {
171        let mut w = 0.0;
172        for i in 0..self.nodes() {
173            w += self.weight(i);
174        }
175        w
176    }
177
178    /// Sum of all edge weights (each undirected edge counted once).
179    #[must_use]
180    pub fn get_total_edge_weight(&self) -> f64 {
181        let mut s = 0.0;
182        for (src, edges) in self.adj.iter().enumerate() {
183            for &(t, w) in edges {
184                if t as usize >= src {
185                    s += f64::from(w);
186                }
187            }
188        }
189        s
190    }
191
192    /// Parallel total edge weight. Sums the double-counted weights then divides by 2.
193    /// Chunked + serially reduced for determinism.
194    #[must_use]
195    pub fn get_total_edge_weight_par(&self) -> f64 {
196        let mut partial_sums = vec![];
197
198        self.adj
199            .par_chunks(256)
200            .map(|chunk| {
201                chunk
202                    .iter()
203                    .map(|edges| edges.iter().fold(0.0, |a, &(_, w)| a + f64::from(w)))
204                    .sum::<f64>()
205            })
206            .collect_into_vec(&mut partial_sums);
207
208        partial_sums.iter().sum::<f64>() / 2.0
209    }
210
211    /// Tabulate the total edge weight of each node into `result`.
212    pub fn get_total_edge_weight_per_node(&self, result: &mut Vec<f64>) {
213        result.clear();
214        for edges in &self.adj {
215            let w = edges.iter().fold(0.0, |a, &(_, w)| a + f64::from(w));
216            result.push(w);
217        }
218    }
219
220    /// Aggregate network where each cluster becomes a single node.
221    ///
222    /// Node weights are summed within each cluster. Edge weights between
223    /// distinct clusters are summed. Within-cluster edges are dropped — only
224    /// the inter-cluster summary is preserved.
225    ///
226    /// # Panics
227    /// If a cluster id exceeds `u32::MAX`.
228    #[must_use]
229    pub fn create_reduced_network(&self, clustering: &impl Clustering) -> Network {
230        let mut cluster_g = Network::with_capacity(clustering.num_clusters());
231
232        for _ in 0..clustering.num_clusters() {
233            cluster_g.add_node(0.0);
234        }
235
236        for n in 0..self.nodes() {
237            let cluster = clustering.get(n);
238            cluster_g.node_weights[cluster] += self.node_weights[n];
239        }
240
241        let mut edge_memo: HashMap<(u32, u32), f32> = HashMap::default();
242
243        for (src, tgt, w) in self.edge_references() {
244            let c1 = u32::try_from(clustering.get(src)).expect("cluster id exceeds u32::MAX");
245            let c2 = u32::try_from(clustering.get(tgt)).expect("cluster id exceeds u32::MAX");
246
247            if c1 == c2 {
248                continue;
249            }
250
251            let (mn, mx) = if c1 < c2 { (c1, c2) } else { (c2, c1) };
252            *edge_memo.entry((mn, mx)).or_insert(0.0) += w;
253        }
254
255        for (&(c1, c2), &weight) in &edge_memo {
256            cluster_g.add_edge(c1 as usize, c2 as usize, weight);
257        }
258
259        cluster_g
260    }
261
262    /// One subnetwork per cluster, containing only intra-cluster edges.
263    ///
264    /// # Panics
265    /// If a node index exceeds `u32::MAX`.
266    pub fn create_subnetworks(&self, c: &impl Clustering) -> Vec<Network> {
267        let mut graphs: Vec<Network> = (0..c.num_clusters())
268            .map(|_| Network::with_capacity(0))
269            .collect();
270        let mut new_id_map = Vec::with_capacity(c.nodes());
271        let mut counts = vec![0usize; c.num_clusters()];
272
273        for i in 0..self.nodes() {
274            let cluster = c.get(i);
275            let new_id = counts[cluster];
276            new_id_map.push(new_id);
277            counts[cluster] += 1;
278            graphs[cluster].add_node(self.node_weights[i]);
279        }
280
281        for (n1, n2, w) in self.edge_references() {
282            let c1 = c.get(n1);
283            let c2 = c.get(n2);
284            if c1 == c2 {
285                graphs[c1].add_edge(new_id_map[n1], new_id_map[n2], w);
286            }
287        }
288
289        graphs
290    }
291}
292
293impl Default for Network {
294    fn default() -> Self {
295        Self::new()
296    }
297}