use rayon::prelude::*;
use crate::core::math::sparse::{undirected_edges_to_csr, validate_square_csr};
use crate::graph::graph_structures::UnionFind;
use crate::prelude::*;
fn spanning_forest<T>(
graph: &CompressedSparseData2<T>,
maximise: bool,
) -> Result<CompressedSparseData2<T>, BixverseErrors>
where
T: BixverseFloat + BixverseNumeric,
{
let n = validate_square_csr(graph)?;
let mut edges = candidate_edges(graph, n);
edges.par_sort_unstable_by(|a, b| {
weight_order(a.2, b.2, maximise).then_with(|| (a.0, a.1).cmp(&(b.0, b.1)))
});
let mut uf = UnionFind::new(n);
let mut kept: Vec<(u32, u32, T)> = Vec::with_capacity(n.saturating_sub(1));
for &(lo, hi, w) in &edges {
if uf.union(lo, hi) {
kept.push((lo, hi, w));
}
}
kept.sort_unstable_by_key(|e| (e.0, e.1));
Ok(undirected_edges_to_csr(n, &kept))
}
fn candidate_edges<T>(graph: &CompressedSparseData2<T>, n: usize) -> Vec<(u32, u32, T)>
where
T: BixverseFloat + BixverseNumeric,
{
let mut edges: Vec<(u32, u32, T)> = Vec::with_capacity(graph.get_nnz());
for i in 0..n {
let row = i as u32;
for idx in graph.indptr[i] as usize..graph.indptr[i + 1] as usize {
let j = graph.indices[idx];
let w = graph.data[idx];
if j == row || w.is_nan() {
continue;
}
edges.push((row.min(j), row.max(j), w));
}
}
edges
}
#[inline]
fn weight_order<T>(a: T, b: T, maximise: bool) -> std::cmp::Ordering
where
T: BixverseFloat,
{
if maximise {
b.total_cmp(&a)
} else {
a.total_cmp(&b)
}
}
pub fn minimum_spanning_forest<T>(
graph: &CompressedSparseData2<T>,
) -> Result<CompressedSparseData2<T>, BixverseErrors>
where
T: BixverseFloat + BixverseNumeric,
{
spanning_forest(graph, false)
}
pub fn maximum_spanning_forest<T>(
graph: &CompressedSparseData2<T>,
) -> Result<CompressedSparseData2<T>, BixverseErrors>
where
T: BixverseFloat + BixverseNumeric,
{
spanning_forest(graph, true)
}
#[cfg(test)]
mod tests {
use super::*;
fn undirected_csr(n: usize, edges: &[(usize, usize, f64)]) -> CompressedSparseData2<f64> {
let mut rows: Vec<Vec<(u32, f64)>> = vec![Vec::new(); n];
for &(a, b, w) in edges {
rows[a].push((b as u32, w));
rows[b].push((a as u32, w));
}
let mut indptr: Vec<u32> = vec![0];
let mut indices: Vec<u32> = Vec::new();
let mut data: Vec<f64> = Vec::new();
for row in rows.iter_mut() {
row.sort_unstable_by_key(|e| e.0);
for &(j, w) in row.iter() {
indices.push(j);
data.push(w);
}
indptr.push(indices.len() as u32);
}
CompressedSparseData2::new_csr(&data, &indices, &indptr, None, (n, n))
}
fn undirected_csr_unsorted(
n: usize,
edges: &[(usize, usize, f64)],
) -> CompressedSparseData2<f64> {
let mut rows: Vec<Vec<(u32, f64)>> = vec![Vec::new(); n];
for &(a, b, w) in edges {
rows[a].push((b as u32, w));
rows[b].push((a as u32, w));
}
let mut indptr: Vec<u32> = vec![0];
let mut indices: Vec<u32> = Vec::new();
let mut data: Vec<f64> = Vec::new();
for row in rows.iter() {
for &(j, w) in row.iter() {
indices.push(j);
data.push(w);
}
indptr.push(indices.len() as u32);
}
CompressedSparseData2::new_csr(&data, &indices, &indptr, None, (n, n))
}
fn edge_list(csr: &CompressedSparseData2<f64>) -> Vec<(usize, usize, f64)> {
let n = csr.nrows();
let mut out = Vec::new();
for i in 0..n {
for idx in csr.indptr[i] as usize..csr.indptr[i + 1] as usize {
let j = csr.indices[idx] as usize;
if i < j {
out.push((i, j, csr.data[idx]));
}
}
}
out.sort_unstable_by_key(|e| (e.0, e.1));
out
}
#[test]
fn test_minimum_and_maximum_forests_pick_different_edges() {
let graph = undirected_csr(
4,
&[
(0, 1, 1.0),
(1, 2, 2.0),
(2, 3, 3.0),
(0, 3, 9.0),
(0, 2, 10.0),
],
);
let min_tree = edge_list(&minimum_spanning_forest(&graph).unwrap());
assert_eq!(min_tree, vec![(0, 1, 1.0), (1, 2, 2.0), (2, 3, 3.0)]);
let max_tree = edge_list(&maximum_spanning_forest(&graph).unwrap());
assert_eq!(max_tree, vec![(0, 2, 10.0), (0, 3, 9.0), (1, 2, 2.0)]);
}
#[test]
fn test_forest_spans_each_component_separately() {
let graph = undirected_csr(
6,
&[
(0, 1, 1.0),
(1, 2, 2.0),
(0, 2, 3.0),
(3, 4, 1.0),
(4, 5, 2.0),
(3, 5, 3.0),
],
);
let forest = edge_list(&minimum_spanning_forest(&graph).unwrap());
assert_eq!(forest.len(), 4);
assert_eq!(
forest,
vec![(0, 1, 1.0), (1, 2, 2.0), (3, 4, 1.0), (4, 5, 2.0)]
);
}
#[test]
fn test_isolated_node_contributes_no_edges() {
let graph = undirected_csr(3, &[(0, 1, 1.0)]);
let forest = minimum_spanning_forest(&graph).unwrap();
assert_eq!(edge_list(&forest), vec![(0, 1, 1.0)]);
assert_eq!(forest.shape, (3, 3));
assert_eq!(forest.indptr[3], 2);
}
#[test]
fn test_asymmetric_pair_resolves_towards_the_objective() {
let data = vec![5.0f64, 1.0];
let indices = vec![1u32, 0];
let indptr = vec![0u32, 1, 2];
let graph = CompressedSparseData2::new_csr(&data, &indices, &indptr, None, (2, 2));
assert_eq!(
edge_list(&minimum_spanning_forest(&graph).unwrap()),
vec![(0, 1, 1.0)]
);
assert_eq!(
edge_list(&maximum_spanning_forest(&graph).unwrap()),
vec![(0, 1, 5.0)]
);
}
#[test]
fn test_all_equal_weights_break_ties_deterministically() {
let mut edges = Vec::new();
for i in 0..4 {
for j in (i + 1)..4 {
edges.push((i, j, 1.0));
}
}
let graph = undirected_csr(4, &edges);
let mut permuted = edges.clone();
permuted.reverse();
let permuted = undirected_csr_unsorted(4, &permuted);
let first = edge_list(&minimum_spanning_forest(&graph).unwrap());
let second = edge_list(&minimum_spanning_forest(&permuted).unwrap());
assert_eq!(first, second);
assert_eq!(first, vec![(0, 1, 1.0), (0, 2, 1.0), (0, 3, 1.0)]);
}
#[test]
fn test_self_loops_and_nan_weights_are_dropped() {
let data = vec![f64::NAN, 7.0, 1.0, 1.0];
let indices = vec![0u32, 1, 0, 0];
let indptr = vec![0u32, 2, 3, 4];
let graph = CompressedSparseData2::new_csr(&data, &indices, &indptr, None, (3, 3));
assert_eq!(
edge_list(&minimum_spanning_forest(&graph).unwrap()),
vec![(0, 1, 1.0), (0, 2, 1.0)]
);
}
#[test]
fn test_a_nan_on_a_real_edge_is_dropped() {
let graph = undirected_csr(3, &[(0, 1, f64::NAN), (0, 2, 5.0), (1, 2, 6.0)]);
assert_eq!(
edge_list(&maximum_spanning_forest(&graph).unwrap()),
vec![(0, 2, 5.0), (1, 2, 6.0)]
);
assert_eq!(
edge_list(&minimum_spanning_forest(&graph).unwrap()),
vec![(0, 2, 5.0), (1, 2, 6.0)]
);
}
#[test]
fn test_infinite_weights_are_ordinary_edges() {
let graph = undirected_csr(4, &[(0, 1, 1.0), (2, 3, 1.0), (1, 2, f64::INFINITY)]);
let max_forest = edge_list(&maximum_spanning_forest(&graph).unwrap());
assert_eq!(max_forest.len(), 3);
assert!(max_forest.iter().any(|e| (e.0, e.1) == (1, 2)));
let min_forest = edge_list(&minimum_spanning_forest(&graph).unwrap());
assert_eq!(min_forest.len(), 3);
assert!(min_forest.iter().any(|e| (e.0, e.1) == (1, 2)));
}
#[test]
fn test_zero_weight_edges_are_kept() {
let graph = undirected_csr(3, &[(0, 1, 0.0), (1, 2, 1.0), (0, 2, 2.0)]);
assert_eq!(
edge_list(&minimum_spanning_forest(&graph).unwrap()),
vec![(0, 1, 0.0), (1, 2, 1.0)]
);
}
#[test]
fn test_empty_graph_returns_empty_forest() {
let graph = CompressedSparseData2::<f64>::new_csr(&[], &[], &[0], None, (0, 0));
let forest = minimum_spanning_forest(&graph).unwrap();
assert_eq!(forest.shape, (0, 0));
assert_eq!(forest.get_nnz(), 0);
}
#[test]
fn test_rejects_csc_input() {
let graph = undirected_csr(3, &[(0, 1, 1.0)]).transform();
assert!(minimum_spanning_forest(&graph).is_err());
}
#[test]
fn test_rejects_non_square_input() {
let data = vec![1.0f64];
let indices = vec![1u32];
let indptr = vec![0u32, 1, 1];
let graph = CompressedSparseData2::new_csr(&data, &indices, &indptr, None, (2, 3));
assert!(maximum_spanning_forest(&graph).is_err());
}
}