use ann_search_rs::utils::dist::euclidean_distance_static;
use faer::{Mat, MatRef};
use rayon::prelude::*;
use crate::prelude::*;
use crate::single_cell::sc_processing::knn::{KnnParams, generate_knn_with_dist};
use crate::single_cell::mc_generation::seacells::{
compute_diffusion_kernel, determine_multiscale_space, diffusion_map_from_kernel,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DensityRegion {
Low,
Mid,
High,
}
pub struct DiffusionDensity {
pub dcs: Mat<f32>,
pub density_distances: Vec<f32>,
pub regions: Vec<DensityRegion>,
}
fn classify_density_regions(dist: &[f32]) -> Vec<DensityRegion> {
let n = dist.len();
let mut sorted: Vec<(usize, f32)> = dist.iter().copied().enumerate().collect();
sorted.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
let q1 = n / 4;
let q3 = 3 * n / 4;
let mut regions = vec![DensityRegion::Mid; n];
for &(idx, _) in &sorted[..q1] {
regions[idx] = DensityRegion::High;
}
for &(idx, _) in &sorted[q3..] {
regions[idx] = DensityRegion::Low;
}
regions
}
#[allow(clippy::too_many_arguments)]
pub fn compute_diffusion_density(
knn_indices: &[Vec<usize>],
knn_distances: &[Vec<f32>],
n_dcs: usize,
k_density: usize,
knn_params: &KnnParams,
seed: u64,
verbose: usize,
) -> Result<DiffusionDensity, BixverseErrors> {
let n = knn_indices.len();
let verbosity = parse_verbosity_level(verbose);
if verbosity.normal_verbosity() {
println!("Building diffusion kernel...");
}
let mut kernel = compute_diffusion_kernel(knn_indices, knn_distances)?;
if verbosity.normal_verbosity() {
println!("Computing top {} diffusion components...", n_dcs);
}
let (evals, evecs) = diffusion_map_from_kernel(&mut kernel, n_dcs + 1, seed, None)?;
let dcs = determine_multiscale_space(&evals, &evecs, Some(n_dcs + 1));
let dim = dcs.first().map_or(0, Vec::len);
if dim == 0 {
return Err(BixverseErrors::InvalidArgument(
"Diffusion density: too few metacells to build a diffusion embedding".to_string(),
));
}
if verbosity.normal_verbosity() {
println!("Running kNN on DC embedding (k = {})...", k_density);
}
let dc_mat = Mat::<f32>::from_fn(n, dim, |i, j| dcs[i][j]);
let mut params = knn_params.clone();
params.k = k_density.min(n.saturating_sub(1)).max(1);
params.ann_dist = "euclidean".to_string();
let (_, dc_distances) = generate_knn_with_dist(
dc_mat.as_ref(),
¶ms,
true,
false,
seed as usize,
verbosity.detailed_verbosity(),
)?;
let dc_distances = dc_distances.expect("distances must be returned");
let density_distances: Vec<f32> = dc_distances
.iter()
.map(|d| d.iter().copied().fold(0.0f32, f32::max))
.collect();
let regions = classify_density_regions(&density_distances);
Ok(DiffusionDensity {
dcs: dc_mat,
density_distances,
regions,
})
}
pub fn compute_compactness(dcs: MatRef<f32>, metacells: &[Vec<usize>]) -> Vec<f32> {
let d = dcs.ncols();
metacells
.par_iter()
.map(|cells| {
if cells.is_empty() {
return f32::NAN;
}
let n = cells.len() as f32;
let mut total_var = 0.0f32;
for k in 0..d {
let mut mean = 0.0f32;
for &i in cells {
mean += dcs[(i, k)];
}
mean /= n;
let mut var = 0.0f32;
for &i in cells {
let diff = dcs[(i, k)] - mean;
var += diff * diff;
}
total_var += var / n;
}
total_var / d as f32
})
.collect()
}
pub fn compute_separation(dcs: MatRef<f32>, metacells: &[Vec<usize>]) -> Vec<f32> {
let d = dcs.ncols();
let k = metacells.len();
let centroids: Vec<Vec<f32>> = metacells
.par_iter()
.map(|cells| {
if cells.is_empty() {
return Vec::new();
}
let n = cells.len() as f32;
let mut centroid = vec![0.0f32; d];
for &i in cells {
for j in 0..d {
centroid[j] += dcs[(i, j)];
}
}
for v in &mut centroid {
*v /= n;
}
centroid
})
.collect();
(0..k)
.into_par_iter()
.map(|i| {
if centroids[i].is_empty() {
return f32::NAN;
}
let mut min_dist_sq = f32::INFINITY;
for j in 0..k {
if i == j || centroids[j].is_empty() {
continue;
}
let dist_sq = euclidean_distance_static(¢roids[i], ¢roids[j]);
if dist_sq < min_dist_sq {
min_dist_sq = dist_sq;
}
}
min_dist_sq.sqrt()
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn line_dcs() -> Mat<f32> {
let xs = [0.0f32, 2.0, 6.0, 10.0];
Mat::<f32>::from_fn(4, 2, |i, j| if j == 0 { xs[i] } else { 0.0 })
}
#[test]
fn compute_compactness_averages_variance_across_dcs() {
let dcs = line_dcs();
let metacells = vec![vec![0, 1], vec![2, 3]];
let compactness = compute_compactness(dcs.as_ref(), &metacells);
assert_relative_eq!(compactness[0], 0.5, epsilon = 1e-6);
assert_relative_eq!(compactness[1], 2.0, epsilon = 1e-6);
}
#[test]
fn compute_compactness_is_zero_for_a_singleton_metacell() {
let dcs = line_dcs();
let compactness = compute_compactness(dcs.as_ref(), &[vec![3]]);
assert_relative_eq!(compactness[0], 0.0, epsilon = 1e-6);
}
#[test]
fn compute_compactness_yields_nan_for_empty_metacells() {
let dcs = line_dcs();
let metacells = vec![vec![0, 1], vec![], vec![2, 3]];
let compactness = compute_compactness(dcs.as_ref(), &metacells);
assert_eq!(compactness.len(), 3);
assert!(compactness[1].is_nan());
assert!(compactness[0].is_finite() && compactness[2].is_finite());
}
#[test]
fn compute_separation_returns_the_nearest_centroid_distance() {
let dcs = line_dcs();
let metacells = vec![vec![0, 1], vec![2, 3]];
let separation = compute_separation(dcs.as_ref(), &metacells);
assert_relative_eq!(separation[0], 7.0, epsilon = 1e-5);
assert_relative_eq!(separation[1], 7.0, epsilon = 1e-5);
}
#[test]
fn compute_separation_picks_the_closest_other_centroid() {
let dcs = line_dcs();
let metacells = vec![vec![0, 1], vec![2, 3], vec![3]];
let separation = compute_separation(dcs.as_ref(), &metacells);
assert_relative_eq!(separation[0], 7.0, epsilon = 1e-5);
assert_relative_eq!(separation[1], 2.0, epsilon = 1e-5);
assert_relative_eq!(separation[2], 2.0, epsilon = 1e-5);
}
#[test]
fn compute_separation_yields_nan_for_empty_metacells() {
let dcs = line_dcs();
let metacells = vec![vec![0, 1], vec![], vec![2, 3]];
let separation = compute_separation(dcs.as_ref(), &metacells);
assert!(separation[1].is_nan());
assert_relative_eq!(separation[0], 7.0, epsilon = 1e-5);
assert_relative_eq!(separation[2], 7.0, epsilon = 1e-5);
}
#[test]
fn compute_separation_yields_infinity_for_a_single_metacell() {
let dcs = line_dcs();
let separation = compute_separation(dcs.as_ref(), &[vec![0, 1]]);
assert!(separation[0].is_infinite() && separation[0].is_sign_positive());
}
#[test]
fn classify_density_regions_splits_on_quartiles() {
let dist = [5.0f32, 1.0, 7.0, 0.0, 3.0, 6.0, 2.0, 4.0];
let regions = classify_density_regions(&dist);
use DensityRegion::*;
assert_eq!(regions, vec![Mid, High, Low, High, Mid, Low, Mid, Mid]);
}
#[test]
fn classify_density_regions_handles_tiny_inputs() {
let regions = classify_density_regions(&[1.0, 0.0]);
assert_eq!(regions, vec![DensityRegion::Low, DensityRegion::Mid]);
assert!(classify_density_regions(&[]).is_empty());
}
#[test]
fn compute_diffusion_density_rejects_a_zero_width_embedding() {
let knn_indices = vec![vec![0]];
let knn_distances = vec![vec![0.0f32]];
let result = compute_diffusion_density(
&knn_indices,
&knn_distances,
10,
5,
&KnnParams::default(),
42,
0,
);
assert!(matches!(result, Err(BixverseErrors::InvalidArgument(_))));
}
}