use faer::Mat;
use rayon::prelude::*;
use crate::core::math::sparse::transpose_sparse;
use crate::prelude::*;
use super::params::SelectParams;
use super::pile::Pile;
const TOP_N: usize = 3;
const RELAX_STEP: f32 = 1.0 / 8.0;
const RELAX_FLOOR: f32 = -10.0;
pub fn select_features(pile: &mut Pile, params: &SelectParams) {
let downsampled = pile
.downsampled
.as_ref()
.expect("downsample_pile must be called before select_features");
let n_genes = downsampled.shape.1;
let total_per_gene = sum_columns_csr(downsampled);
let top_n_per_gene = top_n_per_column(downsampled, TOP_N);
let (mean_per_gene, log_norm_var_per_gene) = mean_and_log_norm_var(downsampled);
let rel_var_per_gene = relative_variance(
&log_norm_var_per_gene,
&mean_per_gene,
params.relative_variance_window_size,
);
let high_total = params.min_gene_total.map(|t| {
total_per_gene
.iter()
.map(|&v| v >= t)
.collect::<Vec<bool>>()
});
let high_top_n = params.min_gene_top3.map(|t| {
top_n_per_gene
.iter()
.map(|&v| v >= t)
.collect::<Vec<bool>>()
});
let lateral = params.lateral_gene_mask.as_deref();
let try_threshold = |t: f32| -> (Vec<bool>, usize) {
let var_mask: Vec<bool> = rel_var_per_gene.iter().map(|&v| v >= t).collect();
let combined = combine_masks(
n_genes,
high_total.as_deref(),
high_top_n.as_deref(),
Some(&var_mask),
lateral,
);
let count = combined.iter().filter(|&&b| b).count();
(combined, count)
};
let (mut mask, mut count) = match params.min_gene_relative_variance {
Some(t) => try_threshold(t),
None => {
let m = combine_masks(
n_genes,
high_total.as_deref(),
high_top_n.as_deref(),
None,
lateral,
);
let c = m.iter().filter(|&&b| b).count();
(m, c)
}
};
if count < params.min_genes
&& let Some(t0) = params.min_gene_relative_variance
{
let mut t = t0;
while count < params.min_genes && t > RELAX_FLOOR {
t -= RELAX_STEP;
let (m, c) = try_threshold(t);
mask = m;
count = c;
}
}
if count < params.min_genes {
mask = combine_masks(
n_genes,
high_total.as_deref(),
high_top_n.as_deref(),
None,
lateral,
);
}
let selected: Vec<usize> = mask
.iter()
.enumerate()
.filter_map(|(i, &b)| if b { Some(i) } else { None })
.collect();
let dense = extract_dense_columns(downsampled, &selected);
pile.selected_gene_indices = Some(selected);
pile.selected_dense = Some(dense);
}
fn sum_columns_csr(mat: &CompressedSparseData2<u32, f32>) -> Vec<u32> {
let n_cols = mat.shape.1;
let mut sums = vec![0u32; n_cols];
for (idx, &col) in mat.indices.iter().enumerate() {
sums[col as usize] = sums[col as usize].saturating_add(mat.data[idx]);
}
sums
}
fn top_n_per_column(mat: &CompressedSparseData2<u32, f32>, n: usize) -> Vec<u32> {
debug_assert!(mat.cs_type.is_csr());
let csc = transpose_sparse(mat);
let n_genes = mat.shape.1;
let csc_data = &csc.data;
let csc_indptr = &csc.indptr;
(0..n_genes)
.into_par_iter()
.map(|col| {
let start = csc_indptr[col] as usize;
let end = csc_indptr[col + 1] as usize;
let nnz = end - start;
if nnz < n {
return 0u32;
}
let mut buf: Vec<u32> = csc_data[start..end].to_vec();
let pos = nnz - n;
let (_, mid, _) = buf.select_nth_unstable(pos);
*mid
})
.collect()
}
fn mean_and_log_norm_var(mat: &CompressedSparseData2<u32, f32>) -> (Vec<f32>, Vec<f32>) {
let n_rows = mat.shape.0 as f64;
let n_cols = mat.shape.1;
let mut sum = vec![0.0f64; n_cols];
let mut sum_sq = vec![0.0f64; n_cols];
for (idx, &col) in mat.indices.iter().enumerate() {
let v = mat.data[idx] as f64;
sum[col as usize] += v;
sum_sq[col as usize] += v * v;
}
let mut mean = vec![0.0f32; n_cols];
let mut log_norm_var = vec![0.0f32; n_cols];
if n_rows == 0.0 {
return (mean, log_norm_var);
}
for col in 0..n_cols {
let m = sum[col] / n_rows;
mean[col] = m as f32;
if m == 0.0 {
log_norm_var[col] = 0.0; continue;
}
let var = (sum_sq[col] / n_rows - m * m).max(0.0);
let norm_var = var / m;
log_norm_var[col] = if norm_var > 0.0 {
(norm_var as f32).log2()
} else {
f32::NEG_INFINITY
};
}
(mean, log_norm_var)
}
fn relative_variance(log_norm_var: &[f32], mean: &[f32], window_size: usize) -> Vec<f32> {
let n = log_norm_var.len();
let mut out = vec![0.0f32; n];
if n == 0 {
return out;
}
let w = if window_size.is_multiple_of(2) {
window_size + 1
} else {
window_size
};
let half = w / 2;
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| {
mean[a]
.partial_cmp(&mean[b])
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut buf: Vec<f32> = Vec::with_capacity(w);
for sort_pos in 0..n {
let lo = sort_pos.saturating_sub(half);
let hi = (sort_pos + half + 1).min(n);
buf.clear();
for k in lo..hi {
buf.push(log_norm_var[order[k]]);
}
let med = median_inplace(&mut buf);
let original_idx = order[sort_pos];
out[original_idx] = log_norm_var[original_idx] - med;
}
out
}
fn median_inplace(values: &mut [f32]) -> f32 {
if values.is_empty() {
return 0.0;
}
let cmp = |a: &f32, b: &f32| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal);
let n = values.len();
if n % 2 == 1 {
let mid = n / 2;
values.select_nth_unstable_by(mid, cmp);
values[mid]
} else {
let upper = n / 2;
values.select_nth_unstable_by(upper, cmp);
let upper_val = values[upper];
let mut lower_val = f32::NEG_INFINITY;
for &v in &values[..upper] {
if v > lower_val {
lower_val = v;
}
}
(lower_val + upper_val) / 2.0
}
}
fn combine_masks(
n: usize,
a: Option<&[bool]>,
b: Option<&[bool]>,
c: Option<&[bool]>,
lateral: Option<&[bool]>,
) -> Vec<bool> {
let mut out = vec![true; n];
for m in [a, b, c].iter().flatten() {
for (o, &v) in out.iter_mut().zip(m.iter()) {
*o &= v;
}
}
if let Some(l) = lateral {
for (o, &v) in out.iter_mut().zip(l.iter()) {
*o &= !v;
}
}
out
}
fn extract_dense_columns(csr: &CompressedSparseData2<u32, f32>, selected: &[usize]) -> Mat<f32> {
let n_rows = csr.shape.0;
let n_sel = selected.len();
let mut gene_to_pos = vec![None; csr.shape.1];
for (pos, &g) in selected.iter().enumerate() {
gene_to_pos[g] = Some(pos);
}
let csc = transpose_sparse(csr);
let mut dense = Mat::<f32>::zeros(n_rows, n_sel);
dense
.par_col_iter_mut()
.enumerate()
.for_each(|(pos, mut col)| {
let g = selected[pos];
if gene_to_pos[g] != Some(pos) {
return;
}
let start = csc.indptr[g] as usize;
let end = csc.indptr[g + 1] as usize;
for idx in start..end {
col[csc.indices[idx] as usize] = csc.data[idx] as f32;
}
});
dense
}