use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use crate::prelude::*;
fn compute_initial_connectivity(
incoming: &CompressedSparseData2<f32, f32>,
seed_of_cells: &[i32],
) -> Vec<i32> {
let n = seed_of_cells.len();
let mut connectivity = vec![0i32; n];
for node in 0..n {
if seed_of_cells[node] >= 0 {
continue;
}
let start = incoming.indptr[node] as usize;
let end = incoming.indptr[node + 1] as usize;
let mut c = 0i32;
for idx in start..end {
if seed_of_cells[incoming.indices[idx] as usize] < 0 {
c += 1;
}
}
connectivity[node] = c;
}
connectivity
}
fn retain_connected(candidates: &mut Vec<usize>, connectivity: &[i32]) -> bool {
candidates.retain(|&node| connectivity[node] > 0);
!candidates.is_empty()
}
fn choose_seed_node(
candidates: &[usize],
connectivity: &[i32],
min_q: f32,
max_q: f32,
rng: &mut StdRng,
) -> usize {
let size = candidates.len();
debug_assert!(size > 0);
let min_rank = ((size as f32 - 1.0) * min_q).floor() as usize;
let max_rank = ((size as f32 - 1.0) * max_q).ceil() as usize;
debug_assert!(min_rank <= max_rank && max_rank < size);
let mut positions: Vec<usize> = (0..size).collect();
let key = |pos: &usize| connectivity[candidates[*pos]];
positions.select_nth_unstable_by_key(min_rank, &key);
if max_rank > min_rank {
let (_lo, _mid, _hi) =
positions[min_rank..].select_nth_unstable_by_key(max_rank - min_rank, &key);
}
let band_len = max_rank - min_rank + 1;
let pick = rng.random_range(0..band_len);
candidates[positions[min_rank + pick]]
}
fn claim_seed(
outgoing: &CompressedSparseData2<f32, f32>,
incoming: &CompressedSparseData2<f32, f32>,
seed_id: usize,
seed_node: usize,
mean_seed_size: usize,
seed_of_cells: &mut [i32],
connectivity: &mut [i32],
) {
let in_start = incoming.indptr[seed_node] as usize;
let in_end = incoming.indptr[seed_node + 1] as usize;
let mut neighbours: Vec<(u32, f32)> = (in_start..in_end)
.filter_map(|idx| {
let other = incoming.indices[idx];
if seed_of_cells[other as usize] < 0 {
Some((other, incoming.data[idx]))
} else {
None
}
})
.collect();
if neighbours.len() > mean_seed_size {
neighbours.select_nth_unstable_by(mean_seed_size, |a, b| {
b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
});
neighbours.truncate(mean_seed_size);
}
debug_assert!(seed_of_cells[seed_node] < 0);
seed_of_cells[seed_node] = seed_id as i32;
connectivity[seed_node] = 0;
for &(node, _) in &neighbours {
seed_of_cells[node as usize] = seed_id as i32;
connectivity[node as usize] = 0;
}
decrement_connectivity_for_outgoing_into(outgoing, seed_node, seed_of_cells, connectivity);
for &(node, _) in &neighbours {
decrement_connectivity_for_outgoing_into(
outgoing,
node as usize,
seed_of_cells,
connectivity,
);
}
}
fn decrement_connectivity_for_outgoing_into(
outgoing: &CompressedSparseData2<f32, f32>,
origin: usize,
seed_of_cells: &[i32],
connectivity: &mut [i32],
) {
let start = outgoing.indptr[origin] as usize;
let end = outgoing.indptr[origin + 1] as usize;
for idx in start..end {
let other = outgoing.indices[idx] as usize;
if seed_of_cells[other] < 0 {
connectivity[other] = (connectivity[other] - 1).max(0);
}
}
}
fn degree_product(
outgoing: &CompressedSparseData2<f32, f32>,
incoming: &CompressedSparseData2<f32, f32>,
node: usize,
) -> u64 {
let deg_out = (outgoing.indptr[node + 1] - outgoing.indptr[node]) as u64;
let deg_in = (incoming.indptr[node + 1] - incoming.indptr[node]) as u64;
(deg_in + 1) * (deg_out + 1)
}
fn complete_seeds(
outgoing: &CompressedSparseData2<f32, f32>,
incoming: &CompressedSparseData2<f32, f32>,
seed_of_cells: &mut [i32],
seeds_count: usize,
rng: &mut StdRng,
) {
if seeds_count == 0 {
return;
}
let n = seed_of_cells.len();
let mut seed_sizes = vec![0i32; seeds_count];
for &s in seed_of_cells.iter() {
if s >= 0 {
seed_sizes[s as usize] += 1;
}
}
let mut weights = vec![0f64; seeds_count];
let mut disconnected: Vec<usize> = Vec::new();
for node in 0..n {
if seed_of_cells[node] >= 0 {
continue;
}
if !try_connect_node(
outgoing,
incoming,
node,
seed_of_cells,
&mut seed_sizes,
&mut weights,
rng,
) {
disconnected.push(node);
}
}
while !disconnected.is_empty() {
let prev_len = disconnected.len();
let old = std::mem::take(&mut disconnected);
for node in old {
if !try_connect_node(
outgoing,
incoming,
node,
seed_of_cells,
&mut seed_sizes,
&mut weights,
rng,
) {
disconnected.push(node);
}
}
debug_assert!(
disconnected.len() < prev_len,
"complete_seeds did not converge: {} disconnected nodes remain",
disconnected.len()
);
}
}
fn try_connect_node(
outgoing: &CompressedSparseData2<f32, f32>,
incoming: &CompressedSparseData2<f32, f32>,
node: usize,
seed_of_cells: &mut [i32],
seed_sizes: &mut [i32],
weights: &mut [f64],
rng: &mut StdRng,
) -> bool {
weights.fill(0.0);
let mut total = 0f64;
for (mat, sign_label) in [(incoming, "in"), (outgoing, "out")] {
let _ = sign_label; let start = mat.indptr[node] as usize;
let end = mat.indptr[node + 1] as usize;
for idx in start..end {
let other = mat.indices[idx] as usize;
let other_seed = seed_of_cells[other];
if other_seed >= 0 {
let w = (mat.data[idx] as f64) / (seed_sizes[other_seed as usize] as f64);
weights[other_seed as usize] += w;
total += w;
}
}
}
if total <= 0.0 {
return false;
}
let mut pick = rng.random_range(0.0..total);
for (seed_id, &w) in weights.iter().enumerate() {
pick -= w;
if pick <= 0.0 {
seed_of_cells[node] = seed_id as i32;
seed_sizes[seed_id] += 1;
return true;
}
}
let (best, _) = weights
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
.expect("weights non-empty given seeds_count > 0");
seed_of_cells[node] = best as i32;
seed_sizes[best] += 1;
true
}
pub fn seeds_count_for(
total_size: usize,
total_umis: f64,
min_metacell_size: usize,
max_metacell_size: usize,
min_metacell_umis: f64,
max_metacell_umis: f64,
) -> usize {
let ceil_div = |a: f64, b: f64| (a / b).ceil() as i64;
let min_by_umis = ceil_div(total_umis, max_metacell_umis);
let max_by_umis = ceil_div(total_umis, min_metacell_umis.max(1.0));
let min_by_size = ceil_div(total_size as f64, max_metacell_size as f64);
let max_by_size = ceil_div(total_size as f64, min_metacell_size.max(1) as f64);
let count = if max_by_size < min_by_umis {
(max_by_size + min_by_umis + 1) / 2
} else if max_by_umis < min_by_size {
(max_by_umis + min_by_size + 1) / 2
} else {
let lo = min_by_size.max(min_by_umis);
let hi = max_by_size.min(max_by_umis);
(lo + hi + 1) / 2
};
(count.max(1)) as usize
}
pub fn choose_seeds(
outgoing: &CompressedSparseData2<f32, f32>,
incoming: &CompressedSparseData2<f32, f32>,
seed_of_cells: &mut [i32],
max_seeds_count: usize,
min_seed_size_quantile: f32,
max_seed_size_quantile: f32,
random_seed: u64,
) -> usize {
let n = seed_of_cells.len();
assert_eq!(outgoing.shape, (n, n), "outgoing shape mismatch");
assert_eq!(incoming.shape, (n, n), "incoming shape mismatch");
assert!(
(0.0..=1.0).contains(&min_seed_size_quantile)
&& (0.0..=1.0).contains(&max_seed_size_quantile)
&& min_seed_size_quantile <= max_seed_size_quantile,
"invalid seed-size quantiles"
);
let given_seeds_count = seed_of_cells.iter().copied().max().unwrap_or(-1).max(-1) as i64 + 1;
let mut seeds_count = given_seeds_count as usize;
let mut rng = StdRng::seed_from_u64(random_seed);
if seeds_count < max_seeds_count {
let mut connectivity = compute_initial_connectivity(incoming, seed_of_cells);
let mut candidates: Vec<usize> = (0..n).filter(|&i| seed_of_cells[i] < 0).collect();
let unseeded = candidates.len();
let to_create = max_seeds_count - seeds_count;
let mean_seed_size = if to_create == 0 {
1
} else {
((unseeded as f64) / (to_create as f64)).ceil() as usize
}
.max(1);
while seeds_count < max_seeds_count && retain_connected(&mut candidates, &connectivity) {
let seed_node = choose_seed_node(
&candidates,
&connectivity,
min_seed_size_quantile,
max_seed_size_quantile,
&mut rng,
);
claim_seed(
outgoing,
incoming,
seeds_count,
seed_node,
mean_seed_size,
seed_of_cells,
&mut connectivity,
);
seeds_count += 1;
}
if seeds_count < max_seeds_count {
let mut leftover: Vec<usize> = (0..n).filter(|&i| seed_of_cells[i] < 0).collect();
leftover.sort_unstable_by(|&a, &b| {
let da = degree_product(outgoing, incoming, a);
let db = degree_product(outgoing, incoming, b);
db.cmp(&da)
});
for node in leftover.into_iter() {
if seeds_count >= max_seeds_count {
break;
}
seed_of_cells[node] = seeds_count as i32;
seeds_count += 1;
}
}
}
complete_seeds(outgoing, incoming, seed_of_cells, seeds_count, &mut rng);
seeds_count
}
#[cfg(test)]
mod tests {
use super::*;
use bixverse_core::core::math::sparse::CompressedSparseFormat;
fn make_graph(
rows: Vec<usize>,
cols: Vec<usize>,
vals: Vec<f32>,
n: usize,
) -> (
CompressedSparseData2<f32, f32>,
CompressedSparseData2<f32, f32>,
) {
let mut entries: Vec<(usize, usize, f32)> = rows
.into_iter()
.zip(cols)
.zip(vals)
.map(|((r, c), v)| (r, c, v))
.collect();
entries.sort_unstable_by_key(|a| (a.0, a.1));
let mut data = Vec::with_capacity(entries.len());
let mut indices = Vec::with_capacity(entries.len());
let mut indptr = vec![0usize; n + 1];
for &(r, c, v) in &entries {
data.push(v);
indices.push(c);
indptr[r + 1] += 1;
}
for i in 0..n {
indptr[i + 1] += indptr[i];
}
let outgoing = CompressedSparseData2 {
data,
indices: indices.index_cast(),
indptr: indptr.index_cast(),
cs_type: CompressedSparseFormat::Csr,
data_2: None,
shape: (n, n),
};
let incoming = bixverse_core::core::math::sparse::transpose_sparse(&outgoing);
let incoming = CompressedSparseData2 {
data: incoming.data,
indices: incoming.indices,
indptr: incoming.indptr,
cs_type: CompressedSparseFormat::Csr,
data_2: incoming.data_2,
shape: incoming.shape,
};
(outgoing, incoming)
}
#[test]
fn seeds_count_for_basic() {
let n = seeds_count_for(1000, 100_000.0, 50, 150, 5_000.0, 15_000.0);
assert_eq!(n, 14);
assert!(seeds_count_for(1, 1.0, 1, 2, 1.0, 2.0) >= 1);
}
#[test]
fn complete_assignment_after_phase3() {
let (out, inc) = make_graph(
vec![0, 1, 2, 3],
vec![1, 2, 3, 0],
vec![1.0, 1.0, 1.0, 1.0],
4,
);
let mut seeds = vec![-1i32; 4];
let count = choose_seeds(&out, &inc, &mut seeds, 2, 0.0, 1.0, 42);
assert!(
seeds
.iter()
.copied()
.all(|s| (0..count as i32).contains(&s))
);
}
#[test]
fn respects_partial_input() {
let (out, inc) = make_graph(
vec![0, 1, 2, 3, 0, 1, 2, 3],
vec![1, 2, 3, 0, 2, 3, 0, 1],
vec![1.0; 8],
4,
);
let mut seeds = vec![-1i32; 4];
seeds[0] = 0;
let count = choose_seeds(&out, &inc, &mut seeds, 2, 0.0, 1.0, 7);
assert_eq!(seeds[0], 0);
assert!(seeds.iter().all(|&s| s >= 0));
assert!(count >= 1);
}
#[test]
fn deterministic_under_same_seed() {
let (out, inc) = make_graph(
(0..6).flat_map(|i| [i, i]).collect(),
(0..6)
.flat_map(|i| vec![(i + 1) % 6, (i + 2) % 6])
.collect(),
vec![1.0; 12],
6,
);
let mut a = vec![-1i32; 6];
let mut b = vec![-1i32; 6];
let ca = choose_seeds(&out, &inc, &mut a, 3, 0.0, 1.0, 99);
let cb = choose_seeds(&out, &inc, &mut b, 3, 0.0, 1.0, 99);
assert_eq!(ca, cb);
assert_eq!(a, b);
}
}