use std::cmp::Ordering;
use faer::Mat;
use rayon::prelude::*;
use crate::core::math::sparse::coo_to_csr_presorted;
use crate::prelude::*;
use super::params::MC2KnnParams;
type RowScan = (Vec<usize>, Vec<f32>, (usize, f32));
type PreservedEdge = (usize, usize, f32);
fn rank_rows_descending(similarity: &Mat<f32>) -> Mat<u32> {
let n = similarity.nrows();
let mut ranks = Mat::<u32>::from_fn(n, n, |_, _| 0);
let col_stride = ranks.col_stride() as usize;
let ranks_addr = ranks.as_ptr_mut() as usize;
(0..n).into_par_iter().for_each(|i| {
let mut order: Vec<usize> = (0..n).collect();
order.sort_unstable_by(|&a, &b| {
similarity[(i, b)]
.partial_cmp(&similarity[(i, a)])
.unwrap_or(Ordering::Equal)
});
unsafe {
let ranks_ptr = ranks_addr as *mut u32;
for (rank_minus_one, &col) in order.iter().enumerate() {
*ranks_ptr.add(col * col_stride + i) = (rank_minus_one as u32) + 1;
}
*ranks_ptr.add(i * col_stride + i) = n as u32;
}
});
ranks
}
fn balance_threshold_and_preserve(
ranks: &Mat<u32>,
max_rank: u32,
) -> (CompressedSparseData2<f32, f32>, Vec<PreservedEdge>) {
let n = ranks.nrows();
let max_rank_f = max_rank as f32;
let cutoff = max_rank_f + 1.0;
let per_row: Vec<RowScan> = (0..n)
.into_par_iter()
.map(|i| {
let mut cols: Vec<usize> = Vec::new();
let mut vals: Vec<f32> = Vec::new();
let mut best_col = 0usize;
let mut best_val = f32::NEG_INFINITY;
for j in 0..n {
if i == j {
continue;
}
let r_ij = ranks[(i, j)];
let r_ji = ranks[(j, i)];
let balanced = ((r_ij as f64) * (r_ji as f64)).sqrt() as f32;
if balanced >= cutoff {
continue;
}
let stored = cutoff - balanced;
if stored > best_val {
best_val = stored;
best_col = j;
}
cols.push(j);
vals.push(stored);
}
if best_val == f32::NEG_INFINITY {
let mut min_balanced = f32::INFINITY;
for j in 0..n {
if i == j {
continue;
}
let r_ij = ranks[(i, j)];
let r_ji = ranks[(j, i)];
let balanced = ((r_ij as f64) * (r_ji as f64)).sqrt() as f32;
if balanced < min_balanced {
min_balanced = balanced;
best_col = j;
}
}
best_val = 1.0; } else {
best_val = best_val.max(1.0);
}
(cols, vals, (best_col, best_val))
})
.collect();
let total_nnz: usize = per_row.iter().map(|(c, _, _)| c.len()).sum();
let mut data = Vec::with_capacity(total_nnz);
let mut indices = Vec::with_capacity(total_nnz);
let mut indptr = Vec::with_capacity(n + 1);
indptr.push(0);
let mut preserved = Vec::with_capacity(n);
for (i, (cols, vals, (bc, bv))) in per_row.into_iter().enumerate() {
for (c, v) in cols.into_iter().zip(vals) {
indices.push(c);
data.push(v);
}
indptr.push(data.len());
preserved.push((i, bc, bv));
}
let sparse = CompressedSparseData2 {
data,
indices: indices.index_cast(),
indptr: indptr.index_cast(),
cs_type: CompressedSparseFormat::Csr,
data_2: None,
shape: (n, n),
};
(sparse, preserved)
}
fn prune_per_row(
mat: &CompressedSparseData2<f32, f32>,
degree: usize,
) -> CompressedSparseData2<f32, f32> {
debug_assert!(mat.cs_type.is_csr());
let n_rows = mat.shape.0;
let per_row: Vec<Vec<(u32, f32)>> = (0..n_rows)
.into_par_iter()
.map(|i| {
let start = mat.indptr[i] as usize;
let end = mat.indptr[i + 1] as usize;
let nnz = end - start;
if nnz <= degree {
return mat.indices[start..end]
.iter()
.copied()
.zip(mat.data[start..end].iter().copied())
.collect();
}
let mut buf: Vec<(u32, f32)> = mat.indices[start..end]
.iter()
.copied()
.zip(mat.data[start..end].iter().copied())
.collect();
buf.select_nth_unstable_by(degree - 1, |a, b| {
b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal)
});
buf.truncate(degree);
buf.sort_unstable_by_key(|&(c, _)| c);
buf
})
.collect();
let total_nnz: usize = per_row.iter().map(|r| r.len()).sum();
let mut data = Vec::with_capacity(total_nnz);
let mut indices = Vec::with_capacity(total_nnz);
let mut indptr = Vec::with_capacity(n_rows + 1);
indptr.push(0);
for row in per_row {
for (c, v) in row {
indices.push(c);
data.push(v);
}
indptr.push(data.len() as u32);
}
CompressedSparseData2 {
data,
indices,
indptr,
cs_type: CompressedSparseFormat::Csr,
data_2: None,
shape: mat.shape,
}
}
fn prune_per_column(
mat: &CompressedSparseData2<f32, f32>,
degree: usize,
) -> CompressedSparseData2<f32, f32> {
debug_assert!(mat.cs_type.is_csr());
let transposed = mat.transpose_and_convert();
let pruned = prune_per_row(&transposed, degree);
pruned.transpose_and_convert()
}
fn merge_max_with_preserved(
pruned: CompressedSparseData2<f32, f32>,
preserved: &[(usize, usize, f32)],
n: usize,
) -> CompressedSparseData2<f32, f32> {
let mut entries: Vec<(usize, usize, f32)> =
Vec::with_capacity(pruned.indices.len() + preserved.len() * 2);
for i in 0..n {
let start = pruned.indptr[i] as usize;
let end = pruned.indptr[i + 1] as usize;
for idx in start..end {
entries.push((i, pruned.indices[idx] as usize, pruned.data[idx]));
}
}
for &(i, j, v) in preserved {
entries.push((i, j, v));
entries.push((j, i, v));
}
entries.sort_unstable_by_key(|a| (a.0, a.1));
let mut deduped: Vec<(usize, usize, f32)> = Vec::with_capacity(entries.len());
for entry in entries {
match deduped.last_mut() {
Some(last) if last.0 == entry.0 && last.1 == entry.1 => {
if entry.2 > last.2 {
last.2 = entry.2;
}
}
_ => deduped.push(entry),
}
}
let rows: Vec<usize> = deduped.iter().map(|e| e.0).collect();
let cols: Vec<usize> = deduped.iter().map(|e| e.1).collect();
let vals: Vec<f32> = deduped.iter().map(|e| e.2).collect();
coo_to_csr_presorted(&rows.index_cast(), &cols.index_cast(), &vals, (n, n))
}
fn weigh_rows_l1(mut mat: CompressedSparseData2<f32, f32>) -> CompressedSparseData2<f32, f32> {
debug_assert!(mat.cs_type.is_csr());
let n_rows = mat.shape.0;
for i in 0..n_rows {
let start = mat.indptr[i] as usize;
let end = mat.indptr[i + 1] as usize;
let sum: f32 = mat.data[start..end].iter().sum();
if sum > 0.0 {
for v in &mut mat.data[start..end] {
*v /= sum;
}
}
}
mat
}
pub fn build_knn_graph(
similarity: &Mat<f32>,
k: usize,
params: &MC2KnnParams,
) -> CompressedSparseData2<f32, f32> {
let n = similarity.nrows();
assert_eq!(similarity.ncols(), n, "similarity must be square");
assert!(k > 0, "k must be positive");
let max_rank = (k as f32 * params.balanced_ranks_factor).round().max(1.0) as u32;
let incoming_degree =
((k as f32 * params.incoming_degree_factor).round() as usize).min(n.saturating_sub(1));
let outgoing_degree = ((k as f32 * params.outgoing_degree_factor).round() as usize)
.max(params.min_outgoing_degree)
.min(n.saturating_sub(1));
let ranks = rank_rows_descending(similarity);
let (sparse, preserved) = balance_threshold_and_preserve(&ranks, max_rank);
drop(ranks);
let pruned_in = prune_per_column(&sparse, incoming_degree);
drop(sparse);
let pruned_out = prune_per_row(&pruned_in, outgoing_degree);
drop(pruned_in);
let merged = merge_max_with_preserved(pruned_out, &preserved, n);
weigh_rows_l1(merged)
}