use hdf5::File;
use rayon::prelude::*;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Instant;
use thousands::Separable;
use crate::prelude::*;
use crate::single_cell::sc_data::data_io::*;
use crate::single_cell::sc_data::h5_10x_io::{TenxVersion, validate_feature_types_tenx};
pub struct TenxFileTask {
pub exp_id: String,
pub h5_path: String,
pub version: TenxVersion,
pub no_cells: usize,
pub no_genes: usize,
pub gene_local_to_universe: Vec<Option<usize>>,
pub feature_type: Option<String>,
}
pub struct TenxFileQcResult {
pub exp_id: String,
pub cells_to_keep: Vec<usize>,
pub lib_size: Vec<usize>,
pub nnz: Vec<usize>,
}
pub struct MultiTenxResult {
pub global_gene_indices: Vec<usize>,
pub total_cells: usize,
pub total_genes: usize,
pub per_file: Vec<TenxFileQcResult>,
}
const CELL_CHUNK_SIZE: usize = 10_000;
const CELL_BATCH_SIZE: usize = 1_000;
fn build_effective_mapping(task: &TenxFileTask) -> Result<Vec<Option<usize>>, BixverseErrors> {
match task.version {
TenxVersion::V2 => Ok(task.gene_local_to_universe.clone()),
TenxVersion::V3 => {
let target = task.feature_type.as_deref();
let valid = validate_feature_types_tenx(&task.h5_path, task.version, target)?;
let mut mask = vec![false; task.no_genes];
for i in valid {
if i < task.no_genes {
mask[i] = true;
}
}
Ok(task
.gene_local_to_universe
.iter()
.enumerate()
.map(|(local, opt)| {
if mask.get(local).copied().unwrap_or(false) {
*opt
} else {
None
}
})
.collect())
}
}
}
fn scan_gene_nnz(
task: &TenxFileTask,
gene_local_to_universe: &[Option<usize>],
universe_size: usize,
) -> Result<Vec<usize>, BixverseErrors> {
let file = File::open(&task.h5_path)?;
let indptr: Vec<usize> = file.dataset(task.version.get_indptr())?.read_1d()?.to_vec();
let indices_ds = file.dataset(task.version.get_indices())?;
let mut gene_nnz = vec![0usize; universe_size];
for chunk_start in (0..task.no_cells).step_by(CELL_CHUNK_SIZE) {
let chunk_end = (chunk_start + CELL_CHUNK_SIZE).min(task.no_cells);
let data_start = indptr[chunk_start];
let data_end = indptr[chunk_end];
if data_start >= data_end {
continue;
}
let chunk_indices: Vec<usize> = indices_ds.read_slice_1d(data_start..data_end)?.to_vec();
for cell_idx in chunk_start..chunk_end {
let start = indptr[cell_idx] - data_start;
let end = indptr[cell_idx + 1] - data_start;
for local_idx in start..end {
let local_gene = chunk_indices[local_idx];
if let Some(&Some(u_idx)) = gene_local_to_universe.get(local_gene) {
gene_nnz[u_idx] += 1;
}
}
}
}
Ok(gene_nnz)
}
fn scan_cell_stats(
task: &TenxFileTask,
gene_local_to_final: &[Option<usize>],
) -> Result<Vec<(usize, f32)>, BixverseErrors> {
let file = File::open(&task.h5_path)?;
let indptr: Vec<usize> = file.dataset(task.version.get_indptr())?.read_1d()?.to_vec();
let data_ds = file.dataset(task.version.get_data())?;
let indices_ds = file.dataset(task.version.get_indices())?;
let mut cell_stats = Vec::with_capacity(task.no_cells);
for chunk_start in (0..task.no_cells).step_by(CELL_CHUNK_SIZE) {
let chunk_end = (chunk_start + CELL_CHUNK_SIZE).min(task.no_cells);
let data_start = indptr[chunk_start];
let data_end = indptr[chunk_end];
if data_start >= data_end {
cell_stats.extend((chunk_start..chunk_end).map(|_| (0usize, 0.0f32)));
continue;
}
let chunk_data: Vec<f32> = data_ds.read_slice_1d(data_start..data_end)?.to_vec();
let chunk_indices: Vec<usize> = indices_ds.read_slice_1d(data_start..data_end)?.to_vec();
for cell_idx in chunk_start..chunk_end {
let start = indptr[cell_idx] - data_start;
let end = indptr[cell_idx + 1] - data_start;
let mut unique = 0usize;
let mut lib_size = 0.0f32;
for local_idx in start..end {
if let Some(&Some(_)) = gene_local_to_final.get(chunk_indices[local_idx]) {
unique += 1;
lib_size += chunk_data[local_idx];
}
}
cell_stats.push((unique, lib_size));
}
}
Ok(cell_stats)
}
fn write_tenx_file_cells(
task: &TenxFileTask,
cells_to_keep: &[usize],
gene_local_to_final: &[Option<usize>],
target_size: f32,
cell_offset: usize,
writer: &mut CellGeneSparseWriter,
verbose: bool,
) -> Result<TenxFileQcResult, BixverseErrors> {
if verbose {
println!(
" Writing {} ({} cells)...",
task.exp_id,
cells_to_keep.len().separate_with_underscores()
);
}
let file = File::open(&task.h5_path)?;
let data_ds = file.dataset(task.version.get_data())?;
let indices_ds = file.dataset(task.version.get_indices())?;
let indptr: Vec<usize> = file.dataset(task.version.get_indptr())?.read_1d()?.to_vec();
let mut lib_size = Vec::with_capacity(cells_to_keep.len());
let mut nnz = Vec::with_capacity(cells_to_keep.len());
let mut cell_buf: Vec<(usize, u32)> = Vec::with_capacity(10_000);
let mut gene_idx_buf: Vec<u32> = Vec::with_capacity(10_000);
let mut count_buf: Vec<u32> = Vec::with_capacity(10_000);
let mut written = 0usize;
for cell_batch in cells_to_keep.chunks(CELL_BATCH_SIZE) {
let start_pos = cell_batch.iter().map(|&c| indptr[c]).min().unwrap_or(0);
let end_pos = cell_batch.iter().map(|&c| indptr[c + 1]).max().unwrap_or(0);
if start_pos >= end_pos {
for _ in cell_batch {
let empty = CsrCellChunk::from_data(
&[] as &[u32],
&[] as &[u32],
cell_offset + written,
target_size,
true,
);
lib_size.push(0);
nnz.push(0);
writer.write_cell_chunk(empty)?;
written += 1;
}
continue;
}
let chunk_data: Vec<f32> = data_ds.read_slice_1d(start_pos..end_pos)?.to_vec();
let chunk_indices: Vec<usize> = indices_ds.read_slice_1d(start_pos..end_pos)?.to_vec();
for &old_cell in cell_batch {
let start = indptr[old_cell] - start_pos;
let end = indptr[old_cell + 1] - start_pos;
cell_buf.clear();
gene_idx_buf.clear();
count_buf.clear();
for local_idx in start..end {
let old_gene = chunk_indices[local_idx];
if let Some(&Some(final_gene)) = gene_local_to_final.get(old_gene) {
cell_buf.push((final_gene, chunk_data[local_idx] as u32));
}
}
if !cell_buf.is_empty() {
if cell_buf.windows(2).any(|w| w[0].0 > w[1].0) {
cell_buf.sort_unstable_by_key(|&(g, _)| g);
}
gene_idx_buf.extend(cell_buf.iter().map(|(g, _)| *g as u32));
count_buf.extend(cell_buf.iter().map(|(_, c)| *c));
}
let chunk = CsrCellChunk::from_data(
&count_buf,
&gene_idx_buf,
cell_offset + written,
target_size,
true,
);
let (nnz_i, lib_i) = chunk.get_qc_info();
nnz.push(nnz_i);
lib_size.push(lib_i);
writer.write_cell_chunk(chunk)?;
written += 1;
}
}
Ok(TenxFileQcResult {
exp_id: task.exp_id.clone(),
cells_to_keep: cells_to_keep.to_vec(),
lib_size,
nnz,
})
}
pub fn multi_10x_h5_to_file<P: AsRef<Path>>(
tasks: &[TenxFileTask],
bin_path: P,
universe_size: usize,
cell_qc: &MinCellQuality,
verbose: bool,
) -> Result<MultiTenxResult, BixverseErrors> {
let total_start = Instant::now();
if verbose {
println!(
"Resolving modality filters across {} 10x files...",
tasks.len()
);
}
let effective_mappings: Vec<Vec<Option<usize>>> = tasks
.par_iter()
.map(build_effective_mapping)
.collect::<Result<Vec<_>, _>>()?;
if verbose {
for (task, mapping) in tasks.iter().zip(effective_mappings.iter()) {
let kept = mapping.iter().filter(|o| o.is_some()).count();
let in_universe = task
.gene_local_to_universe
.iter()
.filter(|o| o.is_some())
.count();
println!(
" {}: {} / {} features kept after modality filter",
task.exp_id,
kept.separate_with_underscores(),
in_universe.separate_with_underscores()
);
}
println!("Scan 1/2: gene NNZ across {} 10x files...", tasks.len());
}
let completed = Arc::new(AtomicUsize::new(0));
let report_interval = (tasks.len() / 10).max(1);
let per_file_nnz: Vec<Vec<usize>> = tasks
.par_iter()
.zip(effective_mappings.par_iter())
.map(|(task, mapping)| {
let res = scan_gene_nnz(task, mapping, universe_size);
if verbose {
let done = completed.fetch_add(1, Ordering::Relaxed) + 1;
if done.is_multiple_of(report_interval) || done == tasks.len() {
println!(" Scanned {}/{} files", done, tasks.len());
}
}
res
})
.collect::<Result<Vec<_>, _>>()?;
let mut global_gene_nnz = vec![0usize; universe_size];
for f in &per_file_nnz {
for (i, &c) in f.iter().enumerate() {
global_gene_nnz[i] += c;
}
}
let mut universe_to_final = vec![None; universe_size];
let mut final_idx = 0usize;
for (u, &nnz) in global_gene_nnz.iter().enumerate() {
if nnz >= cell_qc.min_cells {
universe_to_final[u] = Some(final_idx);
final_idx += 1;
}
}
let global_gene_indices: Vec<usize> = (0..universe_size)
.filter(|&i| universe_to_final[i].is_some())
.collect();
let total_genes = global_gene_indices.len();
if verbose {
println!(
" Genes passing global QC: {} / {}",
total_genes.separate_with_underscores(),
universe_size.separate_with_underscores()
);
}
let composed: Vec<Vec<Option<usize>>> = effective_mappings
.iter()
.map(|m| {
m.iter()
.map(|opt| opt.and_then(|u| universe_to_final[u]))
.collect()
})
.collect();
if verbose {
println!("Scan 2/2: cell stats with final gene set...");
}
let completed = Arc::new(AtomicUsize::new(0));
let per_file_cell_stats: Vec<Vec<(usize, f32)>> = tasks
.par_iter()
.zip(composed.par_iter())
.map(|(task, mapping)| {
let res = scan_cell_stats(task, mapping);
if verbose {
let done = completed.fetch_add(1, Ordering::Relaxed) + 1;
if done.is_multiple_of(report_interval) || done == tasks.len() {
println!(" Scanned {}/{} files", done, tasks.len());
}
}
res
})
.collect::<Result<Vec<_>, _>>()?;
let per_file_cells: Vec<Vec<usize>> = per_file_cell_stats
.iter()
.map(|stats| {
stats
.iter()
.enumerate()
.filter(|(_, (u, l))| {
*u >= cell_qc.min_unique_genes && *l >= cell_qc.min_lib_size as f32
})
.map(|(i, _)| i)
.collect()
})
.collect();
let total_cells: usize = per_file_cells.iter().map(|v| v.len()).sum();
if verbose {
for (i, t) in tasks.iter().enumerate() {
println!(
" {}: {} cells passing QC",
t.exp_id,
per_file_cells[i].len().separate_with_underscores()
);
}
println!(" Total cells: {}", total_cells.separate_with_underscores());
}
if verbose {
println!("Writing cells to binary...");
}
let mut writer = CellGeneSparseWriter::new(&bin_path, true, total_cells, total_genes)?;
let mut cell_offset = 0usize;
let mut per_file_results = Vec::with_capacity(tasks.len());
for (idx, task) in tasks.iter().enumerate() {
let res = write_tenx_file_cells(
task,
&per_file_cells[idx],
&composed[idx],
cell_qc.target_size,
cell_offset,
&mut writer,
verbose,
)?;
cell_offset += per_file_cells[idx].len();
per_file_results.push(res);
}
writer.finalise()?;
if verbose {
println!("Multi-10x loading complete: {:.2?}", total_start.elapsed());
}
Ok(MultiTenxResult {
global_gene_indices,
total_cells,
total_genes,
per_file: per_file_results,
})
}