Skip to main content

bixverse_methods/graph/
graph_structures.rs

1//! Structures for graphs within bixverse.
2
3#![allow(dead_code)]
4
5use faer::{Mat, MatRef};
6use rayon::prelude::*;
7use rustc_hash::FxHashMap;
8use std::clone::Clone;
9use std::collections::BinaryHeap;
10
11use crate::prelude::*;
12use bixverse_core::core::math::sparse::{coo_to_csr, thread_chunks, validate_square_csr};
13use bixverse_sparse::serialise::{decode_with_header, elem_width, encode_with_header};
14use serde::{Deserialize, Serialize, de::DeserializeOwned};
15
16////////////
17// Consts //
18////////////
19
20/// Rows of the similarity matrix processed per parallel task in the kNN build
21const KNN_ROW_BLOCK: usize = 32;
22
23/// Tile edge for the symmetrisation pass
24const KNN_SYM_TILE: usize = 64;
25
26/// Magic bytes of a serialised [`SparseGraph`].
27const GRAPH_MAGIC: &[u8; 8] = b"BXSPGRPH";
28
29////////////////
30// Union-find //
31////////////////
32
33/// Disjoint-set forest with path halving and union by size.
34pub struct UnionFind {
35    /// Parent pointer per element; a root points at itself.
36    parent: Vec<u32>,
37    /// Size of the tree rooted at each element. Only meaningful at roots.
38    size: Vec<u32>,
39}
40
41impl UnionFind {
42    /// Create `n` singleton sets.
43    ///
44    /// ### Params
45    ///
46    /// * `n` - Number of elements.
47    ///
48    /// ### Returns
49    ///
50    /// The initialised forest.
51    pub fn new(n: usize) -> Self {
52        Self {
53            parent: (0..n as u32).collect(),
54            size: vec![1; n],
55        }
56    }
57
58    /// Find the root of `x`, halving the path on the way up.
59    ///
60    /// ### Params
61    ///
62    /// * `x` - Element to look up.
63    ///
64    /// ### Returns
65    ///
66    /// The root of `x`'s set.
67    pub fn find(&mut self, mut x: u32) -> u32 {
68        while self.parent[x as usize] != x {
69            let grandparent = self.parent[self.parent[x as usize] as usize];
70            self.parent[x as usize] = grandparent;
71            x = grandparent;
72        }
73        x
74    }
75
76    /// Merge the sets containing `a` and `b`, smaller tree under larger.
77    ///
78    /// ### Params
79    ///
80    /// * `a` - First element.
81    /// * `b` - Second element.
82    ///
83    /// ### Returns
84    ///
85    /// `true` when the two were in different sets and have just been merged,
86    /// `false` when they already shared a root. Kruskal needs that answer to
87    /// decide whether an edge joins two components or closes a cycle.
88    pub fn union(&mut self, a: u32, b: u32) -> bool {
89        let (mut ra, mut rb) = (self.find(a), self.find(b));
90        if ra == rb {
91            return false;
92        }
93        if self.size[ra as usize] < self.size[rb as usize] {
94            std::mem::swap(&mut ra, &mut rb);
95        }
96        self.parent[rb as usize] = ra;
97        self.size[ra as usize] += self.size[rb as usize];
98        true
99    }
100}
101
102/////////////////////////////
103// Sparse graph structures //
104/////////////////////////////
105
106/// Structure representation of a sparse graph
107///
108/// ### Fields
109///
110/// * `adjacency` - Sparse CSR representation of the graph with `f16` as
111///   weights.
112/// * `num_nodes` - Number of nodes represented in the graph.
113/// * `directed` - Whether the graph is directed or not.
114#[derive(Clone, Debug, Serialize, Deserialize)]
115pub struct SparseGraph<T: Clone> {
116    /// Adjacency matrix in CSR (symmetric for undirected graphs)
117    adjacency: CompressedSparseData2<T>,
118    /// Number of nodes, equal to both sides of the adjacency's shape
119    num_nodes: usize,
120    /// Whether the graph is directed
121    directed: bool,
122}
123
124impl<T> SparseGraph<T>
125where
126    T: Clone + BixverseFloat + std::iter::Sum,
127{
128    /// Generate a new SparseGraph
129    ///
130    /// ### Params
131    ///
132    /// * `num_nodes` - Number of nodes in the graph
133    /// * `adjacency` - CompressedSparseData2 containing the adjacency
134    ///   information
135    /// * `directed` - Is this a directed or undirected graph
136    ///
137    /// ### Returns
138    ///
139    /// Initialised self
140    pub fn new(num_nodes: usize, adjacency: CompressedSparseData2<T>, directed: bool) -> Self {
141        Self {
142            adjacency,
143            num_nodes,
144            directed,
145        }
146    }
147
148    /// Helper function to get the neighbours and weights
149    ///
150    /// ### Params
151    ///
152    /// * `node` - Index of the node for which to get the neighbours
153    ///
154    /// ### Return
155    ///
156    /// Tuple of `(neighbour_indices, edge_weights)`
157    #[inline]
158    pub fn get_neighbours(&self, node: usize) -> (Vec<usize>, &[T]) {
159        let start = self.adjacency.indptr[node] as usize;
160        let end = self.adjacency.indptr[node + 1] as usize;
161        (
162            self.adjacency.indices[start..end].index_cast(),
163            &self.adjacency.data[start..end],
164        )
165    }
166
167    /// Get the neighbours and weights without allocating
168    ///
169    /// ### Params
170    ///
171    /// * `node` - Index of the node for which to get the neighbours
172    ///
173    /// ### Return
174    ///
175    /// Tuple of `(neighbour_indices, edge_weights)`, borrowed from the CSR
176    #[inline]
177    pub fn get_neighbours_raw(&self, node: usize) -> (&[u32], &[T]) {
178        let start = self.adjacency.indptr[node] as usize;
179        let end = self.adjacency.indptr[node + 1] as usize;
180        (
181            &self.adjacency.indices[start..end],
182            &self.adjacency.data[start..end],
183        )
184    }
185
186    /// Get the node degree
187    ///
188    /// ### Params
189    ///
190    /// * `node` - Index of the node to get the node degree from
191    ///
192    /// ### Return
193    ///
194    /// The node degree for this node.
195    #[inline]
196    pub fn get_node_degree(&self, node: usize) -> usize {
197        (self.adjacency.indptr[node + 1] - self.adjacency.indptr[node]) as usize
198    }
199
200    /// Get total weight
201    ///
202    /// ### Params
203    ///
204    /// * `undirected` - Is the graph undirected or not.
205    ///
206    /// ### Returns
207    ///
208    /// The total edge weight
209    pub fn total_weight(&self) -> T {
210        let mut total = T::zero();
211        for i in 0..self.num_nodes {
212            let (_, weights) = self.get_neighbours(i);
213            total += weights.iter().copied().sum::<T>();
214        }
215        let half = T::from_f64(0.5).unwrap();
216        if !self.directed { total * half } else { total }
217    }
218
219    /// Expose the number of nodes
220    ///
221    /// ### Returns
222    ///
223    /// The number of nodes in the graph
224    pub fn get_node_number(&self) -> usize {
225        self.num_nodes
226    }
227
228    /// Expose if graph is directed
229    ///
230    /// ### Returns
231    ///
232    /// Boolean indicating if graph is directed
233    pub fn is_directed(&self) -> bool {
234        self.directed
235    }
236
237    /// Number of edges
238    ///
239    /// An undirected graph stores every edge twice, a self-loop included (both
240    /// entries in its own row, as [`edge_pairs_to_sparse_graph`] builds it).
241    ///
242    /// ### Returns
243    ///
244    /// The number of stored entries for a directed graph, half of them for an
245    /// undirected one.
246    pub fn get_edge_number(&self) -> usize {
247        let nnz = self.adjacency.indices.len();
248        if self.directed { nnz } else { nnz / 2 }
249    }
250
251    /// Flatten the graph into an edge-pair list
252    ///
253    /// The inverse of [`edge_pairs_to_sparse_graph`]: an undirected edge comes
254    /// out once, as `(i, j)` with `i < j`, and a self-loop once for each pair
255    /// of entries it holds. A directed graph gives every stored entry. Rows
256    /// are flattened in parallel, one chunk per worker, so the order is by
257    /// source node, then by the CSR order within the row.
258    ///
259    /// ### Returns
260    ///
261    /// Tuple of `(edges, weights)`: `edges` holds the pairs flat,
262    /// `[u, v, u, v, ...]`, 0-based; `weights` one weight per pair.
263    pub fn to_edge_pairs(&self) -> (Vec<u32>, Vec<T>)
264    where
265        T: Send + Sync,
266    {
267        let directed = self.directed;
268        let parts: Vec<(Vec<u32>, Vec<T>)> = thread_chunks(self.num_nodes)
269            .into_par_iter()
270            .map(|(start, end)| {
271                let mut edges = Vec::new();
272                let mut weights = Vec::new();
273                for i in start..end {
274                    let (nbrs, ws) = self.get_neighbours_raw(i);
275                    // toggles on every self-loop entry, so each pair emits once
276                    let mut odd_loop = false;
277                    for (&j, &w) in nbrs.iter().zip(ws) {
278                        let j_us = j as usize;
279                        if j_us == i {
280                            odd_loop = !odd_loop;
281                        }
282                        if directed || j_us > i || (j_us == i && odd_loop) {
283                            edges.push(i as u32);
284                            edges.push(j);
285                            weights.push(w);
286                        }
287                    }
288                }
289                (edges, weights)
290            })
291            .collect();
292
293        let n_pairs = parts.iter().map(|(_, w)| w.len()).sum();
294        let mut edges = Vec::with_capacity(2 * n_pairs);
295        let mut weights = Vec::with_capacity(n_pairs);
296        for (e, w) in parts {
297            edges.extend_from_slice(&e);
298            weights.extend_from_slice(&w);
299        }
300        (edges, weights)
301    }
302}
303
304impl<T> SparseGraph<T>
305where
306    T: Clone + BixverseFloat + std::iter::Sum + Serialize + DeserializeOwned,
307{
308    /// Serialise the graph to bytes
309    ///
310    /// ### Returns
311    ///
312    /// A blob that [`Self::from_bytes`] reads back. See
313    /// [`bixverse_sparse::serialise`] for the layout.
314    pub fn to_bytes(&self) -> Result<Vec<u8>, MethodsErrors> {
315        let w = elem_width::<T>();
316        encode_with_header(GRAPH_MAGIC, [w, w], self)
317            .map_err(|e| MethodsErrors::Core(CoreErrors::from(e)))
318    }
319
320    /// Read a graph written by [`Self::to_bytes`]
321    ///
322    /// The adjacency is checked for being a structurally sound square CSR
323    /// over `num_nodes` nodes, since the bytes may come from disk.
324    ///
325    /// ### Params
326    ///
327    /// * `bytes` - The blob
328    ///
329    /// ### Returns
330    ///
331    /// The graph, or a header, decode or validation error.
332    pub fn from_bytes(bytes: &[u8]) -> Result<Self, MethodsErrors> {
333        let w = elem_width::<T>();
334        let graph: Self = decode_with_header(GRAPH_MAGIC, [w, w], "SparseGraph", bytes)
335            .map_err(CoreErrors::from)?;
336        let n = validate_square_csr(&graph.adjacency).map_err(CoreErrors::from)?;
337        if n != graph.num_nodes {
338            return Err(CoreErrors::from(SparseErrors::ShapeMismatch {
339                expected: (graph.num_nodes, graph.num_nodes),
340                got: graph.adjacency.shape,
341            })
342            .into());
343        }
344        Ok(graph)
345    }
346}
347
348//////////////////////////////////
349// Hierachical graph structures //
350//////////////////////////////////
351
352/// Represents one level in the hierarchy
353///
354/// ### Fields
355///
356/// * `graph` - The sparse graph
357/// * `node_map` - Maps fine node -> coarse node
358/// * `coarse_weights` - Number of fine nodes that mapped to each coarse node
359#[allow(dead_code)]
360pub struct CoarseLevel<T>
361where
362    T: Clone,
363{
364    graph: SparseGraph<T>,
365    node_map: Vec<usize>,
366    coarse_weights: Vec<usize>,
367}
368
369/// Multi-level graph hierarchy
370///
371/// ### Fields
372///
373/// * `finest` - Original finest level graph
374/// * `levels` - Coarse levels
375pub struct GraphHierarchy<T>
376where
377    T: Clone,
378{
379    finest: SparseGraph<T>,
380    levels: Vec<CoarseLevel<T>>,
381}
382
383impl<T> GraphHierarchy<T>
384where
385    T: Clone + BixverseFloat + std::iter::Sum + std::default::Default,
386{
387    /// Build a Graph hierarchy based on an initial graph
388    ///
389    /// This one assumes undirected graphs!!!
390    ///
391    /// ### Params
392    ///
393    /// * `graph` - The initial SparseGraph
394    /// * `reduction_ratio` - By how much to reduce per level the number of
395    ///   nodes. Typically 0.5.
396    /// * `max_levels` - Maximum depth in terms of coarsion.
397    ///
398    /// ### Returns
399    ///
400    /// Initialised self.
401    pub fn build(graph: SparseGraph<T>, reduction_ratio: T, max_levels: usize) -> Self {
402        assert!(
403            !graph.is_directed(),
404            "This is implemented for undirected graphs!"
405        );
406        let mut levels = Vec::new();
407        let mut current_graph = graph.clone();
408
409        let min_nodes = T::from_usize(100).unwrap();
410
411        for _ in 0..max_levels {
412            let coarse_level = Self::coarsen_level(&current_graph);
413
414            let current_nodes = T::from_usize(current_graph.num_nodes).unwrap();
415            let coarse_nodes = T::from_usize(coarse_level.graph.num_nodes).unwrap();
416            let reduction = coarse_nodes / current_nodes;
417
418            if reduction > reduction_ratio || coarse_nodes < min_nodes {
419                break;
420            }
421
422            current_graph = coarse_level.graph.clone();
423            levels.push(coarse_level);
424        }
425
426        Self {
427            finest: graph,
428            levels,
429        }
430    }
431
432    /// Coarsen one level using heavy-edge matching
433    ///
434    /// Generates a CoarseLevel of a given graph via heavy edge matching.
435    ///
436    /// ### Params
437    ///
438    /// * `graph` - The SparseGraph to coarsen
439    ///
440    /// ### Returns
441    ///
442    /// CoarseLevel containing the coarsened graph and mapping information
443    fn coarsen_level(graph: &SparseGraph<T>) -> CoarseLevel<T> {
444        let matching = Self::heavy_edge_matching(graph);
445        let (coarse_graph, node_map, coarse_weights) = Self::contract_graph(graph, &matching);
446
447        CoarseLevel {
448            graph: coarse_graph,
449            node_map,
450            coarse_weights,
451        }
452    }
453
454    /// Match nodes via heavy edges
455    ///
456    /// Finds pairs of nodes to match based on heaviest edge weights. Each node
457    /// is matched with its heaviest unmatched neighbour.
458    ///
459    /// ### Params
460    ///
461    /// * `graph` - SparseGraph for which to identify the matching nodes
462    ///
463    /// ### Returns
464    ///
465    /// Vector where each element is either None (unmatched) or Some(node_id)
466    /// indicating which node this node is matched with
467    fn heavy_edge_matching(graph: &SparseGraph<T>) -> Vec<Option<usize>> {
468        let n = graph.num_nodes;
469        let mut matching = vec![None; n];
470        let mut matched = vec![false; n];
471
472        for i in 0..n {
473            if matched[i] {
474                continue;
475            }
476
477            let (neighbour, weights) = graph.get_neighbours(i);
478
479            // Find heaviest unmatched neighbour
480            let mut best_neighbour = None;
481            let mut best_weight = T::zero();
482
483            for (&j, &w) in neighbour.iter().zip(weights.iter()) {
484                if !matched[j] && j != i && w > best_weight {
485                    best_weight = w;
486                    best_neighbour = Some(j)
487                }
488            }
489
490            if let Some(j) = best_neighbour {
491                matching[i] = Some(j);
492                matching[j] = Some(i);
493                matched[i] = true;
494                matched[j] = true;
495            }
496        }
497
498        matching
499    }
500
501    /// Contract graph based on matching
502    ///
503    /// Creates a coarser graph by merging matched nodes. Edges between matched
504    /// nodes are removed, and edges to external nodes are combined.
505    ///
506    /// ### Params
507    ///
508    /// * `graph` - The fine-level graph to contract
509    /// * `matching` - Node matching information from heavy_edge_matching
510    ///
511    /// ### Returns
512    ///
513    /// Tuple of (coarse_graph, node_map, coarse_weights) where node_map
514    /// indicates which coarse node each fine node maps to, and coarse_weights
515    /// counts how many fine nodes were merged into each coarse node
516    fn contract_graph(
517        graph: &SparseGraph<T>,
518        matching: &[Option<usize>],
519    ) -> (SparseGraph<T>, Vec<usize>, Vec<usize>) {
520        let n = graph.num_nodes;
521        let mut node_map = vec![0; n];
522        let mut coarse_id = 0;
523
524        // assign coarse node IDs
525        for i in 0..n {
526            if node_map[i] == 0 || i == 0 {
527                match matching[i] {
528                    Some(j) if j > i => {
529                        node_map[i] = coarse_id;
530                        node_map[j] = coarse_id;
531                        coarse_id += 1;
532                    }
533                    None => {
534                        // i unchanged
535                        node_map[i] = coarse_id;
536                        coarse_id += 1;
537                    }
538                    _ => {} // already assigned
539                }
540            }
541        }
542
543        let num_coarse = coarse_id;
544        let mut coarse_weights = vec![0; num_coarse];
545
546        for i in 0..n {
547            coarse_weights[node_map[i]] += 1;
548        }
549
550        // Build coarse graph edges
551        let mut rows = Vec::new();
552        let mut cols = Vec::new();
553        let mut vals = Vec::new();
554
555        for i in 0..n {
556            let ci = node_map[i];
557            let (neighbours, weights) = graph.get_neighbours(i);
558
559            for (&j, &w) in neighbours.iter().zip(weights.iter()) {
560                let cj = node_map[j];
561                if ci != cj {
562                    rows.push(ci);
563                    cols.push(cj);
564                    vals.push(w);
565                }
566            }
567        }
568
569        // Sort to group duplicates
570        let mut edges: Vec<(usize, usize, T)> = rows
571            .into_iter()
572            .zip(cols)
573            .zip(vals)
574            .map(|((r, c), v)| (r, c, v))
575            .collect();
576        edges.sort_unstable_by_key(|(r, c, _)| (*r, *c));
577
578        // Sum duplicates
579        let mut deduped_rows = Vec::new();
580        let mut deduped_cols = Vec::new();
581        let mut deduped_vals = Vec::new();
582
583        if !edges.is_empty() {
584            let (mut curr_r, mut curr_c, mut curr_v) = edges[0];
585
586            for &(r, c, v) in &edges[1..] {
587                if r == curr_r && c == curr_c {
588                    curr_v += v;
589                } else {
590                    deduped_rows.push(curr_r);
591                    deduped_cols.push(curr_c);
592                    deduped_vals.push(curr_v);
593                    (curr_r, curr_c, curr_v) = (r, c, v);
594                }
595            }
596            deduped_rows.push(curr_r);
597            deduped_cols.push(curr_c);
598            deduped_vals.push(curr_v);
599        }
600
601        let coarse_csr = coo_to_csr(
602            &deduped_rows.index_cast(),
603            &deduped_cols.index_cast(),
604            &deduped_vals,
605            (num_coarse, num_coarse),
606        );
607
608        let coarse_graph = SparseGraph::new(num_coarse, coarse_csr, false);
609
610        (coarse_graph, node_map, coarse_weights)
611    }
612
613    /// Project coarse solution to fine level
614    ///
615    /// ### Params
616    ///
617    /// * `level` - Which coarse level (0 = first coarsening)
618    /// * `coarse_labels` - Labels at coarse level
619    ///
620    /// ### Returns
621    ///
622    /// Vector of labels at the fine level
623    pub fn prolong(&self, level: usize, coarse_labels: &[usize]) -> Vec<usize> {
624        if level >= self.levels.len() {
625            return coarse_labels.to_vec();
626        }
627
628        let node_map = &self.levels[level].node_map;
629        node_map.iter().map(|&ci| coarse_labels[ci]).collect()
630    }
631
632    /// Get coarsest graph
633    ///
634    /// ### Returns
635    ///
636    /// Reference to the coarsest graph in the hierarchy
637    pub fn coarsest(&self) -> &SparseGraph<T> {
638        self.levels.last().map(|l| &l.graph).unwrap_or(&self.finest)
639    }
640}
641
642/////////////////////////
643// Heterogenous graphs //
644/////////////////////////
645
646/// Directed edge list with a type per node and a type and weight per edge,
647/// for constrained personalised PageRank.
648#[derive(Debug, Clone)]
649pub struct AttributedGraph<'a, T> {
650    /// Type per node
651    pub node_types: Vec<&'a str>,
652    /// 0-based source node per edge
653    pub from: Vec<usize>,
654    /// 0-based target node per edge
655    pub to: Vec<usize>,
656    /// Type per edge
657    pub edge_types: Vec<&'a str>,
658    /// Weight per edge
659    pub weights: Vec<T>,
660}
661
662/// Generate a weighted, typed graph from node and edge lists
663///
664/// ### Params
665///
666/// * `nodes` - Slice of node names.
667/// * `node_types` - Slice of the node types.
668/// * `from` - Slice of the names of the from nodes.
669/// * `to` - Slice of the names of the to nodes.
670/// * `edge_types` - Slice of the edge types.
671/// * `edge_weights` - Slice of the edge weights.
672///
673/// ### Returns
674///
675/// An `AttributedGraph` for subsequent usage in constrained personalised
676/// page-rank iteration.
677///
678/// ### Panics
679///
680/// Function will panic if `nodes` and `nodes_types` do not have the same
681/// length, when `from`, `to`, `edge_types` and `edge_weights` do not have the
682/// same length, or when an edge names a node that is not in `nodes`.
683pub fn graph_from_strings_with_attributes<'a, T>(
684    nodes: &'a [String],
685    node_types: &'a [String],
686    from: &'a [String],
687    to: &'a [String],
688    edge_types: &'a [String],
689    edge_weights: &'a [T],
690) -> AttributedGraph<'a, T>
691where
692    T: BixverseFloat,
693{
694    assert_same_len!(nodes, node_types);
695    assert_same_len!(from, to, edge_types, edge_weights);
696
697    let name_to_idx: FxHashMap<&String, usize> = nodes
698        .iter()
699        .enumerate()
700        .map(|(idx, name)| (name, idx))
701        .collect();
702    let lookup = |name: &String| {
703        *name_to_idx
704            .get(name)
705            .unwrap_or_else(|| panic!("Node '{name}' not found in nodes list"))
706    };
707
708    AttributedGraph {
709        node_types: node_types.iter().map(String::as_str).collect(),
710        from: from.iter().map(lookup).collect(),
711        to: to.iter().map(lookup).collect(),
712        edge_types: edge_types.iter().map(String::as_str).collect(),
713        weights: edge_weights.to_vec(),
714    }
715}
716
717///////////////////////////////
718// Structure transformations //
719///////////////////////////////
720
721/// Build a CSR adjacency from an edge list via counting sort.
722///
723/// Duplicate `(row, col)` pairs are kept as separate entries, in input order.
724///
725/// ### Params
726///
727/// * `n` - Number of rows (nodes)
728/// * `rows` - Row index per edge
729/// * `cols` - Column index per edge
730/// * `vals` - Value per edge
731///
732/// ### Returns
733///
734/// Tuple of `(indptr, indices, values)`.
735pub(crate) fn edges_to_csr<T>(
736    n: usize,
737    rows: &[u32],
738    cols: &[u32],
739    vals: &[T],
740) -> (Vec<usize>, Vec<u32>, Vec<T>)
741where
742    T: BixverseFloat,
743{
744    let mut indptr = vec![0usize; n + 1];
745    for &r in rows {
746        indptr[r as usize + 1] += 1;
747    }
748    for i in 0..n {
749        indptr[i + 1] += indptr[i];
750    }
751
752    let mut next = indptr[..n].to_vec();
753    let mut indices = vec![0u32; rows.len()];
754    let mut values = vec![T::zero(); rows.len()];
755    for ((&r, &c), &v) in rows.iter().zip(cols).zip(vals) {
756        let pos = next[r as usize];
757        indices[pos] = c;
758        values[pos] = v;
759        next[r as usize] += 1;
760    }
761
762    (indptr, indices, values)
763}
764
765/// Generate a KNN graph adjacency matrix from a similarity matrix
766///
767/// ### Params
768///
769/// * `similarities` - The symmetric similarity matrix.
770/// * `k` - Number of neighbours to take
771///
772/// ### Returns
773///
774/// The KNN adjacency matrix
775pub fn get_knn_graph_adj<T>(similarities: &MatRef<T>, k: usize) -> Mat<T>
776where
777    T: BixverseFloat,
778{
779    assert_symmetric_mat!(similarities);
780    let n = similarities.nrows();
781    let mut adjacency: Mat<T> = Mat::zeros(n, n);
782
783    // Row blocks, walking each column once per block so the reads are
784    // contiguous in the column-major matrix.
785    let blocks: Vec<usize> = (0..n).step_by(KNN_ROW_BLOCK).collect();
786    let rows: Vec<Vec<(usize, T)>> = blocks
787        .par_iter()
788        .flat_map_iter(|&start| {
789            let end = (start + KNN_ROW_BLOCK).min(n);
790            let mut heaps: Vec<BinaryHeap<(RevOrderedFloat<T>, usize)>> = (start..end)
791                .map(|_| BinaryHeap::with_capacity(k + 1))
792                .collect();
793
794            for j in 0..n {
795                let col = similarities.col(j);
796                for (heap, i) in heaps.iter_mut().zip(start..end) {
797                    if i == j {
798                        continue;
799                    }
800                    let entry = (RevOrderedFloat(col[i]), j);
801                    // The entry that would be pushed and immediately popped
802                    if heap.len() == k && heap.peek().is_some_and(|top| entry >= *top) {
803                        continue;
804                    }
805                    heap.push(entry);
806                    if heap.len() > k {
807                        heap.pop();
808                    }
809                }
810            }
811
812            heaps.into_iter().map(|heap| {
813                heap.into_iter()
814                    .map(|(ordered_sim, index)| (index, ordered_sim.0))
815                    .collect::<Vec<_>>()
816            })
817        })
818        .collect();
819
820    // Fill adjacency matrix
821    for (i, neighbors) in rows.iter().enumerate() {
822        for &(j, sim) in neighbors {
823            adjacency[(i, j)] = sim;
824        }
825    }
826
827    // Symmetrise tile by tile to avoid strided walks
828    let two = T::from_f64(2.0).unwrap();
829    for ib in (0..n).step_by(KNN_SYM_TILE) {
830        for jb in (ib..n).step_by(KNN_SYM_TILE) {
831            for j in jb..(jb + KNN_SYM_TILE).min(n) {
832                for i in ib..(ib + KNN_SYM_TILE).min(n).min(j) {
833                    let val = (adjacency[(i, j)] + adjacency[(j, i)]) / two;
834                    adjacency[(i, j)] = val;
835                    adjacency[(j, i)] = val;
836                }
837            }
838        }
839    }
840
841    adjacency
842}
843
844/// Generate a Laplacian matrix from an adjacency matrix
845///
846/// ### Params
847///
848/// * `adjacency` - The symmetric adjacency matrix.
849///
850/// ### Returns
851///
852/// The Laplacian matrix
853pub fn adjacency_to_laplacian<T>(adjacency: &MatRef<T>, normalise: bool) -> Mat<T>
854where
855    T: BixverseFloat,
856{
857    assert_symmetric_mat!(adjacency);
858    let n = adjacency.nrows();
859
860    // Column-wise accumulation, in the same order as a row sum
861    let mut degrees = vec![T::zero(); n];
862    for j in 0..n {
863        for (d, &x) in degrees.iter_mut().zip(adjacency.col(j).iter()) {
864            *d += x;
865        }
866    }
867
868    if !normalise {
869        let mut laplacian = adjacency.cloned();
870        for j in 0..n {
871            for i in 0..n {
872                if i != j {
873                    laplacian[(i, j)] = -adjacency[(i, j)];
874                }
875            }
876            laplacian[(j, j)] = degrees[j] - adjacency[(j, j)];
877        }
878        laplacian
879    } else {
880        let inv_sqrt_d: Vec<T> = degrees
881            .iter()
882            .map(|&d| {
883                if d > T::from_f64(1e-10).unwrap() {
884                    T::one() / d.sqrt()
885                } else {
886                    T::zero()
887                }
888            })
889            .collect();
890        let mut laplacian = Mat::zeros(n, n);
891        for j in 0..n {
892            laplacian[(j, j)] = T::one();
893            for i in 0..n {
894                laplacian[(i, j)] -= inv_sqrt_d[i] * adjacency[(i, j)] * inv_sqrt_d[j];
895            }
896        }
897        laplacian
898    }
899}
900
901/// Build an undirected SparseGraph from a flat edge-pair list.
902///
903/// Counting-sort CSR build, then a parallel per-row sort of the column
904/// indices. Duplicate edges are kept as separate entries (algorithms that scan
905/// rows sum them), and a self-loop lands twice in its row, as in igraph.
906///
907/// ### Params
908///
909/// * `edges` - Flat edge list, pairs `[u, v, u, v, ...]`, 0-based
910/// * `weights` - Edge weights, one per pair
911/// * `n_nodes` - Number of nodes in the graph
912///
913/// ### Returns
914///
915/// Undirected graph with a symmetric, row-sorted CSR
916pub fn edge_pairs_to_sparse_graph<T>(
917    edges: &[usize],
918    weights: &[T],
919    n_nodes: usize,
920) -> SparseGraph<T>
921where
922    T: BixverseFloat + std::iter::Sum + Send + Sync,
923{
924    let n_edges = weights.len();
925
926    let mut indptr = vec![0usize; n_nodes + 1];
927    for e in 0..n_edges {
928        indptr[edges[2 * e] + 1] += 1;
929        indptr[edges[2 * e + 1] + 1] += 1;
930    }
931    for i in 1..=n_nodes {
932        indptr[i] += indptr[i - 1];
933    }
934
935    let nnz = indptr[n_nodes];
936    let mut indices = vec![0usize; nnz];
937    let mut data = vec![T::zero(); nnz];
938    let mut cursor = indptr.clone();
939
940    for e in 0..n_edges {
941        let i = edges[2 * e];
942        let j = edges[2 * e + 1];
943        let w = weights[e];
944
945        indices[cursor[i]] = j;
946        data[cursor[i]] = w;
947        cursor[i] += 1;
948
949        indices[cursor[j]] = i;
950        data[cursor[j]] = w;
951        cursor[j] += 1;
952    }
953
954    // sort column indices within each row, keeping data aligned
955    let mut rows: Vec<(&mut [usize], &mut [T])> = Vec::with_capacity(n_nodes);
956    let (mut idx_rest, mut data_rest) = (indices.as_mut_slice(), data.as_mut_slice());
957    for node in 0..n_nodes {
958        let len = indptr[node + 1] - indptr[node];
959        let (idx_row, idx_tail) = idx_rest.split_at_mut(len);
960        let (data_row, data_tail) = data_rest.split_at_mut(len);
961        rows.push((idx_row, data_row));
962        idx_rest = idx_tail;
963        data_rest = data_tail;
964    }
965
966    rows.par_iter_mut()
967        .for_each_init(Vec::<(usize, T)>::new, |scratch, (idx_row, data_row)| {
968            if idx_row.len() < 2 {
969                return;
970            }
971            scratch.clear();
972            scratch.extend(idx_row.iter().copied().zip(data_row.iter().copied()));
973            scratch.sort_unstable_by_key(|&(node, _)| node);
974            for ((i, d), &(node, w)) in idx_row.iter_mut().zip(data_row.iter_mut()).zip(&*scratch) {
975                *i = node;
976                *d = w;
977            }
978        });
979
980    let adjacency = CompressedSparseData2 {
981        data,
982        indices: indices.index_cast(),
983        indptr: indptr.index_cast(),
984        cs_type: CompressedSparseFormat::Csr,
985        data_2: None,
986        shape: (n_nodes, n_nodes),
987    };
988
989    SparseGraph::new(n_nodes, adjacency, false)
990}
991
992///////////
993// Tests //
994///////////
995
996#[cfg(test)]
997mod tests {
998    use super::*;
999    use bixverse_core::core::math::sparse::CompressedSparseData2;
1000    use faer::Mat;
1001
1002    /// Degrees, total weight and neighbour slices read straight off the CSR.
1003    #[test]
1004    fn test_sparse_graph_properties() {
1005        // Simple line graph: 0 <-> 1 <-> 2
1006        let data = vec![1.0, 1.0, 1.0, 1.0];
1007        let indices = vec![1, 0, 2, 1]; // neighbors
1008        let indptr = vec![0, 1, 3, 4]; // node offsets
1009        let csr =
1010            CompressedSparseData2::<f64, f64>::new_csr(&data, &indices, &indptr, None, (3, 3));
1011
1012        let graph = SparseGraph::new(3, csr, false);
1013
1014        assert_eq!(graph.get_node_degree(0), 1);
1015        assert_eq!(graph.get_node_degree(1), 2);
1016        assert_eq!(graph.total_weight(), 2.0); // 2 undirected edges of weight 1.0
1017
1018        let (n0_neighbors, n0_weights) = graph.get_neighbours(0);
1019        assert_eq!(n0_neighbors, &[1]);
1020        assert_eq!(n0_weights, &[1.0]);
1021    }
1022
1023    /// The unnormalised Laplacian is `D - A`, degrees on the diagonal.
1024    #[test]
1025    fn test_adjacency_to_laplacian() {
1026        // Adjacency for 0 <-> 1
1027        let mut adj: Mat<f64> = Mat::zeros(2, 2);
1028        adj[(0, 1)] = 1.0;
1029        adj[(1, 0)] = 1.0;
1030
1031        // Unnormalized Laplacian: L = D - A
1032        // D is [[1, 0], [0, 1]]
1033        let lap = adjacency_to_laplacian(&adj.as_ref(), false);
1034
1035        assert!((lap[(0, 0)] - 1.0).abs() < 1e-6);
1036        assert!((lap[(1, 1)] - 1.0).abs() < 1e-6);
1037        assert!((lap[(0, 1)] - (-1.0)).abs() < 1e-6);
1038        assert!((lap[(1, 0)] - (-1.0)).abs() < 1e-6);
1039    }
1040
1041    /// String node names map onto indices without dropping or duplicating edges.
1042    #[test]
1043    fn test_graph_from_strings_with_attributes() {
1044        let nodes = vec!["A".to_string(), "B".to_string(), "C".to_string()];
1045        let node_types = vec!["x".to_string(), "y".to_string(), "x".to_string()];
1046        let from = vec!["A".to_string(), "B".to_string(), "C".to_string()];
1047        let to = vec!["B".to_string(), "C".to_string(), "B".to_string()];
1048        let edge_types = vec!["e".to_string(), "f".to_string(), "e".to_string()];
1049        let weights = vec![1.5, 2.5, 0.5];
1050
1051        let graph = graph_from_strings_with_attributes(
1052            &nodes,
1053            &node_types,
1054            &from,
1055            &to,
1056            &edge_types,
1057            &weights,
1058        );
1059
1060        assert_eq!(graph.node_types, vec!["x", "y", "x"]);
1061        assert_eq!(graph.from, vec![0, 1, 2]);
1062        assert_eq!(graph.to, vec![1, 2, 1]);
1063        assert_eq!(graph.edge_types, vec!["e", "f", "e"]);
1064        assert_eq!(graph.weights, weights);
1065    }
1066
1067    /// A small undirected graph with a duplicate edge and a self-loop.
1068    fn small_graph() -> SparseGraph<f32> {
1069        let edges = [0usize, 1, 1, 2, 2, 0, 3, 3, 1, 2];
1070        let weights = [0.5f32, 1.0, 1.5, 2.0, 0.25];
1071        edge_pairs_to_sparse_graph(&edges, &weights, 5)
1072    }
1073
1074    /// Sorted `(u, v, w)` triples, so edge lists compare as sets.
1075    fn edge_set(edges: &[u32], weights: &[f32]) -> Vec<(u32, u32, u32)> {
1076        let mut out: Vec<_> = edges
1077            .chunks_exact(2)
1078            .zip(weights)
1079            .map(|(e, w)| (e[0].min(e[1]), e[0].max(e[1]), w.to_bits()))
1080            .collect();
1081        out.sort_unstable();
1082        out
1083    }
1084
1085    /// Edge pairs in, CSR, edge pairs out: the same multiset of edges.
1086    #[test]
1087    fn test_sparse_graph_edge_pairs_round_trip() {
1088        let graph = small_graph();
1089        let (edges, weights) = graph.to_edge_pairs();
1090        assert_eq!(weights.len(), 5);
1091        assert_eq!(graph.get_edge_number(), 5);
1092        assert_eq!(
1093            edge_set(&edges, &weights),
1094            edge_set(&[0, 1, 1, 2, 2, 0, 3, 3, 1, 2], &[0.5, 1.0, 1.5, 2.0, 0.25])
1095        );
1096    }
1097
1098    /// Bytes round trip keeps the CSR, node count and direction.
1099    #[test]
1100    fn test_sparse_graph_bytes_round_trip() {
1101        let graph = small_graph();
1102        let back = SparseGraph::<f32>::from_bytes(&graph.to_bytes().unwrap()).unwrap();
1103        assert_eq!(back.get_node_number(), 5);
1104        assert!(!back.is_directed());
1105        assert_eq!(back.adjacency.indptr, graph.adjacency.indptr);
1106        assert_eq!(back.adjacency.indices, graph.adjacency.indices);
1107        assert_eq!(back.adjacency.data, graph.adjacency.data);
1108    }
1109
1110    /// Foreign bytes, or a CSR blob, are not a graph.
1111    #[test]
1112    fn test_sparse_graph_bytes_bad_magic() {
1113        let csr_bytes = small_graph().adjacency.to_bytes().unwrap();
1114        assert!(matches!(
1115            SparseGraph::<f32>::from_bytes(&csr_bytes),
1116            Err(MethodsErrors::Core(CoreErrors::NotSerialised(_)))
1117        ));
1118    }
1119
1120    /// A blob from another layout version is refused.
1121    #[test]
1122    fn test_sparse_graph_bytes_version_mismatch() {
1123        let mut bytes = small_graph().to_bytes().unwrap();
1124        bytes[8..12].copy_from_slice(&999u32.to_le_bytes());
1125        assert!(matches!(
1126            SparseGraph::<f32>::from_bytes(&bytes),
1127            Err(MethodsErrors::Core(CoreErrors::SerialisedVersionMismatch {
1128                found: 999,
1129                ..
1130            }))
1131        ));
1132    }
1133
1134    /// An `f32` graph does not decode as `f64`.
1135    #[test]
1136    fn test_sparse_graph_bytes_width_mismatch() {
1137        let bytes = small_graph().to_bytes().unwrap();
1138        assert!(matches!(
1139            SparseGraph::<f64>::from_bytes(&bytes),
1140            Err(MethodsErrors::Core(
1141                CoreErrors::SerialisedWidthMismatch { .. }
1142            ))
1143        ));
1144    }
1145}