use rayon::prelude::*;
use rustc_hash::FxHashMap;
use std::time::Instant;
use crate::prelude::*;
#[derive(Clone, Copy, Default, Debug)]
pub enum SnnSimilarityMethod {
#[default]
Intersection,
Rank,
}
#[derive(Clone, Copy, Default, Debug)]
pub enum SnnType {
#[default]
FullConnection,
LimitedConnection,
}
pub fn parse_snn_similiarity_method(s: &str) -> Option<SnnSimilarityMethod> {
match s.to_lowercase().as_str() {
"jaccard" => Some(SnnSimilarityMethod::Intersection),
"rank" => Some(SnnSimilarityMethod::Rank),
_ => None,
}
}
pub fn parse_snn_type(s: &str) -> Option<SnnType> {
match s.to_lowercase().as_str() {
"full" => Some(SnnType::FullConnection),
"limited" => Some(SnnType::LimitedConnection),
_ => None,
}
}
pub fn generate_snn_full(
flat_knn: &[usize],
k: usize,
n_samples: usize,
pruning: f32,
method: SnnSimilarityMethod,
verbose: usize,
) -> (Vec<usize>, Vec<f32>) {
let verbosity = parse_verbosity_level(verbose);
let start_time = Instant::now();
let knn_rm: Vec<usize> = {
let mut v = vec![0usize; n_samples * k];
for i in 0..n_samples {
for nb in 0..k {
v[i * k + nb] = flat_knn[nb * n_samples + i];
}
}
v
};
let mut reverse_mappings: Vec<Vec<(usize, usize)>> =
(0..n_samples).map(|_| Vec::with_capacity(k + 1)).collect();
for i in 0..n_samples {
reverse_mappings[i].push((i, 0));
for nb in 0..k {
let neighbor = flat_knn[nb * n_samples + i];
reverse_mappings[neighbor].push((i, nb + 1));
}
}
let results: Vec<(usize, usize, f32)> = (0..n_samples)
.into_par_iter()
.map_init(
|| (vec![0.0f32; n_samples], Vec::<usize>::with_capacity(64)),
|(scores, added), j| {
added.clear();
for i in 0..=k {
let cur_neighbor = if i == 0 { j } else { knn_rm[j * k + (i - 1)] };
for &(othernode, other_rank) in &reverse_mappings[cur_neighbor] {
if othernode < j {
match method {
SnnSimilarityMethod::Rank => {
let combined_rank = (i + other_rank) as f32;
if scores[othernode] == 0.0 {
scores[othernode] = combined_rank;
added.push(othernode);
} else if combined_rank < scores[othernode] {
scores[othernode] = combined_rank;
}
}
SnnSimilarityMethod::Intersection => {
if scores[othernode] == 0.0 {
added.push(othernode);
}
scores[othernode] += 1.0;
}
}
}
}
}
let out: Vec<(usize, usize, f32)> = added
.iter()
.filter_map(|&othernode| {
let weight = match method {
SnnSimilarityMethod::Rank => {
let preliminary = k as f32 - scores[othernode] / 2.0;
let raw_weight = preliminary.max(1e-6);
raw_weight / k as f32
}
SnnSimilarityMethod::Intersection => {
scores[othernode] / (2.0 * (k as f32 + 1.0) - scores[othernode])
}
};
(weight >= pruning).then_some((j, othernode, weight))
})
.collect();
for &othernode in added.iter() {
scores[othernode] = 0.0;
}
out
},
)
.flatten()
.collect();
let mut edges = Vec::with_capacity(results.len() * 2);
let mut weights = Vec::with_capacity(results.len());
for (i, j, weight) in results {
edges.push(i);
edges.push(j);
weights.push(weight);
}
let end_snn = start_time.elapsed();
if verbosity.normal_verbosity() {
println!("Transformed kNN into a full sNN graph: {:.2?}", end_snn);
}
(edges, weights)
}
pub fn generate_snn_limited(
flat_knn: &[usize],
k: usize,
n_samples: usize,
pruning: f32,
method: SnnSimilarityMethod,
verbose: usize,
) -> (Vec<usize>, Vec<f32>) {
let verbosity = parse_verbosity_level(verbose);
let start_time = Instant::now();
let knn_rm: Vec<usize> = {
let mut v = vec![0usize; n_samples * k];
for r in 0..k {
let col_offset = r * n_samples;
for c in 0..n_samples {
v[c * k + r] = flat_knn[col_offset + c];
}
}
v
};
let attached: Vec<Vec<(usize, u32)>> = (0..n_samples)
.into_par_iter()
.map(|c| {
let mut v: Vec<(usize, u32)> = Vec::with_capacity(k + 1);
v.push((c, 0));
let row_start = c * k;
for r in 0..k {
v.push((knn_rm[row_start + r], (r + 1) as u32));
}
v.sort_unstable_by_key(|&(node, _)| node);
v
})
.collect();
let edge_map: FxHashMap<(usize, usize), f32> = (0..n_samples)
.into_par_iter()
.fold(FxHashMap::<(usize, usize), f32>::default, |mut acc, i| {
let ai = &attached[i];
let row_start = i * k;
for r in 0..k {
let j = knn_rm[row_start + r];
let aj = &attached[j];
let weight = match method {
SnnSimilarityMethod::Intersection => {
let (mut pi, mut pj) = (0usize, 0usize);
let mut count: u32 = 0;
while pi < ai.len() && pj < aj.len() {
let ni = ai[pi].0;
let nj = aj[pj].0;
if ni == nj {
count += 1;
pi += 1;
pj += 1;
} else if ni < nj {
pi += 1;
} else {
pj += 1;
}
}
let c = count as f32;
c / (2.0 * (k as f32 + 1.0) - c)
}
SnnSimilarityMethod::Rank => {
let (mut pi, mut pj) = (0usize, 0usize);
let mut min_combined: u32 = 2 * (k as u32);
while pi < ai.len() && pj < aj.len() {
let (ni, ri) = ai[pi];
let (nj, rj) = aj[pj];
if ni == nj {
let combined = ri + rj;
if combined < min_combined {
min_combined = combined;
}
pi += 1;
pj += 1;
} else if ni < nj {
pi += 1;
} else {
pj += 1;
}
}
let preliminary = k as f32 - min_combined as f32 / 2.0;
preliminary.max(1e-6) / k as f32
}
};
if weight >= pruning {
let edge_key = if i < j { (i, j) } else { (j, i) };
acc.entry(edge_key)
.and_modify(|w| {
if weight > *w {
*w = weight
}
})
.or_insert(weight);
}
}
acc
})
.reduce(FxHashMap::<(usize, usize), f32>::default, |mut a, mut b| {
if a.len() < b.len() {
std::mem::swap(&mut a, &mut b);
}
for (key, v) in b {
a.entry(key)
.and_modify(|w| {
if v > *w {
*w = v
}
})
.or_insert(v);
}
a
});
let mut edges = Vec::with_capacity(edge_map.len() * 2);
let mut weights = Vec::with_capacity(edge_map.len());
for ((i, j), weight) in edge_map {
edges.push(i);
edges.push(j);
weights.push(weight);
}
let end_snn = start_time.elapsed();
if verbosity.normal_verbosity() {
println!("Transformed kNN into an sNN graph: {:.2?}", end_snn);
}
(edges, weights)
}
pub fn snn_edges_to_sparse_graph(
edges: &[usize],
weights: &[f32],
n_nodes: usize,
) -> SparseGraph<f32> {
let n_edges = weights.len();
let mut indptr = vec![0usize; n_nodes + 1];
for e in 0..n_edges {
indptr[edges[2 * e] + 1] += 1;
indptr[edges[2 * e + 1] + 1] += 1;
}
for i in 1..=n_nodes {
indptr[i] += indptr[i - 1];
}
let nnz = indptr[n_nodes];
let mut indices = vec![0usize; nnz];
let mut data = vec![0.0f32; nnz];
let mut cursor = indptr.clone();
for e in 0..n_edges {
let i = edges[2 * e];
let j = edges[2 * e + 1];
let w = weights[e];
indices[cursor[i]] = j;
data[cursor[i]] = w;
cursor[i] += 1;
indices[cursor[j]] = i;
data[cursor[j]] = w;
cursor[j] += 1;
}
for node in 0..n_nodes {
let start = indptr[node];
let end = indptr[node + 1];
let len = end - start;
if len < 2 {
continue;
}
let mut order: Vec<usize> = (0..len).collect();
order.sort_unstable_by_key(|&k| indices[start + k]);
let row_idx: Vec<usize> = order.iter().map(|&k| indices[start + k]).collect();
let row_data: Vec<f32> = order.iter().map(|&k| data[start + k]).collect();
indices[start..end].copy_from_slice(&row_idx);
data[start..end].copy_from_slice(&row_data);
}
let adjacency = CompressedSparseData2 {
data,
indices: indices.index_cast(),
indptr: indptr.index_cast(),
cs_type: CompressedSparseFormat::Csr,
data_2: None,
shape: (n_nodes, n_nodes),
};
SparseGraph::new(n_nodes, adjacency, false)
}
#[cfg(test)]
mod tests {
use super::*;
fn small_knn() -> (Vec<usize>, usize, usize) {
let flat_knn = vec![1, 0, 0, 0, 2, 2, 1, 1];
(flat_knn, 2, 4)
}
fn edge_map(edges: &[usize], weights: &[f32]) -> FxHashMap<(usize, usize), f32> {
edges
.chunks(2)
.zip(weights.iter())
.map(|(p, &w)| ((p[0].min(p[1]), p[0].max(p[1])), w))
.collect()
}
#[test]
fn snn_full_jaccard_known_weights() {
let (knn, k, n) = small_knn();
let (edges, weights) =
generate_snn_full(&knn, k, n, 0.0, SnnSimilarityMethod::Intersection, 0);
let m = edge_map(&edges, &weights);
assert_eq!(m.len(), 6);
assert!((m[&(0, 1)] - 1.0).abs() < 1e-6);
assert!((m[&(0, 3)] - 0.5).abs() < 1e-6);
for &w in &weights {
assert!(w > 0.0 && w <= 1.0);
}
}
#[test]
fn snn_pruning() {
let (knn, k, n) = small_knn();
let (_, w_unpruned) =
generate_snn_full(&knn, k, n, 0.0, SnnSimilarityMethod::Intersection, 0);
let (_, w_pruned) =
generate_snn_full(&knn, k, n, 0.6, SnnSimilarityMethod::Intersection, 0);
assert!(w_pruned.len() < w_unpruned.len());
assert!(w_pruned.iter().all(|&w| w >= 0.6));
}
#[test]
fn snn_limited_is_subset_of_full() {
let (knn, k, n) = small_knn();
let (fe, fw) = generate_snn_full(&knn, k, n, 0.0, SnnSimilarityMethod::Intersection, 0);
let (le, lw) = generate_snn_limited(&knn, k, n, 0.0, SnnSimilarityMethod::Intersection, 0);
let full = edge_map(&fe, &fw);
let lim = edge_map(&le, &lw);
assert!(lim.len() <= full.len());
for (k, w) in &lim {
assert!(full.contains_key(k));
assert!((full[k] - w).abs() < 1e-6);
}
assert!(!lim.contains_key(&(2, 3)));
}
#[test]
fn snn_rank_weights_valid() {
let (knn, k, n) = small_knn();
for f in [generate_snn_full, generate_snn_limited] {
let (_, weights) = f(&knn, k, n, 0.0, SnnSimilarityMethod::Rank, 0);
assert!(!weights.is_empty());
assert!(weights.iter().all(|&w| w > 0.0 && w <= 1.0));
}
}
#[test]
fn method_parser() {
assert!(matches!(
parse_snn_similiarity_method("jaccard"),
Some(SnnSimilarityMethod::Intersection)
));
assert!(matches!(
parse_snn_similiarity_method("RANK"),
Some(SnnSimilarityMethod::Rank)
));
assert!(parse_snn_similiarity_method("nope").is_none());
}
#[test]
fn helper_round_trip() {
let edges = vec![0, 1, 1, 2, 0, 2];
let weights = vec![0.5, 0.7, 0.3];
let g = snn_edges_to_sparse_graph(&edges, &weights, 3);
assert_eq!(g.get_node_number(), 3);
assert!(!g.is_directed());
let (n0, w0) = g.get_neighbours(0);
assert_eq!(n0, &[1, 2]);
assert_eq!(w0, &[0.5, 0.3]);
let (n1, w1) = g.get_neighbours(1);
assert_eq!(n1, &[0, 2]);
assert_eq!(w1, &[0.5, 0.7]);
}
#[test]
fn helper_indices_sorted() {
let edges = vec![0, 5, 0, 2, 0, 4, 0, 1, 0, 3];
let weights = vec![0.1, 0.2, 0.3, 0.4, 0.5];
let g = snn_edges_to_sparse_graph(&edges, &weights, 6);
let (nbrs, _) = g.get_neighbours(0);
assert_eq!(nbrs, &[1, 2, 3, 4, 5]);
}
#[test]
fn helper_symmetric() {
let edges = vec![0, 2, 1, 3, 0, 3];
let weights = vec![0.4, 0.6, 0.2];
let g = snn_edges_to_sparse_graph(&edges, &weights, 4);
for u in 0..4 {
let (nbrs, ws) = g.get_neighbours(u);
for (&v, &w) in nbrs.iter().zip(ws.iter()) {
let (rn, rw) = g.get_neighbours(v);
let p = rn
.iter()
.position(|&x| x == u)
.expect("reverse edge missing");
assert!((rw[p] - w).abs() < 1e-6);
}
}
}
#[test]
fn helper_total_weight() {
let edges = vec![0, 1, 1, 2];
let weights = vec![0.4, 0.6];
let g = snn_edges_to_sparse_graph(&edges, &weights, 3);
assert!((g.total_weight() - 1.0).abs() < 1e-6);
}
#[test]
fn helper_empty() {
let g = snn_edges_to_sparse_graph(&[], &[], 5);
assert_eq!(g.get_node_number(), 5);
for i in 0..5 {
assert_eq!(g.get_node_degree(i), 0);
}
}
#[test]
fn helper_end_to_end() {
let (knn, k, n) = small_knn();
let (edges, weights) =
generate_snn_full(&knn, k, n, 0.0, SnnSimilarityMethod::Intersection, 0);
let g = snn_edges_to_sparse_graph(&edges, &weights, n);
let total_degree: usize = (0..n).map(|i| g.get_node_degree(i)).sum();
assert_eq!(total_degree, 2 * weights.len());
for i in 0..n {
let (nbrs, _) = g.get_neighbours(i);
let mut sorted = nbrs.to_vec();
sorted.sort();
assert_eq!(nbrs, &sorted[..]);
}
}
}