1use hdf5::File;
5use ndarray::{Array2, s};
6use rayon::prelude::*;
7use std::path::Path;
8use std::time::Instant;
9use thousands::Separable;
10
11use super::data_io::*;
12use super::h5_filters::check_h5ad_filters;
13use super::h5ad_io::*;
14use crate::h5_chunked::H5ChunkedReader;
15use crate::prelude::*;
16
17pub struct H5adFileTask {
23 pub exp_id: String,
25 pub h5_path: String,
27 pub cs_type: H5ADFormat,
29 pub no_cells: usize,
31 pub no_genes: usize,
33 pub gene_local_to_universe: Vec<Option<usize>>,
35 pub raw_slot: RawDataSlot,
37}
38
39fn scan_gene_nnz_csr(
60 task: &H5adFileTask,
61 universe_size: usize,
62) -> Result<Vec<usize>, ScDataErrors> {
63 let file = File::open(&task.h5_path)?;
64
65 check_h5ad_is_raw(&file, &task.raw_slot, None)?;
66
67 let indptr = read_indptr(&file.dataset(task.raw_slot.get_indptr())?)?;
68 let indices_rd = H5ChunkedReader::new(file.dataset(task.raw_slot.get_indices())?);
69
70 let mut gene_nnz = vec![0usize; universe_size];
71
72 const CHUNK_SIZE: usize = 10_000;
73
74 for chunk_start in (0..task.no_cells).step_by(CHUNK_SIZE) {
75 let chunk_end = (chunk_start + CHUNK_SIZE).min(task.no_cells);
76 let data_start = indptr[chunk_start];
77 let data_end = indptr[chunk_end];
78
79 if data_start >= data_end {
80 continue;
81 }
82
83 let chunk_indices: Vec<u32> = indices_rd.read(data_start..data_end)?;
84
85 for cell_idx in chunk_start..chunk_end {
86 let start = indptr[cell_idx] - data_start;
87 let end = indptr[cell_idx + 1] - data_start;
88
89 for local_idx in start..end {
90 let gene_idx = chunk_indices[local_idx] as usize;
91 if let Some(&Some(u_idx)) = task.gene_local_to_universe.get(gene_idx) {
92 gene_nnz[u_idx] += 1;
93 }
94 }
95 }
96 }
97
98 Ok(gene_nnz)
99}
100
101fn scan_gene_nnz_csc(
114 task: &H5adFileTask,
115 universe_size: usize,
116) -> Result<Vec<usize>, ScDataErrors> {
117 let file = File::open(&task.h5_path)?;
118
119 check_h5ad_is_raw(&file, &task.raw_slot, None)?;
120
121 let indptr = read_indptr(&file.dataset(task.raw_slot.get_indptr())?)?;
122
123 let mut gene_nnz = vec![0usize; universe_size];
124
125 for (local, opt) in task.gene_local_to_universe.iter().enumerate() {
126 if let Some(u_idx) = opt {
127 gene_nnz[*u_idx] = indptr[local + 1] - indptr[local];
128 }
129 }
130
131 Ok(gene_nnz)
132}
133
134pub fn scan_gene_nnz_dense_row(
145 task: &H5adFileTask,
146 universe_size: usize,
147) -> Result<Vec<usize>, ScDataErrors> {
148 let file = File::open(&task.h5_path)?;
149 check_h5ad_dense_is_raw(&file, &task.raw_slot, None)?;
150
151 let ds = file.dataset(task.raw_slot.get_dense_path())?;
152 let mut gene_nnz = vec![0usize; universe_size];
153
154 const CHUNK_SIZE: usize = 10_000;
155 for chunk_start in (0..task.no_cells).step_by(CHUNK_SIZE) {
156 let chunk_end = (chunk_start + CHUNK_SIZE).min(task.no_cells);
157 let block: Array2<f32> = ds.read_slice_2d(s![chunk_start..chunk_end, 0..task.no_genes])?;
158 for row in block.outer_iter() {
159 for (gene_idx, &val) in row.iter().enumerate() {
160 if val != 0.0
161 && let Some(&Some(u_idx)) = task.gene_local_to_universe.get(gene_idx)
162 {
163 gene_nnz[u_idx] += 1;
164 }
165 }
166 }
167 }
168
169 Ok(gene_nnz)
170}
171
172fn scan_gene_nnz(task: &H5adFileTask, universe_size: usize) -> Result<Vec<usize>, ScDataErrors> {
183 match task.cs_type {
184 H5ADFormat::Csr => scan_gene_nnz_csr(task, universe_size),
185 H5ADFormat::Csc => scan_gene_nnz_csc(task, universe_size),
186 H5ADFormat::DenseRow => scan_gene_nnz_dense_row(task, universe_size),
187 H5ADFormat::DenseCol => dense_col_not_supported(),
188 }
189}
190
191fn scan_cell_stats_csr(
208 task: &H5adFileTask,
209 gene_local_to_final: &[Option<usize>],
210) -> Result<Vec<(usize, f32)>, ScDataErrors> {
211 let file = File::open(&task.h5_path)?;
212
213 check_h5ad_is_raw(&file, &task.raw_slot, None)?;
214
215 let indptr = read_indptr(&file.dataset(task.raw_slot.get_indptr())?)?;
216 let data_rd = H5ChunkedReader::new(file.dataset(task.raw_slot.get_data())?);
217 let indices_rd = H5ChunkedReader::new(file.dataset(task.raw_slot.get_indices())?);
218
219 let mut cell_stats = Vec::with_capacity(task.no_cells);
220
221 const CHUNK_SIZE: usize = 10_000;
222
223 for chunk_start in (0..task.no_cells).step_by(CHUNK_SIZE) {
224 let chunk_end = (chunk_start + CHUNK_SIZE).min(task.no_cells);
225 let data_start = indptr[chunk_start];
226 let data_end = indptr[chunk_end];
227
228 if data_start >= data_end {
229 cell_stats.extend((chunk_start..chunk_end).map(|_| (0usize, 0.0f32)));
230 continue;
231 }
232
233 let chunk_data: Vec<f32> = data_rd.read(data_start..data_end)?;
234 let chunk_indices: Vec<u32> = indices_rd.read(data_start..data_end)?;
235
236 for cell_idx in chunk_start..chunk_end {
237 let start = indptr[cell_idx] - data_start;
238 let end = indptr[cell_idx + 1] - data_start;
239
240 let mut unique = 0usize;
241 let mut lib_size = 0.0f32;
242
243 for local_idx in start..end {
244 let gene_idx = chunk_indices[local_idx] as usize;
245 if let Some(&Some(_)) = gene_local_to_final.get(gene_idx) {
246 unique += 1;
247 lib_size += chunk_data[local_idx];
248 }
249 }
250
251 cell_stats.push((unique, lib_size));
252 }
253 }
254
255 Ok(cell_stats)
256}
257
258fn scan_cell_stats_csc(
271 task: &H5adFileTask,
272 gene_local_to_final: &[Option<usize>],
273) -> Result<Vec<(usize, f32)>, ScDataErrors> {
274 let file = File::open(&task.h5_path)?;
275
276 check_h5ad_is_raw(&file, &task.raw_slot, None)?;
277
278 let indptr = read_indptr(&file.dataset(task.raw_slot.get_indptr())?)?;
279 let data_rd = H5ChunkedReader::new(file.dataset(task.raw_slot.get_data())?);
280 let indices_rd = H5ChunkedReader::new(file.dataset(task.raw_slot.get_indices())?);
281
282 let mut cell_unique = vec![0usize; task.no_cells];
283 let mut cell_lib_size = vec![0.0f32; task.no_cells];
284
285 let genes_with_final: Vec<usize> = gene_local_to_final
286 .iter()
287 .enumerate()
288 .filter_map(|(local, opt)| opt.map(|_| local))
289 .collect();
290
291 const GENE_CHUNK_SIZE: usize = 5_000;
292
293 for gene_chunk in genes_with_final.chunks(GENE_CHUNK_SIZE) {
294 let start_pos = gene_chunk.iter().map(|&g| indptr[g]).min().unwrap_or(0);
295 let end_pos = gene_chunk.iter().map(|&g| indptr[g + 1]).max().unwrap_or(0);
296
297 if start_pos >= end_pos {
298 continue;
299 }
300
301 let chunk_data: Vec<f32> = data_rd.read(start_pos..end_pos)?;
302 let chunk_indices: Vec<u32> = indices_rd.read(start_pos..end_pos)?;
303
304 for &local_gene in gene_chunk {
305 let gene_start = indptr[local_gene] - start_pos;
306 let gene_end = indptr[local_gene + 1] - start_pos;
307
308 for idx in gene_start..gene_end {
309 let cell_idx = chunk_indices[idx] as usize;
310 if cell_idx < task.no_cells {
311 cell_unique[cell_idx] += 1;
312 cell_lib_size[cell_idx] += chunk_data[idx];
313 }
314 }
315 }
316 }
317
318 Ok(cell_unique.into_iter().zip(cell_lib_size).collect())
319}
320
321pub fn scan_cell_stats_dense_row(
332 task: &H5adFileTask,
333 gene_local_to_final: &[Option<usize>],
334) -> Result<Vec<(usize, f32)>, ScDataErrors> {
335 let file = File::open(&task.h5_path)?;
336 check_h5ad_dense_is_raw(&file, &task.raw_slot, None)?;
337
338 let ds = file.dataset(task.raw_slot.get_dense_path())?;
339 let mut cell_stats: Vec<(usize, f32)> = Vec::with_capacity(task.no_cells);
340
341 const CHUNK_SIZE: usize = 10_000;
342 for chunk_start in (0..task.no_cells).step_by(CHUNK_SIZE) {
343 let chunk_end = (chunk_start + CHUNK_SIZE).min(task.no_cells);
344 let block: Array2<f32> = ds.read_slice_2d(s![chunk_start..chunk_end, 0..task.no_genes])?;
345 for row in block.outer_iter() {
346 let mut unique = 0usize;
347 let mut lib_size = 0.0f32;
348 for (gene_idx, &val) in row.iter().enumerate() {
349 if val == 0.0 {
350 continue;
351 }
352 if let Some(&Some(_)) = gene_local_to_final.get(gene_idx) {
353 unique += 1;
354 lib_size += val;
355 }
356 }
357 cell_stats.push((unique, lib_size));
358 }
359 }
360
361 Ok(cell_stats)
362}
363
364fn scan_cell_stats(
377 task: &H5adFileTask,
378 gene_local_to_final: &[Option<usize>],
379) -> Result<Vec<(usize, f32)>, ScDataErrors> {
380 match task.cs_type {
381 H5ADFormat::Csr => scan_cell_stats_csr(task, gene_local_to_final),
382 H5ADFormat::Csc => scan_cell_stats_csc(task, gene_local_to_final),
383 H5ADFormat::DenseRow => scan_cell_stats_dense_row(task, gene_local_to_final),
384 H5ADFormat::DenseCol => dense_col_not_supported(),
385 }
386}
387
388pub struct H5FileQcResult {
394 pub exp_id: String,
396 pub cells_to_keep: Vec<usize>,
398 pub lib_size: Vec<usize>,
400 pub nnz: Vec<usize>,
402}
403
404pub struct MultiH5adResult {
406 pub global_gene_indices: Vec<usize>,
408 pub total_cells: usize,
410 pub total_genes: usize,
412 pub per_file: Vec<H5FileQcResult>,
414}
415
416fn write_h5_csr_cells(
431 task: &H5adFileTask,
432 cells_to_keep: &[usize],
433 gene_mapping: &[Option<usize>],
434 target_size: f32,
435 cell_offset: usize,
436 writer: &mut CellGeneSparseWriter,
437) -> Result<H5FileQcResult, ScDataErrors> {
438 let indptr = {
439 let file = hdf5::File::open(&task.h5_path)?;
440 check_h5ad_is_raw(&file, &task.raw_slot, None)?;
441 read_indptr(&file.dataset(task.raw_slot.get_indptr())?)?
442 };
443
444 let (nnz, lib_size) = write_h5_cell_major_cells(
445 Path::new(&task.h5_path),
446 task.raw_slot.get_data(),
447 task.raw_slot.get_indices(),
448 &indptr,
449 cells_to_keep,
450 &dense_gene_map_from_options(gene_mapping),
451 cell_offset,
452 target_size,
453 writer,
454 false,
455 )?;
456
457 Ok(H5FileQcResult {
458 exp_id: task.exp_id.clone(),
459 cells_to_keep: cells_to_keep.to_vec(),
460 lib_size,
461 nnz,
462 })
463}
464
465fn write_h5_csc_cells(
480 task: &H5adFileTask,
481 cells_to_keep: &[usize],
482 gene_mapping: &[Option<usize>],
483 target_size: f32,
484 cell_offset: usize,
485 writer: &mut CellGeneSparseWriter,
486) -> Result<H5FileQcResult, ScDataErrors> {
487 let file = hdf5::File::open(&task.h5_path)?;
488
489 check_h5ad_is_raw(&file, &task.raw_slot, None)?;
490
491 let data_rd = H5ChunkedReader::new(file.dataset(task.raw_slot.get_data())?);
492 let indices_rd = H5ChunkedReader::new(file.dataset(task.raw_slot.get_indices())?);
493 let indptr = read_indptr(&file.dataset(task.raw_slot.get_indptr())?)?;
494
495 let cell_map = dense_index_map(cells_to_keep);
496
497 let mut cell_data: Vec<Vec<(u32, u32)>> = vec![Vec::new(); cells_to_keep.len()];
499
500 let genes_with_final: Vec<(usize, usize)> = gene_mapping
501 .iter()
502 .enumerate()
503 .filter_map(|(local, opt)| opt.map(|f| (local, f)))
504 .collect();
505
506 const GENE_CHUNK_SIZE: usize = 5_000;
507
508 for gene_chunk in genes_with_final.chunks(GENE_CHUNK_SIZE) {
509 let start_pos = gene_chunk
510 .iter()
511 .map(|&(local, _)| indptr[local])
512 .min()
513 .unwrap_or(0);
514 let end_pos = gene_chunk
515 .iter()
516 .map(|&(local, _)| indptr[local + 1])
517 .max()
518 .unwrap_or(0);
519
520 if start_pos >= end_pos {
521 continue;
522 }
523
524 let chunk_data: Vec<f32> = data_rd.read(start_pos..end_pos)?;
525 let chunk_indices: Vec<u32> = indices_rd.read(start_pos..end_pos)?;
526
527 for &(local_gene, final_gene) in gene_chunk {
528 let gene_start = indptr[local_gene] - start_pos;
529 let gene_end = indptr[local_gene + 1] - start_pos;
530
531 for idx in gene_start..gene_end {
532 let old_cell = chunk_indices[idx] as usize;
533 let new_cell = cell_map.get(old_cell).copied().unwrap_or(INDEX_DROPPED);
534 if new_cell != INDEX_DROPPED {
535 cell_data[new_cell as usize].push((final_gene as u32, chunk_data[idx] as u32));
536 }
537 }
538 }
539 }
540
541 let (nnz, lib_size) = write_cell_rows(&mut cell_data, cell_offset, target_size, writer)?;
542
543 Ok(H5FileQcResult {
544 exp_id: task.exp_id.clone(),
545 cells_to_keep: cells_to_keep.to_vec(),
546 lib_size,
547 nnz,
548 })
549}
550
551pub fn write_h5_dense_row_cells(
566 task: &H5adFileTask,
567 cells_to_keep: &[usize],
568 gene_mapping: &[Option<usize>],
569 target_size: f32,
570 cell_offset: usize,
571 writer: &mut CellGeneSparseWriter,
572) -> Result<H5FileQcResult, ScDataErrors> {
573 let file = File::open(&task.h5_path)?;
574 check_h5ad_dense_is_raw(&file, &task.raw_slot, None)?;
575
576 let ds = file.dataset(task.raw_slot.get_dense_path())?;
577 let no_genes_total = ds.shape().get(1).copied().unwrap_or(0);
578
579 let gene_map = dense_gene_map_from_options(gene_mapping);
580 let mut lib_size = Vec::with_capacity(cells_to_keep.len());
581 let mut nnz = Vec::with_capacity(cells_to_keep.len());
582
583 const BATCH_SIZE: usize = 1_000;
584 let mut written = 0usize;
585
586 for cell_batch in cells_to_keep.chunks(BATCH_SIZE) {
587 let row_start = *cell_batch
588 .iter()
589 .min()
590 .expect("batch non-empty by construction");
591 let row_end = *cell_batch
592 .iter()
593 .max()
594 .expect("batch non-empty by construction")
595 + 1;
596 let block: Array2<f32> = ds.read_slice_2d(s![row_start..row_end, 0..no_genes_total])?;
597
598 let first = cell_offset + written;
599 let built = cell_batch
600 .par_iter()
601 .enumerate()
602 .map_init(Vec::<(u32, u32)>::new, |buf, (k, &old_cell)| {
603 buf.clear();
604 let row = block.row(old_cell - row_start);
605 for (gene_idx, &val) in row.iter().enumerate() {
606 let final_gene = gene_map.get(gene_idx).copied().unwrap_or(INDEX_DROPPED);
607 if val != 0.0 && final_gene != INDEX_DROPPED {
608 buf.push((final_gene, val as u32));
609 }
610 }
611 compress_cell_row(buf, first + k, target_size, writer.format())
612 })
613 .collect::<Result<Vec<_>, ScDataErrors>>()?;
614
615 let mut payloads = Vec::with_capacity(built.len());
616 for (nnz_i, payload) in built {
617 nnz.push(nnz_i);
618 lib_size.push(payload.library_size);
619 payloads.push(payload);
620 }
621 writer.write_compressed_cell_chunks(&payloads)?;
622 written += cell_batch.len();
623 }
624
625 Ok(H5FileQcResult {
626 exp_id: task.exp_id.clone(),
627 cells_to_keep: cells_to_keep.to_vec(),
628 lib_size,
629 nnz,
630 })
631}
632
633fn write_h5_file_cells(
649 task: &H5adFileTask,
650 cells_to_keep: &[usize],
651 gene_mapping: &[Option<usize>],
652 target_size: f32,
653 cell_offset: usize,
654 writer: &mut CellGeneSparseWriter,
655 verbose: bool,
656) -> Result<H5FileQcResult, ScDataErrors> {
657 if verbose {
658 println!(
659 " Writing {} ({} cells)...",
660 task.exp_id,
661 cells_to_keep.len().separate_with_underscores()
662 );
663 }
664 match task.cs_type {
665 H5ADFormat::Csr => write_h5_csr_cells(
666 task,
667 cells_to_keep,
668 gene_mapping,
669 target_size,
670 cell_offset,
671 writer,
672 ),
673 H5ADFormat::Csc => write_h5_csc_cells(
674 task,
675 cells_to_keep,
676 gene_mapping,
677 target_size,
678 cell_offset,
679 writer,
680 ),
681 H5ADFormat::DenseRow => write_h5_dense_row_cells(
682 task,
683 cells_to_keep,
684 gene_mapping,
685 target_size,
686 cell_offset,
687 writer,
688 ),
689 H5ADFormat::DenseCol => dense_col_not_supported(),
690 }
691}
692
693pub fn multi_h5ad_to_file<P: AsRef<Path>>(
711 tasks: &[H5adFileTask],
712 bin_path: P,
713 universe_size: usize,
714 cell_qc: &MinCellQuality,
715 verbose: bool,
716) -> Result<MultiH5adResult, ScDataErrors> {
717 let total_start = Instant::now();
718
719 for task in tasks {
720 check_h5ad_filters(&File::open(&task.h5_path)?, &task.raw_slot, &task.cs_type)?;
721 }
722
723 if verbose {
724 println!(
725 "Scan 1/2: Counting gene expression across {} files...",
726 tasks.len()
727 );
728 }
729
730 let per_file_nnz: Vec<Vec<usize>> = tasks
732 .par_iter()
733 .map(|task| {
734 if verbose {
735 println!(" Scanning genes in {}...", task.exp_id);
736 }
737 scan_gene_nnz(task, universe_size)
738 })
739 .collect::<Result<Vec<_>, _>>()?;
740
741 let mut global_gene_nnz = vec![0usize; universe_size];
743 for file_nnz in &per_file_nnz {
744 for (i, &count) in file_nnz.iter().enumerate() {
745 global_gene_nnz[i] += count;
746 }
747 }
748
749 let mut universe_to_final = vec![None; universe_size];
751 let mut final_idx = 0usize;
752 for (u_idx, &nnz) in global_gene_nnz.iter().enumerate() {
753 if nnz >= cell_qc.min_cells {
754 universe_to_final[u_idx] = Some(final_idx);
755 final_idx += 1;
756 }
757 }
758
759 let global_gene_indices: Vec<usize> = (0..universe_size)
760 .filter(|&i| universe_to_final[i].is_some())
761 .collect();
762 let total_genes = global_gene_indices.len();
763
764 if verbose {
765 println!(
766 " Genes passing global QC: {} / {}",
767 total_genes.separate_with_underscores(),
768 universe_size.separate_with_underscores()
769 );
770 }
771
772 let composed_mappings: Vec<Vec<Option<usize>>> = tasks
774 .iter()
775 .map(|task| {
776 task.gene_local_to_universe
777 .iter()
778 .map(|opt| opt.and_then(|u| universe_to_final[u]))
779 .collect()
780 })
781 .collect();
782
783 if verbose {
785 println!("Scan 2/2: Computing cell QC with final gene set...");
786 }
787
788 let per_file_cell_stats: Vec<Vec<(usize, f32)>> = tasks
789 .par_iter()
790 .zip(composed_mappings.par_iter())
791 .map(|(task, mapping)| {
792 if verbose {
793 println!(" Scanning cells in {}...", task.exp_id);
794 }
795 scan_cell_stats(task, mapping)
796 })
797 .collect::<Result<Vec<_>, _>>()?;
798
799 let per_file_cells: Vec<Vec<usize>> = per_file_cell_stats
800 .iter()
801 .map(|stats| {
802 stats
803 .iter()
804 .enumerate()
805 .filter(|(_, (unique, lib))| {
806 *unique >= cell_qc.min_unique_genes && *lib >= cell_qc.min_lib_size as f32
807 })
808 .map(|(idx, _)| idx)
809 .collect()
810 })
811 .collect();
812
813 let total_cells: usize = per_file_cells.iter().map(|v| v.len()).sum();
814
815 if verbose {
816 for (i, task) in tasks.iter().enumerate() {
817 println!(
818 " {}: {} / {} cells passing QC",
819 task.exp_id,
820 per_file_cells[i].len().separate_with_underscores(),
821 task.no_cells.separate_with_underscores()
822 );
823 }
824 println!(" Total cells: {}", total_cells.separate_with_underscores());
825 }
826
827 if verbose {
829 println!("Writing cells to binary...");
830 }
831
832 let mut writer = CellGeneSparseWriter::new(
833 &bin_path,
834 true,
835 total_cells,
836 total_genes,
837 cell_qc.target_size,
838 )?;
839
840 let mut cell_offset = 0usize;
841 let mut per_file_results = Vec::with_capacity(tasks.len());
842
843 for (file_idx, task) in tasks.iter().enumerate() {
844 let qc_result = write_h5_file_cells(
845 task,
846 &per_file_cells[file_idx],
847 &composed_mappings[file_idx],
848 cell_qc.target_size,
849 cell_offset,
850 &mut writer,
851 verbose,
852 )?;
853
854 cell_offset += per_file_cells[file_idx].len();
855 per_file_results.push(qc_result);
856 }
857
858 writer.finalise()?;
859
860 let elapsed = total_start.elapsed();
861 if verbose {
862 println!("Multi-h5ad loading complete: {:.2?}", elapsed);
863 }
864
865 Ok(MultiH5adResult {
866 global_gene_indices,
867 total_cells,
868 total_genes,
869 per_file: per_file_results,
870 })
871}