use rayon::prelude::*;
use rustc_hash::{FxHashMap, FxHashSet};
use crate::prelude::*;
pub const CXDS_NTOP: usize = 500;
const CXDS_READ_BLOCK: usize = 50_000;
const CXDS_NO_GENE: u16 = u16::MAX;
type CellGeneSet = Vec<u16>;
#[inline]
fn pair_score_sum(pair_scores: &[f32], ntop: usize, expressed: &[u16]) -> f32 {
let mut total = 0.0f32;
for (a, &gi) in expressed.iter().enumerate() {
let i = gi as usize;
let row = &pair_scores[i * ntop..(i + 1) * ntop];
let rest = &expressed[a + 1..];
let mut acc = [0.0f32; 4];
let mut quads = rest.chunks_exact(4);
for q in &mut quads {
for (slot, &j) in acc.iter_mut().zip(q) {
*slot += row[j as usize];
}
}
let mut tail = 0.0f32;
for &j in quads.remainder() {
tail += row[j as usize];
}
total += (acc[0] + acc[1]) + (acc[2] + acc[3]) + tail;
}
total
}
fn norm_cdf(x: f64) -> f64 {
if x < -8.0 {
return 0.0;
}
if x > 8.0 {
return 1.0;
}
let t = 1.0 / (1.0 + 0.231_641_9 * x.abs());
let poly = t
* (0.319_381_530
+ t * (-0.356_563_782
+ t * (1.781_477_937 + t * (-1.821_255_978 + t * 1.330_274_429))));
let pdf = (-x * x / 2.0).exp() * std::f64::consts::FRAC_2_SQRT_PI * 0.5;
let tail = pdf * poly;
if x >= 0.0 { 1.0 - tail } else { tail }
}
fn neg_log_binom_upper(k: u32, n: u32, p: f64) -> f32 {
let np = n as f64 * p;
let var = np * (1.0 - p);
if var < 1e-10 {
return 0.0;
}
let z = (k as f64 - 0.5 - np) / var.sqrt();
let tail_p = 1.0 - norm_cdf(z);
if tail_p < 1e-300 {
return 700.0; }
(-tail_p.ln()) as f32
}
#[allow(dead_code)]
pub struct CxdsModel {
pair_scores: Vec<f32>,
selected_genes: Vec<usize>,
ntop: usize,
gene_map: FxHashMap<usize, u16>,
}
impl CxdsModel {
pub fn fit<S: SingleCellReading>(
reader: &S,
cells_to_keep: &[usize],
hvg_genes: &[usize],
ntop: usize,
) -> Result<(Self, Vec<CellGeneSet>), ScErrors> {
let n_cells = cells_to_keep.len();
let hvg_set: FxHashSet<usize> = hvg_genes.iter().copied().collect();
let mut gene_counts: FxHashMap<usize, u32> = FxHashMap::default();
let mut cell_hvg_indices: Vec<Vec<usize>> = Vec::with_capacity(n_cells);
for block in cells_to_keep.chunks(CXDS_READ_BLOCK) {
let chunks = reader.read_cells_parallel(block)?;
let expressed: Vec<Vec<usize>> = chunks
.par_iter()
.map(|chunk| {
chunk
.indices
.iter()
.enumerate()
.filter(|&(i, &g)| {
hvg_set.contains(&(g as usize)) && chunk.data_raw.get(i) > 0
})
.map(|(_, &g)| g as usize)
.collect()
})
.collect();
for hvgs in expressed {
for &gi in &hvgs {
*gene_counts.entry(gi).or_insert(0) += 1;
}
cell_hvg_indices.push(hvgs);
}
}
let nf = n_cells as f64;
let mut gene_vars: Vec<(usize, f64)> = gene_counts
.iter()
.map(|(&gene, &count)| {
let p = count as f64 / nf;
(gene, p * (1.0 - p))
})
.collect();
gene_vars.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
let actual_ntop = ntop.min(gene_vars.len());
let selected_genes: Vec<usize> = gene_vars[..actual_ntop].iter().map(|&(g, _)| g).collect();
let gene_map: FxHashMap<usize, u16> = selected_genes
.iter()
.enumerate()
.map(|(pos, &g)| (g, pos as u16))
.collect();
let marginals: Vec<f64> = selected_genes
.iter()
.map(|g| *gene_counts.get(g).unwrap_or(&0) as f64 / nf)
.collect();
let obs_gene_sets: Vec<CellGeneSet> = cell_hvg_indices
.par_iter()
.map(|hvgs| {
let mut expressed: CellGeneSet = hvgs
.iter()
.filter_map(|&gi| gene_map.get(&gi).copied())
.collect();
expressed.sort_unstable();
expressed
})
.collect();
drop(cell_hvg_indices);
let n_coexpress = obs_gene_sets
.par_iter()
.fold(
|| vec![0u32; actual_ntop * actual_ntop],
|mut local_coex, expressed| {
let ne = expressed.len();
for a in 0..ne {
let i = expressed[a] as usize;
let row = i * actual_ntop;
for b in (a + 1)..ne {
let j = expressed[b] as usize;
local_coex[row + j] += 1;
}
}
local_coex
},
)
.reduce(
|| vec![0u32; actual_ntop * actual_ntop],
|mut a, b| {
for i in 0..a.len() {
a[i] += b[i];
}
a
},
);
let n_express: Vec<u32> = selected_genes
.iter()
.map(|g| *gene_counts.get(g).unwrap_or(&0))
.collect();
let n = n_cells as u32;
let mut pair_scores = vec![0.0f32; actual_ntop * actual_ntop];
for i in 0..actual_ntop {
for j in (i + 1)..actual_ntop {
let pi = marginals[i];
let pj = marginals[j];
let p_excl = pi * (1.0 - pj) + (1.0 - pi) * pj;
let coex = n_coexpress[i * actual_ntop + j];
let k = n_express[i] + n_express[j] - 2 * coex;
let score = neg_log_binom_upper(k, n, p_excl);
pair_scores[i * actual_ntop + j] = score;
pair_scores[j * actual_ntop + i] = score;
}
}
let model = CxdsModel {
pair_scores,
selected_genes,
ntop: actual_ntop,
gene_map,
};
Ok((model, obs_gene_sets))
}
pub fn score(&self, gene_sets: &[CellGeneSet]) -> Vec<f32> {
gene_sets
.par_iter()
.map(|expressed| pair_score_sum(&self.pair_scores, self.ntop, expressed))
.collect()
}
pub fn score_simulated_from_chunks(
&self,
sim_chunks: &[CsrCellChunk],
selected_genes_to_cxds_pos: &FxHashMap<usize, u16>,
bin_thresh: u32,
) -> Vec<f32> {
let table_len = selected_genes_to_cxds_pos
.keys()
.max()
.map_or(0, |&m| m + 1);
let mut pos_table = vec![CXDS_NO_GENE; table_len];
for (&gene, &pos) in selected_genes_to_cxds_pos {
pos_table[gene] = pos;
}
sim_chunks
.par_iter()
.map(|chunk| {
let mut expressed: Vec<u16> = Vec::new();
for i in 0..chunk.indices.len() {
let raw = chunk.data_raw.get(i);
if raw < bin_thresh {
continue;
}
let cxds_pos = pos_table
.get(chunk.indices[i] as usize)
.copied()
.unwrap_or(CXDS_NO_GENE);
if cxds_pos != CXDS_NO_GENE {
expressed.push(cxds_pos);
}
}
expressed.sort_unstable();
pair_score_sum(&self.pair_scores, self.ntop, &expressed)
})
.collect()
}
pub fn hvg_position_map(&self, selected_genes: &[usize]) -> FxHashMap<usize, u16> {
selected_genes
.iter()
.enumerate()
.filter_map(|(hvg_pos, orig_gene)| {
self.gene_map
.get(orig_gene)
.map(|&cxds_pos| (hvg_pos, cxds_pos))
})
.collect()
}
}