use faer::Mat;
use rayon::prelude::*;
use crate::prelude::*;
use super::params::MC2KnnParams;
const ROW_TILE: usize = 16;
const BALANCE_SHIFT: f32 = 2_097_152.0;
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.div_ceil(ROW_TILE)).into_par_iter().for_each_init(
|| (Vec::<f32>::new(), Vec::<usize>::new()),
|(panel, order), tile| {
let first = tile * ROW_TILE;
let rows = ROW_TILE.min(n - first);
panel.clear();
panel.resize(rows * n, 0.0);
for j in 0..n {
for r in 0..rows {
panel[r * n + j] = similarity[(first + r, j)];
}
}
for r in 0..rows {
let i = first + r;
let row = &panel[r * n..(r + 1) * n];
order.clear();
order.extend(0..n);
order.sort_unstable_by(|&a, &b| row[b].total_cmp(&row[a]));
unsafe {
let ranks_ptr = ranks_addr as *mut u32;
let mut rank = 1u32;
for &col in order.iter() {
if col != i {
*ranks_ptr.add(i * col_stride + col) = rank;
rank += 1;
}
}
*ranks_ptr.add(i * col_stride + i) = n as u32;
}
}
},
);
ranks
}
#[inline]
fn balanced_stored(r_ij: u32, r_ji: u32, offset: f32) -> f32 {
let balanced = ((r_ij as f32) * (r_ji as f32)).sqrt();
(BALANCE_SHIFT - balanced) + offset
}
fn balance_ranks(ranks: &Mat<u32>, max_rank: f64) -> CompressedSparseData2<f32, f32> {
let n = ranks.nrows();
let offset = (max_rank + 1.0 - BALANCE_SHIFT as f64) as f32;
let per_row: Vec<Vec<(u32, f32)>> = (0..n.div_ceil(ROW_TILE))
.into_par_iter()
.map_init(Vec::<u32>::new, |panel, tile| {
let first = tile * ROW_TILE;
let rows = ROW_TILE.min(n - first);
panel.clear();
panel.resize(rows * n, 0);
for j in 0..n {
for r in 0..rows {
panel[r * n + j] = ranks[(first + r, j)];
}
}
let mut out = Vec::with_capacity(rows);
for r in 0..rows {
let i = first + r;
let ranks_ji = &panel[r * n..(r + 1) * n];
let col_i = ranks.col(i);
let mut row: Vec<(u32, f32)> = Vec::new();
let mut best: Option<(usize, f32)> = None;
for j in 0..n {
if i == j {
continue;
}
let stored = balanced_stored(col_i[j], ranks_ji[j], offset);
if best.is_none_or(|(_, v)| stored > v) {
best = Some((j, stored));
}
if stored > 0.0 {
row.push((j as u32, stored));
}
}
if let Some((best_col, best_val)) = best {
let floored = best_val.max(1.0);
match row.binary_search_by_key(&(best_col as u32), |&(c, _)| c) {
Ok(pos) => row[pos].1 = floored,
Err(pos) => row.insert(pos, (best_col as u32, floored)),
}
}
out.push(row);
}
out
})
.flatten_iter()
.collect();
rows_to_csr(per_row, (n, n))
}
fn rows_to_csr(
rows: Vec<Vec<(u32, f32)>>,
shape: (usize, usize),
) -> CompressedSparseData2<f32, f32> {
let total_nnz: usize = rows.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(rows.len() + 1);
indptr.push(0u32);
for row in rows {
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,
}
}
fn row_argmax(mat: &CompressedSparseData2<f32, f32>) -> Vec<(usize, usize, f32)> {
(0..mat.shape.0)
.filter_map(|i| {
let start = mat.indptr[i] as usize;
let end = mat.indptr[i + 1] as usize;
(start..end)
.reduce(|b, idx| if mat.data[idx] > mat.data[b] { idx } else { b })
.map(|b| (i, mat.indices[b] as usize, mat.data[b]))
})
.collect()
}
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 mut buf: Vec<(u32, f32)> = mat.indices[start..end]
.iter()
.copied()
.zip(mat.data[start..end].iter().copied())
.collect();
if buf.len() <= degree {
return buf;
}
if degree == 0 {
return Vec::new();
}
buf.select_nth_unstable_by(degree - 1, |a, b| b.1.total_cmp(&a.1));
buf.truncate(degree);
buf.sort_unstable_by_key(|&(c, _)| c);
buf
})
.collect();
rows_to_csr(per_row, 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)],
) -> CompressedSparseData2<f32, f32> {
let n = pruned.shape.0;
let mut extra: Vec<Vec<(u32, f32)>> = vec![Vec::new(); n];
for &(i, j, v) in preserved {
extra[i].push((j as u32, v));
extra[j].push((i as u32, v));
}
let rows: Vec<Vec<(u32, f32)>> = extra
.into_par_iter()
.enumerate()
.map(|(i, mut row)| {
let start = pruned.indptr[i] as usize;
let end = pruned.indptr[i + 1] as usize;
row.extend(
pruned.indices[start..end]
.iter()
.copied()
.zip(pruned.data[start..end].iter().copied()),
);
row.sort_unstable_by_key(|&(c, _)| c);
row.dedup_by(|later, kept| {
let same = later.0 == kept.0;
if same {
kept.1 = kept.1.max(later.1);
}
same
});
row
})
.collect();
rows_to_csr(rows, (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 {
let scale = sum.recip();
for v in &mut mat.data[start..end] {
*v *= scale;
}
}
}
mat
}
fn balance_and_prune(
ranks: &Mat<u32>,
k: usize,
params: &MC2KnnParams,
) -> CompressedSparseData2<f32, f32> {
let n = ranks.nrows();
let degree =
|factor: f64| ((k as f64 * factor).round_ties_even() as usize).min(n.saturating_sub(1));
let balanced = balance_ranks(ranks, k as f64 * params.balanced_ranks_factor);
let preserved = row_argmax(&balanced);
let pruned_in = prune_per_column(&balanced, degree(params.incoming_degree_factor));
drop(balanced);
let pruned_out = prune_per_row(&pruned_in, degree(params.outgoing_degree_factor));
drop(pruned_in);
merge_max_with_preserved(&pruned_out, &preserved)
}
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 required = params.min_outgoing_degree.min(n.saturating_sub(1));
let ranks = rank_rows_descending(similarity);
let mut k = k;
loop {
let pruned = balance_and_prune(&ranks, k, params);
let min_degree = (0..n)
.map(|i| (pruned.indptr[i + 1] - pruned.indptr[i]) as usize)
.min()
.unwrap_or(0);
if min_degree >= required {
return weigh_rows_l1(pruned);
}
k += (k / 10).max(1);
}
}