use faer::Mat;
use half::f16;
use indexmap::IndexSet;
use rayon::prelude::*;
use std::time::Instant;
use crate::core::math::pca_svd::randomised_sparse_svd;
use crate::core::math::pca_svd::*;
use crate::core::math::sparse::sparse_svd_lanczos;
use crate::prelude::*;
pub type SingleCellPcaResScaled =
Result<(Mat<f32>, Mat<f32>, Vec<f32>, Option<Mat<f32>>), BixverseErrors>;
pub type SingleCellPcaResScaledStats = Result<
(
Mat<f32>,
Mat<f32>,
Vec<f32>,
Option<Mat<f32>>,
Vec<f32>,
Vec<f32>,
),
BixverseErrors,
>;
pub type SingleCellPcaRes = Result<(Mat<f32>, Mat<f32>, Vec<f32>), BixverseErrors>;
pub type SingleCellPcaResStats =
Result<(Mat<f32>, Mat<f32>, Vec<f32>, Vec<f32>, Vec<f32>), BixverseErrors>;
#[derive(Clone, Debug)]
pub struct SingleCellPcaParams {
pub mean_center: bool,
pub normalise_variance: bool,
pub randomised: bool,
pub clr: bool,
pub size_factor: f32,
}
impl SingleCellPcaParams {
pub fn new(
mean_center: bool,
normalise_variance: bool,
randomised: bool,
clr: bool,
size_factor: f32,
) -> Self {
Self {
mean_center,
normalise_variance,
randomised,
clr,
size_factor,
}
}
}
impl Default for SingleCellPcaParams {
fn default() -> Self {
Self {
mean_center: true,
normalise_variance: true,
randomised: true,
clr: false,
size_factor: 1e4,
}
}
}
impl CscGeneChunk {
pub fn transform_to_clr(&mut self, size_factor: f32) {
let sf_inv = 1.0_f32 / size_factor;
let mut new_sum = 0.0_f32;
for v in self.data_norm.iter_mut() {
let new_val = (v.to_f32().exp_m1() * sf_inv).ln_1p();
new_sum += new_val;
*v = F16::from(f16::from_f32(new_val));
}
self.avg_exp = F16::from(f16::from_f32(new_sum));
}
}
pub fn scale_csc_chunk(
chunk: &CscGeneChunk,
no_cells: usize,
mean_center: bool,
normalise_variance: bool,
row_offsets: Option<&[f64]>,
) -> (Vec<f32>, f32, f32) {
let mut dense_data = vec![0_f32; no_cells];
for (idx, &row_idx) in chunk.indices.iter().enumerate() {
dense_data[row_idx as usize] = chunk.data_norm[idx].to_f32();
}
if let Some(off) = row_offsets {
for i in 0..no_cells {
dense_data[i] -= off[i] as f32;
}
}
let n = no_cells as f64;
match (mean_center, normalise_variance) {
(false, false) => (dense_data, 0.0, 0.0),
(true, false) => {
let mean = (dense_data.iter().map(|&x| x as f64).sum::<f64>() / n) as f32;
let scaled = dense_data.iter().map(|&x| x - mean).collect();
(scaled, mean, 0.0)
}
(false, true) => {
let mean_f64 = dense_data.iter().map(|&x| x as f64).sum::<f64>() / n;
let mean = mean_f64 as f32;
let std_dev = (dense_data
.iter()
.map(|&x| {
let d = x as f64 - mean_f64;
d * d
})
.sum::<f64>()
/ (n - 1.0))
.sqrt() as f32;
let scaled = if std_dev < 1e-8 {
vec![0_f32; no_cells]
} else {
dense_data.iter().map(|&x| x / std_dev).collect()
};
(scaled, mean, std_dev)
}
(true, true) => {
let mean_f64 = dense_data.iter().map(|&x| x as f64).sum::<f64>() / n;
let mean = mean_f64 as f32;
let std_dev = (dense_data
.iter()
.map(|&x| {
let d = x as f64 - mean_f64;
d * d
})
.sum::<f64>()
/ (n - 1.0))
.sqrt() as f32;
let scaled = if std_dev < 1e-8 {
vec![0_f32; no_cells]
} else {
dense_data.iter().map(|&x| (x - mean) / std_dev).collect()
};
(scaled, mean, std_dev)
}
}
}
pub fn sparse_csc_column_means(
csc: &CompressedSparseData2<f32>,
use_second_layer: bool,
row_offsets: Option<&[f64]>,
) -> Result<Vec<f64>, BixverseErrors> {
if !csc.cs_type.is_csc() {
return Err(BixverseErrors::SparseMatrixMustBeCsc);
}
let (n, m) = csc.shape;
let n_f = n as f64;
let values: &[f32] = if use_second_layer {
csc.data_2
.as_ref()
.ok_or(BixverseErrors::Data2NotAvailable)?
.as_slice()
} else {
&csc.data
};
let m_bar = row_offsets
.map(|off| off.iter().sum::<f64>() / n_f)
.unwrap_or(0.0);
let res = (0..m)
.into_par_iter()
.map(|j| {
let start = csc.indptr[j] as usize;
let end = csc.indptr[j + 1] as usize;
let sum: f64 = values[start..end].iter().map(|&x| x as f64).sum();
sum / n_f - m_bar
})
.collect();
Ok(res)
}
pub fn sparse_csc_column_stds(
csc: &CompressedSparseData2<f32>,
col_means: &[f64],
use_second_layer: bool,
row_offsets: Option<&[f64]>,
) -> Result<Vec<f64>, BixverseErrors> {
if !csc.cs_type.is_csc() {
return Err(BixverseErrors::SparseMatrixMustBeCsc);
}
let (n, m) = csc.shape;
let n_f = n as f64;
let values: &[f32] = if use_second_layer {
csc.data_2
.as_ref()
.ok_or(BixverseErrors::Data2NotAvailable)?
.as_slice()
} else {
&csc.data
};
let (m_bar, var_m, m_centered) = if let Some(off) = row_offsets {
let m_bar = off.iter().sum::<f64>() / n_f;
let m_centered: Vec<f64> = off.iter().map(|&v| v - m_bar).collect();
let var_m = m_centered.iter().map(|&v| v * v).sum::<f64>() / (n_f - 1.0);
(m_bar, var_m, Some(m_centered))
} else {
(0.0, 0.0, None)
};
let res = (0..m)
.into_par_iter()
.map(|j| {
let start = csc.indptr[j] as usize;
let end = csc.indptr[j + 1] as usize;
let nnz = end - start;
let mu_a = col_means[j] + m_bar;
let slice = &values[start..end];
let indices = &csc.indices[start..end];
let ss_nonzero: f64 = slice
.iter()
.map(|&x| {
let d = x as f64 - mu_a;
d * d
})
.sum();
let ss_zeros = (n - nnz) as f64 * mu_a * mu_a;
let var_a = (ss_nonzero + ss_zeros) / (n_f - 1.0);
let variance = if let Some(mc) = &m_centered {
let cov_term: f64 = slice
.iter()
.zip(indices.iter())
.map(|(&x, &i)| x as f64 * mc[i as usize])
.sum();
var_a - (2.0 / (n_f - 1.0)) * cov_term + var_m
} else {
var_a
};
variance.max(0.0).sqrt().max(f64::EPSILON)
})
.collect();
Ok(res)
}
#[allow(clippy::too_many_arguments)]
fn dense_pca(
f_path: &str,
cell_indices: &[usize],
gene_indices: &[usize],
no_pcs: usize,
params_pca: &SingleCellPcaParams,
clr_offsets: Option<&[f64]>,
seed: usize,
return_scaled: bool,
verbose: usize,
) -> SingleCellPcaResScaledStats {
if params_pca.clr && clr_offsets.is_none() {
return Err(BixverseErrors::OffsetsNotProvidedForClrPCA);
}
if params_pca.clr
&& let Some(offs) = clr_offsets
&& offs.len() != cell_indices.len()
{
return Err(BixverseErrors::OffsetsLengthDoesNotMatchNCells {
len_offset: offs.len(),
n_cells: cell_indices.len(),
});
}
let verbosity = parse_verbosity_level(verbose);
let start_total = Instant::now();
let cell_set: IndexSet<u32> = cell_indices.iter().map(|&x| x as u32).collect();
let start_reading = Instant::now();
let reader = ParallelSparseReader::new(f_path)?;
let mut gene_chunks: Vec<CscGeneChunk> =
reader.read_gene_parallel_filtered(gene_indices, &cell_set)?;
if params_pca.clr {
gene_chunks
.par_iter_mut()
.for_each(|chunk| chunk.transform_to_clr(params_pca.size_factor));
}
let end_reading = start_reading.elapsed();
if verbosity.normal_verbosity() {
println!("PCA: Loaded in data in {:.2?}", end_reading);
}
let start_scaling = Instant::now();
let scaled_data: Vec<(Vec<f32>, f32, f32)> = gene_chunks
.par_iter()
.map(|chunk| {
let (scaled, mean, sd) = scale_csc_chunk(
chunk,
cell_indices.len(),
params_pca.mean_center,
params_pca.normalise_variance,
clr_offsets,
);
(scaled, mean, sd)
})
.collect();
let mut feature_means = Vec::with_capacity(scaled_data.len());
let mut feature_sds = Vec::with_capacity(scaled_data.len());
for (_, mean, sd) in scaled_data.iter() {
feature_means.push(*mean);
feature_sds.push(*sd);
}
drop(gene_chunks);
let num_genes = scaled_data.len();
let n_cells = cell_indices.len();
let scaled_f64 = Mat::<f64>::from_fn(n_cells, num_genes, |row, col| {
scaled_data[col].0[row] as f64
});
let scaled_f32 = if return_scaled {
Some(Mat::<f32>::from_fn(n_cells, num_genes, |row, col| {
scaled_data[col].0[row]
}))
} else {
None
};
drop(scaled_data);
let end_scaling = start_scaling.elapsed();
if verbosity.normal_verbosity() {
println!("PCA: Finished scaling in {:.2?}", end_scaling);
}
let start_svd = Instant::now();
let (scores, loadings, s) = if params_pca.randomised {
let res: RandomSvdResults<f64> =
randomised_svd(scaled_f64.as_ref(), no_pcs, seed, Some(100_usize), None)?;
let loadings = Mat::<f32>::from_fn(num_genes, no_pcs, |i, j| res.v[(i, j)] as f32);
let scores = Mat::<f32>::from_fn(n_cells, no_pcs, |i, j| (res.u[(i, j)] * res.s[j]) as f32);
let s: Vec<f32> = res.s[..no_pcs].iter().map(|&x| x as f32).collect();
(scores, loadings, s)
} else {
let res = scaled_f64
.thin_svd()
.map_err(|e| BixverseErrors::FaerSvdError(format!("{e:?}")))?;
let loadings = Mat::<f32>::from_fn(num_genes, no_pcs, |i, j| res.V()[(i, j)] as f32);
let scores = Mat::<f32>::from_fn(n_cells, no_pcs, |i, j| {
(res.U()[(i, j)] * res.S().column_vector()[j]) as f32
});
let s: Vec<f32> = res
.S()
.column_vector()
.iter()
.take(no_pcs)
.map(|&x| x as f32)
.collect();
(scores, loadings, s)
};
let end_svd = start_svd.elapsed();
if verbosity.normal_verbosity() {
println!("PCA: Finished calculations in {:.2?}", end_svd);
}
let end_total = start_total.elapsed();
if verbosity.normal_verbosity() {
println!("PCA: Total run time -> {:.2?}", end_total);
}
Ok((scores, loadings, s, scaled_f32, feature_means, feature_sds))
}
#[allow(clippy::too_many_arguments)]
pub fn pca_on_sc(
f_path: &str,
cell_indices: &[usize],
gene_indices: &[usize],
no_pcs: usize,
params_pca: &SingleCellPcaParams,
clr_offsets: Option<&[f64]>,
seed: usize,
return_scaled: bool,
verbose: usize,
) -> SingleCellPcaResScaled {
let (scores, loadings, s, scaled_f32, _, _) = dense_pca(
f_path,
cell_indices,
gene_indices,
no_pcs,
params_pca,
clr_offsets,
seed,
return_scaled,
verbose,
)?;
Ok((scores, loadings, s, scaled_f32))
}
#[allow(clippy::too_many_arguments)]
pub fn pca_on_sc_stats(
f_path: &str,
cell_indices: &[usize],
gene_indices: &[usize],
no_pcs: usize,
params_pca: &SingleCellPcaParams,
clr_offsets: Option<&[f64]>,
seed: usize,
verbose: usize,
) -> SingleCellPcaResStats {
let (scores, loadings, s, _, feature_means, feature_sds) = dense_pca(
f_path,
cell_indices,
gene_indices,
no_pcs,
params_pca,
clr_offsets,
seed,
false,
verbose,
)?;
Ok((scores, loadings, s, feature_means, feature_sds))
}
#[allow(clippy::too_many_arguments)]
pub fn pca_on_sc_streaming(
f_path: &str,
cell_indices: &[usize],
gene_indices: &[usize],
no_pcs: usize,
params_pca: &SingleCellPcaParams,
clr_offsets: Option<&[f64]>,
seed: usize,
return_scaled: bool,
gene_batch_size: usize,
verbose: usize,
) -> SingleCellPcaResScaled {
if params_pca.clr && clr_offsets.is_none() {
return Err(BixverseErrors::OffsetsNotProvidedForClrPCA);
}
if params_pca.clr
&& let Some(offs) = clr_offsets
&& offs.len() != cell_indices.len()
{
return Err(BixverseErrors::OffsetsLengthDoesNotMatchNCells {
len_offset: offs.len(),
n_cells: cell_indices.len(),
});
}
let verbosity = parse_verbosity_level(verbose);
let start_total = Instant::now();
let cell_set: IndexSet<u32> = cell_indices.iter().map(|&x| x as u32).collect();
let n_cells = cell_indices.len();
let n_genes = gene_indices.len();
let num_batches = n_genes.div_ceil(gene_batch_size);
let reader = ParallelSparseReader::new(f_path)?;
let mut scaled_matrix = Mat::<f64>::zeros(n_cells, n_genes);
let start_scaling = Instant::now();
for batch_idx in 0..num_batches {
if verbosity.normal_verbosity() {
println!(
"PCA (streaming): Scaling batch {}/{} ({} genes each)",
batch_idx + 1,
num_batches,
gene_batch_size
);
}
let start_gene = batch_idx * gene_batch_size;
let end_gene = ((batch_idx + 1) * gene_batch_size).min(n_genes);
let batch_gene_indices = &gene_indices[start_gene..end_gene];
let start_loading = Instant::now();
let mut gene_chunks = reader.read_gene_parallel_filtered(batch_gene_indices, &cell_set)?;
if params_pca.clr {
gene_chunks
.par_iter_mut()
.for_each(|chunk| chunk.transform_to_clr(params_pca.size_factor));
}
if verbosity.detailed_verbosity() {
println!(" Loaded batch in: {:.2?}", start_loading.elapsed());
}
let batch_scaled: Vec<Vec<f32>> = gene_chunks
.par_iter()
.map(|chunk| {
let (scaled, _, _) = scale_csc_chunk(
chunk,
n_cells,
params_pca.mean_center,
params_pca.normalise_variance,
clr_offsets,
);
scaled
})
.collect();
for (local_col, scaled_col) in batch_scaled.iter().enumerate() {
let global_col = start_gene + local_col;
for (row, &val) in scaled_col.iter().enumerate() {
scaled_matrix[(row, global_col)] = val as f64;
}
}
drop(gene_chunks);
}
if verbosity.normal_verbosity() {
println!(
"PCA (streaming): finished scaling in {:.2?}",
start_scaling.elapsed()
);
}
let start_svd = Instant::now();
let (scores, loadings, s) = if params_pca.randomised {
let res: RandomSvdResults<f64> =
randomised_svd(scaled_matrix.as_ref(), no_pcs, seed, Some(100_usize), None)?;
let loadings = Mat::<f32>::from_fn(n_genes, no_pcs, |i, j| res.v[(i, j)] as f32);
let scores = Mat::<f32>::from_fn(n_cells, no_pcs, |i, j| (res.u[(i, j)] * res.s[j]) as f32);
let s: Vec<f32> = res.s[..no_pcs].iter().map(|&x| x as f32).collect();
(scores, loadings, s)
} else {
let res = scaled_matrix
.thin_svd()
.map_err(|e| BixverseErrors::FaerSvdError(format!("{e:?}")))?;
let loadings = Mat::<f32>::from_fn(n_genes, no_pcs, |i, j| res.V()[(i, j)] as f32);
let scores = Mat::<f32>::from_fn(n_cells, no_pcs, |i, j| {
(res.U()[(i, j)] * res.S().column_vector()[j]) as f32
});
let s: Vec<f32> = res
.S()
.column_vector()
.iter()
.take(no_pcs)
.map(|&x| x as f32)
.collect();
(scores, loadings, s)
};
if verbosity.normal_verbosity() {
println!(
"PCA (streaming): finished calculations in {:.2?}",
start_svd.elapsed()
);
println!(
"PCA (streaming): total run time -> {:.2?}",
start_total.elapsed()
);
}
let scaled = if return_scaled {
Some(Mat::<f32>::from_fn(n_cells, n_genes, |i, j| {
scaled_matrix[(i, j)] as f32
}))
} else {
None
};
Ok((scores, loadings, s, scaled))
}
#[allow(clippy::too_many_arguments)]
fn sparse_pca(
f_path: &str,
cell_indices: &[usize],
gene_indices: &[usize],
no_pcs: usize,
params_pca: &SingleCellPcaParams,
clr_offsets: Option<&[f64]>,
seed: usize,
verbose: usize,
) -> SingleCellPcaResStats {
if params_pca.clr && clr_offsets.is_none() {
return Err(BixverseErrors::OffsetsNotProvidedForClrPCA);
}
if params_pca.clr
&& let Some(offs) = clr_offsets
&& offs.len() != cell_indices.len()
{
return Err(BixverseErrors::OffsetsLengthDoesNotMatchNCells {
len_offset: offs.len(),
n_cells: cell_indices.len(),
});
}
let verbosity = parse_verbosity_level(verbose);
let start_total = Instant::now();
let cell_set: IndexSet<u32> = cell_indices.iter().map(|&x| x as u32).collect();
let start_reading = Instant::now();
let reader = ParallelSparseReader::new(f_path)?;
let mut gene_chunks: Vec<CscGeneChunk> =
reader.read_gene_parallel_filtered(gene_indices, &cell_set)?;
if params_pca.clr {
gene_chunks
.par_iter_mut()
.for_each(|chunk| chunk.transform_to_clr(params_pca.size_factor));
}
let end_reading = start_reading.elapsed();
if verbosity.normal_verbosity() {
println!("Sparse PCA: Loaded in data in {:.2?}", end_reading);
}
let start_data_prep = Instant::now();
let n_cells = cell_set.len();
let csc = from_gene_chunks::<f32>(gene_chunks, &DataLayerReturn::Norm, n_cells);
let end_data_prep = start_data_prep.elapsed();
let col_means: Vec<f64> = sparse_csc_column_means(&csc, true, clr_offsets)?;
let col_stds: Vec<f64> = sparse_csc_column_stds(&csc, &col_means, true, clr_offsets)?;
if verbosity.normal_verbosity() {
println!(
"Sparse PCA: finished the data preparations in {:.2?}",
end_data_prep
);
}
let start_svd = Instant::now();
let (scores, loadings, s) = if params_pca.randomised {
let svd_res = randomised_sparse_svd::<f32, f64>(
csc,
no_pcs,
seed as u64,
true,
Some(100_usize),
None,
Some(&col_means),
Some(&col_stds),
clr_offsets,
)?;
let scores_f64 = compute_pc_scores(&svd_res);
let scores = Mat::<f32>::from_fn(n_cells, no_pcs, |i, j| scores_f64[(i, j)] as f32);
let loadings = Mat::<f32>::from_fn(gene_indices.len(), no_pcs, |i, j| {
svd_res.v()[(i, j)] as f32
});
let s: Vec<f32> = svd_res.s()[..no_pcs].iter().map(|&x| x as f32).collect();
(scores, loadings, s)
} else {
let svd_res = sparse_svd_lanczos::<f32, f32, f64>(
&csc,
no_pcs,
seed as u64,
true,
Some(&col_means),
Some(&col_stds),
clr_offsets,
)?;
let scores_f64 = compute_pc_scores(&svd_res);
let scores = Mat::<f32>::from_fn(scores_f64.nrows(), scores_f64.ncols(), |i, j| {
scores_f64[(i, j)] as f32
});
let loadings = Mat::<f32>::from_fn(svd_res.v().nrows(), svd_res.v().ncols(), |i, j| {
svd_res.v()[(i, j)] as f32
});
let s: Vec<f32> = svd_res.s().iter().map(|&x| x as f32).collect();
(scores, loadings, s)
};
let end_svd = start_svd.elapsed();
if verbosity.normal_verbosity() {
println!("Sparse PCA: finished calculations in {:.2?}", end_svd);
}
let end_total = start_total.elapsed();
if verbosity.normal_verbosity() {
println!("Sparse PCA: total run time -> {:.2?}", end_total);
}
let col_means = col_means.iter().map(|x| *x as f32).collect();
let col_stds = col_stds.iter().map(|x| *x as f32).collect();
Ok((scores, loadings, s, col_means, col_stds))
}
#[allow(clippy::too_many_arguments)]
pub fn pca_on_sc_sparse(
f_path: &str,
cell_indices: &[usize],
gene_indices: &[usize],
no_pcs: usize,
params_pca: &SingleCellPcaParams,
clr_offsets: Option<&[f64]>,
seed: usize,
verbose: usize,
) -> SingleCellPcaRes {
let (scores, loadings, s, _, _) = sparse_pca(
f_path,
cell_indices,
gene_indices,
no_pcs,
params_pca,
clr_offsets,
seed,
verbose,
)?;
Ok((scores, loadings, s))
}
#[allow(clippy::too_many_arguments)]
pub fn pca_on_sc_sparse_stats(
f_path: &str,
cell_indices: &[usize],
gene_indices: &[usize],
no_pcs: usize,
params_pca: &SingleCellPcaParams,
clr_offsets: Option<&[f64]>,
seed: usize,
verbose: usize,
) -> SingleCellPcaResStats {
let res = sparse_pca(
f_path,
cell_indices,
gene_indices,
no_pcs,
params_pca,
clr_offsets,
seed,
verbose,
)?;
Ok(res)
}