data_beans/import.rs
1//! Build a backend from the formats single-cell tools write.
2//!
3//! * [`from_mtx`]: a MatrixMarket file with its feature and barcode TSVs,
4//! such as a 10x Cell Ranger MEX triplet ([`MtxImport`]).
5//! * [`from_h5ad`] (feature `hdf5`): an AnnData `.h5ad` file
6//! ([`H5adImport`]), its `obs` written beside the backend as
7//! `{stem}.cell_metadata.tsv.gz`.
8//! * [`from_10x_matrix`] (feature `hdf5`): a Cell Ranger feature-barcode
9//! matrix `.h5` ([`TenxMatrixImport`]).
10//! * [`from_10x_molecule`] (feature `hdf5`): a Cell Ranger
11//! `molecule_info.h5`, counted as Cell Ranger counts it
12//! ([`TenxMoleculeImport`]).
13//!
14//! Each writes the backend [`Output`] names, squeezed when it asks, and
15//! returns the path of the file written: `{output}.zarr.zip` by default. The
16//! `stage_*` functions stop short of the last step and return a [`Staged`]
17//! build, for a caller that looks at the backend before keeping it (as the
18//! CLI's import preview does). These are what `data-beans from-mtx`,
19//! `from-h5ad`, `from-10x-matrix` and `from-10x-molecule` run.
20//!
21//! ```no_run
22//! use data_beans::import::{self, MtxImport, Output};
23//!
24//! let mtx = MtxImport {
25//! row: Some("filtered/features.tsv.gz".into()),
26//! col: Some("filtered/barcodes.tsv.gz".into()),
27//! ..MtxImport::new("filtered/matrix.mtx.gz")
28//! };
29//! let backend = import::from_mtx(&mtx, &Output::new("pbmc"))?; // "pbmc.zarr.zip"
30//!
31//! #[cfg(feature = "hdf5")]
32//! let backend = import::from_h5ad(&import::H5adImport::new("pbmc.h5ad"), &Output::new("pbmc"))?;
33//! # anyhow::Ok(())
34//! ```
35
36#[cfg(feature = "hdf5")]
37mod h5ad;
38mod mtx;
39#[cfg(feature = "hdf5")]
40mod tenx_matrix;
41#[cfg(feature = "hdf5")]
42mod tenx_molecule;
43
44#[cfg(feature = "hdf5")]
45pub use h5ad::{from_h5ad, stage_h5ad, H5adImport};
46pub use mtx::{from_mtx, stage_mtx, MtxImport};
47#[cfg(feature = "hdf5")]
48pub use tenx_matrix::{from_10x_matrix, stage_10x_matrix, TenxMatrixImport};
49#[cfg(feature = "hdf5")]
50pub use tenx_molecule::{from_10x_molecule, stage_10x_molecule, TenxMoleculeImport};
51
52use crate::sparse_io::{open_sparse_matrix_by_path, SparseIoBackend};
53use log::info;
54
55/// Where an import writes its backend.
56#[derive(Clone, Debug)]
57pub struct Output {
58 /// The output's header or name: `{output}.zarr.zip`, `{output}.zarr` or
59 /// `{output}.h5`, a backend suffix it already has kept. Files written
60 /// beside the backend (metadata tables) are named by its stem.
61 pub output: Box<str>,
62 pub backend: SparseIoBackend,
63 /// Zip a zarr backend into `.zarr.zip`; else keep the `.zarr` directory.
64 pub zip: bool,
65 /// Drop rows and columns with too few non-zeros once it is written.
66 pub squeeze: Option<Squeeze>,
67}
68
69impl Output {
70 /// `{output}.zarr.zip`, not squeezed: the CLI's defaults.
71 pub fn new(output: &str) -> Self {
72 Self {
73 output: output.into(),
74 backend: SparseIoBackend::Zarr,
75 zip: true,
76 squeeze: None,
77 }
78 }
79
80 /// Clear what a previous run left at the working file, and stage the
81 /// build there.
82 fn stage(&self) -> anyhow::Result<(Staged, SparseIoBackend)> {
83 let (effective, backend, working) =
84 crate::zarr_io::prepare_output(&self.output, self.backend.clone(), self.zip)?;
85 let squeeze = self.squeeze;
86 Ok((
87 Staged {
88 effective,
89 working,
90 squeeze,
91 },
92 backend,
93 ))
94 }
95}
96
97/// A squeeze: the rows and columns with fewer non-zeros than these go.
98#[derive(Clone, Copy, Debug)]
99pub struct Squeeze {
100 pub rows: usize,
101 pub columns: usize,
102 /// Cells per job; `None` scales it by the number of features.
103 pub block_size: Option<usize>,
104}
105
106impl Default for Squeeze {
107 /// Rows and columns without a single non-zero go.
108 fn default() -> Self {
109 Self {
110 rows: 1,
111 columns: 1,
112 block_size: None,
113 }
114 }
115}
116
117impl Squeeze {
118 /// Squeeze the backend `backend_file` in place.
119 pub fn apply(&self, backend_file: &str) -> anyhow::Result<()> {
120 crate::qc::squeeze_in_place(backend_file, self.rows, self.columns, self.block_size)
121 }
122}
123
124/// A backend written but not yet in place: for a `.zarr.zip` output, the
125/// `.zarr` directory it is zipped from. [`finish`](Self::finish) puts it in
126/// place; [`discard`](Self::discard) deletes it.
127#[derive(Debug)]
128#[must_use = "a staged backend is only in place once finished"]
129pub struct Staged {
130 /// The output as named, `.zarr.zip` added under zip.
131 effective: Box<str>,
132 /// The file the backend was written to.
133 working: Box<str>,
134 /// The squeeze [`Output`] asked for, applied when it is finished.
135 squeeze: Option<Squeeze>,
136}
137
138impl Staged {
139 /// The backend as written, to open and read before it is finished.
140 pub fn working(&self) -> &str {
141 &self.working
142 }
143
144 /// Squeeze the backend as its [`Output`] asked, and put it in place;
145 /// the path of the file to open it by.
146 pub fn finish(self) -> anyhow::Result<Box<str>> {
147 if let Some(squeeze) = &self.squeeze {
148 squeeze.apply(&self.working)?;
149 }
150 let found = crate::zarr_io::finalize_output(&self.working, &self.effective)?;
151 info!("done");
152 Ok(found.into())
153 }
154
155 /// Delete the backend, kept nowhere.
156 pub fn discard(self) -> anyhow::Result<()> {
157 remove_build(&self.working)
158 }
159}
160
161/// Delete the backend written at `working`, and the stage it was built in
162/// once nothing else is built there.
163pub(crate) fn remove_build(working: &str) -> anyhow::Result<()> {
164 info!("deleting {working}");
165 open_sparse_matrix_by_path(working)?.remove_backend_file()?;
166 crate::zarr_io::leave_stage(working);
167 Ok(())
168}
169
170/// Log how many rows of each feature type `label` lists.
171pub(crate) fn log_feature_type_histogram(label: &str, row_types: &[Box<str>]) {
172 let mut counts: std::collections::BTreeMap<&str, usize> = std::collections::BTreeMap::new();
173 for t in row_types {
174 *counts.entry(t.as_ref()).or_insert(0) += 1;
175 }
176 info!("Feature types in {}: {:?}", label, counts);
177}
178
179#[cfg(test)]
180mod tests;