use crate::errors::BixverseErrors;
use crate::single_cell::sc_processing::residuals::{
ResidualSource, intersect_gene_sets, validate_clip_range, validate_groups,
validate_residual_row_inputs,
};
use super::model::{
SctCellContext, SctGeneParams, SctModel, fill_residual_row, fill_residual_row_single,
};
#[derive(Clone, Debug)]
pub struct SctResiduals<'a> {
models: &'a [SctModel],
cells: SctCellContext<'a>,
genes: Vec<usize>,
positions: Vec<Vec<u32>>,
group_of_cell: Vec<u32>,
umi_scale: Option<Vec<f64>>,
}
impl<'a> SctResiduals<'a> {
pub fn new(
models: &'a [SctModel],
cells: SctCellContext<'a>,
group_of_cell: Vec<u32>,
) -> Result<Self, BixverseErrors> {
let n_groups = validate_groups(&group_of_cell, cells.n_cells())?;
if n_groups != models.len() {
return Err(BixverseErrors::ResidualGroupModelCountMismatch {
implied: n_groups,
models: models.len(),
});
}
for model in models {
if model.n_coef == 0 {
return Err(BixverseErrors::SctModelWithoutCoefficients);
}
if cells.covariates.n_covariates() + 1 != model.n_coef {
return Err(BixverseErrors::SctCovariateCountMismatch {
model: model.n_coef - 1,
supplied: cells.covariates.n_covariates(),
});
}
for (position, (model_name, supplied)) in model
.covariate_names
.iter()
.zip(&cells.covariates.names)
.enumerate()
{
if model_name != supplied {
return Err(BixverseErrors::SctCovariateNameMismatch {
position,
model: model_name.clone(),
supplied: supplied.clone(),
});
}
}
}
for model in models {
validate_clip_range(model.clip_range)?;
}
let sets: Vec<&[usize]> = models.iter().map(|m| m.genes.as_slice()).collect();
let genes = intersect_gene_sets(&sets)?;
let positions = models
.iter()
.map(|model| {
genes
.iter()
.map(|&g| {
model
.position(g)
.map(|p| p as u32)
.ok_or(BixverseErrors::SctGeneNotModelled { gene: g })
})
.collect::<Result<Vec<u32>, BixverseErrors>>()
})
.collect::<Result<Vec<_>, BixverseErrors>>()?;
let umi_scale = match models {
[model] => Some(
cells
.log10_umi
.iter()
.map(|&l| (model.log_umi_coef * l).exp())
.collect(),
),
_ => None,
};
Ok(Self {
models,
cells,
genes,
positions,
group_of_cell,
umi_scale,
})
}
pub fn single(
model: &'a SctModel,
cells: SctCellContext<'a>,
) -> Result<SctResiduals<'a>, BixverseErrors> {
let group_of_cell = vec![0_u32; cells.n_cells()];
Self::new(std::slice::from_ref(model), cells, group_of_cell)
}
pub fn models(&self) -> &[SctModel] {
self.models
}
pub fn cells(&self) -> &SctCellContext<'a> {
&self.cells
}
pub fn group_of_cell_slice(&self) -> &[u32] {
&self.group_of_cell
}
pub fn model_gene_position(&self, group: usize, gene_pos: usize) -> usize {
self.positions[group][gene_pos] as usize
}
}
impl ResidualSource for SctResiduals<'_> {
fn genes(&self) -> &[usize] {
&self.genes
}
fn n_cells(&self) -> usize {
self.cells.n_cells()
}
fn group_of_cell(&self) -> &[u32] {
&self.group_of_cell
}
fn n_groups(&self) -> usize {
self.models.len()
}
fn residual_row(
&self,
counts: &[f64],
indices: &[u32],
gene_pos: usize,
out: &mut [f32],
) -> Result<(), BixverseErrors> {
if gene_pos >= self.genes.len() {
return Err(BixverseErrors::SctGeneIndexOutOfRange {
index: gene_pos,
n_genes: self.genes.len(),
});
}
if out.len() != self.cells.n_cells() {
return Err(BixverseErrors::LengthMismatch {
name: "out",
expected: self.cells.n_cells(),
found: out.len(),
});
}
validate_residual_row_inputs(counts, indices, out.len())?;
let per_group: Vec<SctGeneParams<'_>> = self
.models
.iter()
.zip(&self.positions)
.map(|(model, pos)| SctGeneParams::new(model, pos[gene_pos] as usize))
.collect();
if let [gene] = per_group.as_slice() {
fill_residual_row_single(
counts,
indices,
&self.cells,
self.umi_scale.as_deref(),
out,
gene,
);
} else {
fill_residual_row(counts, indices, &self.cells, out, |c| {
&per_group[self.group_of_cell[c] as usize]
});
}
Ok(())
}
}