use ann_search_rs::utils::dist::SimdDistance;
use faer::MatRef;
use rayon::prelude::*;
use rustc_hash::FxHashSet;
use std::time::Instant;
use crate::prelude::*;
use crate::single_cell::sc_processing::snn::*;
#[derive(Clone, Copy, Debug, Default)]
pub enum SigmaMethod {
#[default]
SnnFarthest,
SigmaIdx,
}
pub fn parse_sigma_method(s: &str) -> Option<SigmaMethod> {
match s.to_lowercase().as_str() {
"snn" => Some(SigmaMethod::SnnFarthest),
"sigma" => Some(SigmaMethod::SigmaIdx),
_ => None,
}
}
pub struct ModalityInput<'a> {
pub embedding: MatRef<'a, f32>,
pub knn_indices: &'a [Vec<usize>],
pub knn_distances: &'a [Vec<f32>],
}
impl<'a> ModalityInput<'a> {
pub fn new(
embedding: MatRef<'a, f32>,
knn_indices: &'a [Vec<usize>],
knn_distances: &'a [Vec<f32>],
) -> Self {
Self {
embedding,
knn_distances,
knn_indices,
}
}
}
#[derive(Clone)]
struct ModalityCellStats {
nearest: Vec<f32>,
within_dist: Vec<f32>,
cross_dist: Vec<f32>,
sigma: Vec<f32>,
}
pub struct WnnResult {
pub wnn_indices: Vec<Vec<usize>>,
pub wnn_distances: Vec<Vec<f32>>,
pub modality_weights: Vec<Vec<f32>>,
}
#[derive(Clone, Debug)]
pub struct WnnParams {
pub k_nn: usize,
pub knn_range: usize,
pub sigma_method: SigmaMethod,
pub sigma_idx: usize,
pub s_nn: usize,
pub snn_type: SnnType,
pub sd_scale: f32,
pub kernel_power: f32,
pub cross_const: f32,
pub sigma_floor: f32,
pub knn_params: KnnParams,
}
impl Default for WnnParams {
fn default() -> Self {
Self {
k_nn: 20,
knn_range: 200,
sigma_method: SigmaMethod::SnnFarthest,
sigma_idx: 19,
s_nn: 20,
snn_type: SnnType::FullConnection,
sd_scale: 1.0,
kernel_power: 1.0,
cross_const: 1e-4,
sigma_floor: 1e-8,
knn_params: KnnParams::default(),
}
}
}
fn to_row_major_vec(mat: MatRef<f32>) -> Vec<f32> {
let nc = mat.ncols();
let mut out = vec![0.0f32; mat.nrows() * nc];
out.par_chunks_mut(nc).enumerate().for_each(|(i, row)| {
for (j, v) in row.iter_mut().enumerate() {
*v = mat[(i, j)];
}
});
out
}
#[inline]
fn row_of(data: &[f32], d: usize, i: usize) -> &[f32] {
&data[i * d..(i + 1) * d]
}
#[inline]
fn mean_of_rows_into(data: &[f32], d: usize, indices: &[usize], out: &mut [f32]) {
out.fill(0.0);
for &i in indices {
for (a, v) in out.iter_mut().zip(row_of(data, d, i)) {
*a += v;
}
}
let inv_n = 1.0 / indices.len() as f32;
for v in out.iter_mut() {
*v *= inv_n;
}
}
#[inline]
fn euclid(a: &[f32], b: &[f32]) -> f32 {
f32::euclidean_simd(a, b).sqrt()
}
fn build_snn_for_sigma(
knn_indices: &[Vec<usize>],
s_nn: usize,
snn_type: SnnType,
n_cells: usize,
verbose: usize,
) -> SparseGraph<f32> {
let mut flat = vec![0usize; s_nn * n_cells];
for i in 0..n_cells {
for j in 0..s_nn {
flat[j * n_cells + i] = knn_indices[i][j];
}
}
let (edges, weights) = match snn_type {
SnnType::FullConnection => generate_snn_full(
&flat,
s_nn,
n_cells,
0.0,
SnnSimilarityMethod::Intersection,
verbose,
),
SnnType::LimitedConnection => generate_snn_limited(
&flat,
s_nn,
n_cells,
0.0,
SnnSimilarityMethod::Intersection,
verbose,
),
};
snn_edges_to_sparse_graph(&edges, &weights, n_cells)
}
#[allow(clippy::too_many_arguments)]
fn sigma_from_snn(
snn: &SparseGraph<f32>,
embedding: &[f32],
d: usize,
i: usize,
nearest: f32,
k_far: usize,
sd_scale: f32,
sigma_floor: f32,
) -> f32 {
let (nbrs, weights) = snn.get_neighbours(i);
if nbrs.is_empty() {
return sigma_floor;
}
let mut pairs: Vec<(f32, usize)> = weights
.iter()
.zip(nbrs.iter())
.map(|(&w, &n)| (w, n))
.collect();
pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let take = k_far.min(pairs.len());
let threshold = pairs[take - 1].0;
let selected: Vec<usize> = pairs
.iter()
.take_while(|(w, _)| *w <= threshold)
.map(|(_, c)| *c)
.collect();
let row_i = row_of(embedding, d, i);
let mut total = 0.0f32;
for &c in &selected {
total += (euclid(row_i, row_of(embedding, d, c)) - nearest).max(0.0);
}
let mean = total / selected.len() as f32;
(mean * sd_scale).max(sigma_floor)
}
pub fn compute_wnn(
modalities: [ModalityInput<'_>; 2],
params: &WnnParams,
verbose: usize,
) -> Result<WnnResult, BixverseErrors> {
let verbosity = parse_verbosity_level(verbose);
let n_cells = modalities[0].embedding.nrows();
if n_cells != modalities[1].embedding.nrows() {
return Err(BixverseErrors::WNNModalitySampleMismatch);
}
if params.k_nn > params.knn_range {
return Err(BixverseErrors::WNNKnnLargerThanKnnRange);
}
if params.sigma_idx >= params.knn_range {
return Err(BixverseErrors::WNNSigmaIdxOutOfRange);
}
if params.s_nn > params.knn_range {
return Err(BixverseErrors::WNNSnnLargerThanKnnRange);
}
for (r, m) in modalities.iter().enumerate() {
if m.knn_indices.len() != n_cells {
return Err(BixverseErrors::WNNKnnRowCountMismatch {
modality: r,
expected: n_cells,
found: m.knn_indices.len(),
});
}
if m.knn_distances.len() != n_cells {
return Err(BixverseErrors::WNNKnnDistRowCountMismatch {
modality: r,
expected: n_cells,
found: m.knn_distances.len(),
});
}
if !m.knn_indices.is_empty() {
if m.knn_indices[0].len() < params.knn_range {
return Err(BixverseErrors::WNNInsufficientNeighbours {
modality: r,
expected: params.knn_range,
found: m.knn_indices[0].len(),
});
}
if m.knn_distances[0].len() < params.knn_range {
return Err(BixverseErrors::WNNInsufficientNeighbours {
modality: r,
expected: params.knn_range,
found: m.knn_distances[0].len(),
});
}
}
}
let snn_graphs: Option<Vec<SparseGraph<f32>>> = match params.sigma_method {
SigmaMethod::SnnFarthest => {
let t = Instant::now();
let g: Vec<_> = (0..2)
.map(|r| {
build_snn_for_sigma(
modalities[r].knn_indices,
params.s_nn,
params.snn_type,
n_cells,
verbose,
)
})
.collect();
if verbosity.normal_verbosity() {
println!("WNN: per-modality SNN built in {:.2?}", t.elapsed());
}
Some(g)
}
SigmaMethod::SigmaIdx => None,
};
let t_phase1 = Instant::now();
let k_nn = params.k_nn;
let dims = [
modalities[0].embedding.ncols(),
modalities[1].embedding.ncols(),
];
let rows_rm = [
to_row_major_vec(modalities[0].embedding),
to_row_major_vec(modalities[1].embedding),
];
let per_modality: Vec<ModalityCellStats> = (0..2)
.map(|r| {
let m = &modalities[r];
let m_other = &modalities[1 - r];
let snn = snn_graphs.as_ref().map(|gs| &gs[r]);
let d = dims[r];
let data = &rows_rm[r];
let rows: Vec<(f32, f32, f32, f32)> = (0..n_cells)
.into_par_iter()
.map_init(
|| (vec![0.0f32; d], vec![0.0f32; d]),
|(within, cross), i| {
let nn_r = &m.knn_indices[i][..k_nn];
let nn_other = &m_other.knn_indices[i][..k_nn];
let nearest = m.knn_distances[i][0];
let row_i = row_of(data, d, i);
mean_of_rows_into(data, d, nn_r, within);
mean_of_rows_into(data, d, nn_other, cross);
let d_within = (euclid(row_i, within) - nearest).max(0.0);
let d_cross = (euclid(row_i, cross) - nearest).max(0.0);
let sigma = match params.sigma_method {
SigmaMethod::SigmaIdx => {
let raw = (m.knn_distances[i][params.sigma_idx] - nearest)
* params.sd_scale;
raw.max(params.sigma_floor)
}
SigmaMethod::SnnFarthest => sigma_from_snn(
snn.unwrap(),
data,
d,
i,
nearest,
params.k_nn,
params.sd_scale,
params.sigma_floor,
),
};
(nearest, d_within, d_cross, sigma)
},
)
.collect();
let mut stats = ModalityCellStats {
nearest: Vec::with_capacity(n_cells),
within_dist: Vec::with_capacity(n_cells),
cross_dist: Vec::with_capacity(n_cells),
sigma: Vec::with_capacity(n_cells),
};
for (n, w, c, s) in rows {
stats.nearest.push(n);
stats.within_dist.push(w);
stats.cross_dist.push(c);
stats.sigma.push(s);
}
stats
})
.collect();
let scores: Vec<Vec<f32>> = (0..2)
.map(|r| {
let s = &per_modality[r];
(0..n_cells)
.map(|i| {
let kw = (-s.within_dist[i] / s.sigma[i]).exp();
let kc = (-s.cross_dist[i] / s.sigma[i]).exp();
kw / (kc + params.cross_const)
})
.collect()
})
.collect();
let mut weights = vec![vec![0.0f32; n_cells]; 2];
for i in 0..n_cells {
let m = scores[0][i].max(scores[1][i]);
let e0 = (scores[0][i] - m).exp();
let e1 = (scores[1][i] - m).exp();
let s = e0 + e1;
weights[0][i] = e0 / s;
weights[1][i] = e1 / s;
}
if verbosity.normal_verbosity() {
println!("WNN: modality weights in {:.2?}", t_phase1.elapsed());
}
let t_phase2 = Instant::now();
let kernel_power = params.kernel_power;
let wnn: Vec<(Vec<usize>, Vec<f32>)> = (0..n_cells)
.into_par_iter()
.map_init(
|| {
(
FxHashSet::<usize>::default(),
Vec::<usize>::new(),
Vec::<f32>::new(),
Vec::<usize>::new(),
)
},
|(seen, cands, combined, order), i| {
let r0 = &modalities[0].knn_indices[i][..params.knn_range];
let r1 = &modalities[1].knn_indices[i][..params.knn_range];
seen.clear();
cands.clear();
for &c in r0.iter().chain(r1.iter()) {
if c != i && seen.insert(c) {
cands.push(c);
}
}
let row_i_0 = row_of(&rows_rm[0], dims[0], i);
let row_i_1 = row_of(&rows_rm[1], dims[1], i);
let nearest_0 = per_modality[0].nearest[i];
let nearest_1 = per_modality[1].nearest[i];
let sigma_0 = per_modality[0].sigma[i];
let sigma_1 = per_modality[1].sigma[i];
let w0 = weights[0][i];
let w1 = weights[1][i];
combined.clear();
for &c in cands.iter() {
let d0 =
(euclid(row_i_0, row_of(&rows_rm[0], dims[0], c)) - nearest_0).max(0.0);
let d1 =
(euclid(row_i_1, row_of(&rows_rm[1], dims[1], c)) - nearest_1).max(0.0);
let (a0, a1) = if kernel_power == 1.0 {
((-(d0 / sigma_0)).exp(), (-(d1 / sigma_1)).exp())
} else {
(
(-((d0 / sigma_0).powf(kernel_power))).exp(),
(-((d1 / sigma_1).powf(kernel_power))).exp(),
)
};
combined.push(w0 * a0 + w1 * a1);
}
order.clear();
order.extend(0..cands.len());
order.sort_unstable_by(|&a, &b| {
combined[b]
.partial_cmp(&combined[a])
.unwrap_or(std::cmp::Ordering::Equal)
});
order.truncate(k_nn);
let mut idx_out = Vec::with_capacity(k_nn);
let mut dist_out = Vec::with_capacity(k_nn);
for &o in order.iter() {
idx_out.push(cands[o]);
let aff = combined[o].clamp(0.0, 1.0);
dist_out.push(((1.0 - aff) / 2.0).max(0.0).sqrt());
}
(idx_out, dist_out)
},
)
.collect();
if verbosity.normal_verbosity() {
println!("WNN: graph constructed in {:.2?}", t_phase2.elapsed());
}
let (wnn_indices, wnn_distances): (Vec<_>, Vec<_>) = wnn.into_iter().unzip();
Ok(WnnResult {
wnn_indices,
wnn_distances,
modality_weights: weights,
})
}
#[cfg(test)]
mod tests {
use super::*;
use faer::Mat;
const EPS: f32 = 1e-5;
fn approx_eq(a: f32, b: f32, eps: f32) -> bool {
(a - b).abs() < eps
}
#[test]
fn parse_sigma_method_known_and_unknown() {
assert!(matches!(
parse_sigma_method("snn"),
Some(SigmaMethod::SnnFarthest)
));
assert!(matches!(
parse_sigma_method("SNN"),
Some(SigmaMethod::SnnFarthest)
));
assert!(matches!(
parse_sigma_method("sigma"),
Some(SigmaMethod::SigmaIdx)
));
assert!(parse_sigma_method("nope").is_none());
}
#[test]
fn row_major_copy_has_correct_values() {
let mat = Mat::from_fn(3, 4, |i, j| (i * 10 + j) as f32);
let rm = to_row_major_vec(mat.as_ref());
assert_eq!(row_of(&rm, 4, 1), &[10.0, 11.0, 12.0, 13.0]);
assert_eq!(row_of(&rm, 4, 2), &[20.0, 21.0, 22.0, 23.0]);
}
#[test]
fn mean_of_rows_known_centroid() {
let mat = Mat::from_fn(3, 3, |i, j| (i * (j + 1) * 2) as f32);
let rm = to_row_major_vec(mat.as_ref());
let mut mean = vec![0.0f32; 3];
mean_of_rows_into(&rm, 3, &[0, 1, 2], &mut mean);
assert_eq!(mean, vec![2.0, 4.0, 6.0]);
mean_of_rows_into(&rm, 3, &[1, 2], &mut mean);
assert_eq!(mean, vec![3.0, 6.0, 9.0]);
}
#[test]
fn euclid_known_distance() {
let a = [3.0, 4.0];
let b = [0.0, 0.0];
assert!(approx_eq(euclid(&a, &b), 5.0, EPS));
let c = [1.0, 2.0, 3.0];
let d = [1.0, 2.0, 3.0];
assert!(approx_eq(euclid(&c, &d), 0.0, EPS));
}
fn small_fixture() -> (Mat<f32>, Vec<Vec<usize>>, Vec<Vec<f32>>) {
let coords = [[0.0_f32, 0.0], [0.1, 0.0], [0.0, 0.1], [10.0, 10.0]];
let mat = Mat::from_fn(4, 2, |i, j| coords[i][j]);
let knn_indices = vec![vec![1, 2], vec![0, 2], vec![0, 1], vec![0, 1]];
let d_diag = (0.01_f32 + 0.01).sqrt(); let d_far = (100.0_f32 + 100.0).sqrt(); let knn_distances = vec![
vec![0.1, 0.1],
vec![0.1, d_diag],
vec![0.1, d_diag],
vec![d_far, d_far],
];
(mat, knn_indices, knn_distances)
}
#[test]
fn sigma_from_snn_floors_when_empty() {
let (mat, _, _) = small_fixture();
let empty_csr = CompressedSparseData2 {
data: vec![],
indices: vec![],
indptr: vec![0; 5],
cs_type: CompressedSparseFormat::Csr,
data_2: None,
shape: (4, 4),
};
let snn = SparseGraph::new(4, empty_csr, false);
let rm = to_row_major_vec(mat.as_ref());
let s = sigma_from_snn(&snn, &rm, mat.ncols(), 0, 0.1, 2, 1.0, 1e-8);
assert!(approx_eq(s, 1e-8, EPS));
}
#[test]
fn sigma_from_snn_known_value() {
let (mat, _, _) = small_fixture();
let csr = CompressedSparseData2 {
data: vec![0.5, 0.5, 0.1],
indices: vec![1, 2, 3],
indptr: vec![0, 3, 3, 3, 3],
cs_type: CompressedSparseFormat::Csr,
data_2: None,
shape: (4, 4),
};
let snn = SparseGraph::new(4, csr, false);
let rm = to_row_major_vec(mat.as_ref());
let s = sigma_from_snn(&snn, &rm, mat.ncols(), 0, 0.1, 2, 1.0, 1e-8);
let expected = (14.0421 + 0.0 + 0.0) / 3.0;
assert!(
approx_eq(s, expected, 1e-2),
"expected ~{expected}, got {s}"
);
}
#[test]
fn compute_wnn_identical_modalities_balanced_weights() {
let (mat, knn, dist) = small_fixture();
let mod_a = ModalityInput::new(mat.as_ref(), &knn, &dist);
let mod_b = ModalityInput::new(mat.as_ref(), &knn, &dist);
let params = WnnParams {
k_nn: 2,
knn_range: 2,
sigma_method: SigmaMethod::SigmaIdx,
sigma_idx: 1,
s_nn: 2,
snn_type: SnnType::FullConnection,
sd_scale: 1.0,
kernel_power: 1.0,
cross_const: 1e-4,
sigma_floor: 1e-8,
knn_params: KnnParams::default(),
};
let res = compute_wnn([mod_a, mod_b], ¶ms, 0)
.expect("WNN failed on identical modality fixture");
assert_eq!(res.wnn_indices.len(), 4);
assert_eq!(res.wnn_distances.len(), 4);
assert_eq!(res.modality_weights.len(), 2);
assert_eq!(res.modality_weights[0].len(), 4);
assert_eq!(res.modality_weights[1].len(), 4);
for i in 0..4 {
assert_eq!(res.wnn_indices[i].len(), params.k_nn);
assert_eq!(res.wnn_distances[i].len(), params.k_nn);
}
for i in 0..4 {
let w0 = res.modality_weights[0][i];
let w1 = res.modality_weights[1][i];
assert!(approx_eq(w0, 0.5, 1e-4), "cell {i}: w0={w0}, expected ~0.5");
assert!(approx_eq(w1, 0.5, 1e-4));
assert!(approx_eq(w0 + w1, 1.0, 1e-4));
}
}
#[test]
fn compute_wnn_asymmetric_modalities_shift_weights() {
let (mat, knn, dist) = small_fixture();
let mat_b = Mat::from_fn(4, 2, |i, j| {
let v = mat[(i, j)];
if j == 0 {
mat[(i, 1)]
} else {
v - mat[(i, 0)] + mat[(i, 1)]
}
});
let mod_a = ModalityInput::new(mat.as_ref(), &knn, &dist);
let mod_b = ModalityInput::new(mat_b.as_ref(), &knn, &dist);
let params = WnnParams {
k_nn: 2,
knn_range: 2,
sigma_method: SigmaMethod::SigmaIdx,
sigma_idx: 1,
s_nn: 2,
..Default::default()
};
let res = compute_wnn([mod_a, mod_b], ¶ms, 0)
.expect("WNN failed on rotated modality fixture");
for i in 0..4 {
let s = res.modality_weights[0][i] + res.modality_weights[1][i];
println!(
"PROBE cell {i}: {} {}",
res.modality_weights[0][i], res.modality_weights[1][i]
);
assert!(
approx_eq(s, 1.0, 1e-4),
"cell {i} weights do not sum to 1: {s}"
);
}
}
#[test]
fn compute_wnn_errors_on_cell_count_mismatch() {
let (mat_a, knn, dist) = small_fixture();
let mat_b = Mat::from_fn(3, 2, |_, _| 0.0_f32);
let mod_a = ModalityInput::new(mat_a.as_ref(), &knn, &dist);
let mod_b = ModalityInput::new(mat_b.as_ref(), &knn, &dist);
let params = WnnParams {
k_nn: 2,
knn_range: 2,
sigma_idx: 1,
s_nn: 2,
..Default::default()
};
let res = compute_wnn([mod_a, mod_b], ¶ms, 0);
assert!(matches!(
res,
Err(BixverseErrors::WNNModalitySampleMismatch)
));
}
#[test]
fn compute_wnn_errors_on_bad_k_nn() {
let (mat, knn, dist) = small_fixture();
let mod_a = ModalityInput::new(mat.as_ref(), &knn, &dist);
let mod_b = ModalityInput::new(mat.as_ref(), &knn, &dist);
let params = WnnParams {
k_nn: 10, knn_range: 2,
sigma_idx: 1,
s_nn: 2,
..Default::default()
};
let res = compute_wnn([mod_a, mod_b], ¶ms, 0);
assert!(matches!(res, Err(BixverseErrors::WNNKnnLargerThanKnnRange)));
}
}