1use crate::hdf5_io::*;
2use crate::sparse_io::*;
3use crate::sparse_util::*;
4use crate::zarr_io::*;
5
6use legume_numeric::matrix::common_io::*;
7use log::info;
8
9#[cfg(feature = "hdf5")]
15pub fn convert_h5_to_backend(h5_file: &str, output: &str) -> anyhow::Result<()> {
16 let (backend, backend_file) =
17 resolve_backend_file(&Box::from(output), Some(SparseIoBackend::Zarr))?;
18
19 if std::path::Path::new(backend_file.as_ref()).exists() {
20 info!("Removing existing backend file: {}", &backend_file);
21 remove_file(&backend_file)?;
22 }
23
24 let file = hdf5::File::open(h5_file)?;
25 info!("Opened data file: {}", h5_file);
26
27 let root_group_name = "matrix";
28 let root = file.group(root_group_name).map_err(|_| {
29 anyhow::anyhow!(
30 "Unable to find root group '{}' in {}",
31 root_group_name,
32 h5_file,
33 )
34 })?;
35
36 let CooTripletsShape { triplets, shape } = {
38 let values = root
39 .dataset("data")
40 .map_err(|_| anyhow::anyhow!("missing 'data' dataset"))?
41 .read_1d::<f32>()?
42 .to_vec();
43 let indices = root
44 .dataset("indices")
45 .map_err(|_| anyhow::anyhow!("missing 'indices' dataset"))?
46 .read_1d::<u64>()?
47 .to_vec();
48 let indptr = root
49 .dataset("indptr")
50 .map_err(|_| anyhow::anyhow!("missing 'indptr' dataset"))?
51 .read_1d::<u64>()?
52 .to_vec();
53
54 ValuesIndicesPointers {
55 values: &values,
56 indices: &indices,
57 indptr: &indptr,
58 }
59 .to_coo(IndexPointerType::Column)?
60 };
61
62 let TripletsShape { nrows, ncols, nnz } = shape;
63 info!("Read {} non-zero elements in {} x {}", nnz, nrows, ncols);
64
65 let mut row_ids: Vec<Box<str>> = match root.dataset("features/id") {
67 Ok(rows) => read_hdf5_strings(rows)?,
68 _ => {
69 info!("row (feature) IDs not found");
70 (0..nrows).map(|x| x.to_string().into_boxed_str()).collect()
71 }
72 };
73
74 let mut row_names: Vec<Box<str>> = match root.dataset("features/name") {
76 Ok(rows) => read_hdf5_strings(rows)?,
77 _ => {
78 info!("row (feature) names not found");
79 vec![Box::from(""); nrows]
80 }
81 };
82
83 if nrows < row_ids.len() {
84 row_ids.truncate(nrows);
85 }
86 if nrows < row_names.len() {
87 row_names.truncate(nrows);
88 }
89 assert_eq!(nrows, row_ids.len());
90 assert_eq!(nrows, row_names.len());
91
92 let row_ids: Vec<Box<str>> = row_ids
94 .into_iter()
95 .zip(row_names)
96 .map(|(id, name)| {
97 if !name.is_empty() {
98 format!("{}_{}", id, name).into_boxed_str()
99 } else {
100 id
101 }
102 })
103 .collect();
104
105 let mut row_types: Vec<Box<str>> = match root.dataset("features/feature_type") {
107 Ok(rows) => read_hdf5_strings(rows)?,
108 _ => {
109 info!("use all the types");
110 vec!["Gene Expression".to_string().into_boxed_str(); nrows]
111 }
112 };
113 if nrows < row_types.len() {
114 row_types.truncate(nrows);
115 }
116 assert_eq!(nrows, row_types.len());
117
118 let mut column_names: Vec<Box<str>> = match root.dataset("barcodes") {
120 Ok(columns) => read_hdf5_strings(columns)?,
121 _ => {
122 info!("column (cell) names not found");
123 (0..ncols).map(|x| x.to_string().into_boxed_str()).collect()
124 }
125 };
126 if ncols < column_names.len() {
127 column_names.truncate(ncols);
128 }
129 assert_eq!(ncols, column_names.len());
130
131 let mut out = create_sparse_from_triplets(
133 &triplets,
134 (nrows, ncols, nnz),
135 Some(&backend_file),
136 Some(&backend),
137 )?;
138 info!("Created sparse matrix: {}", backend_file);
139 out.register_row_names_vec(&row_ids);
140 out.register_column_names_vec(&column_names);
141
142 let select_pattern = "gene expression";
144 let remove_pattern = "aggregate";
145 let select_rows: Vec<usize> = row_types
146 .iter()
147 .enumerate()
148 .filter_map(|(i, x)| {
149 let lower = x.to_lowercase();
150 if lower.contains(select_pattern) && !lower.contains(remove_pattern) {
151 Some(i)
152 } else {
153 None
154 }
155 })
156 .collect();
157
158 if select_rows.len() < nrows {
159 info!(
160 "Filtering features: {} -> {} rows of gene expression type",
161 nrows,
162 select_rows.len()
163 );
164 out.subset_columns_rows(None, Some(&select_rows))?;
165 }
166
167 finalize_zarr_output(&backend_file, output)?;
168 info!("Conversion done: {}", output);
169 Ok(())
170}
171
172pub fn convert_zarr_to_backend(zarr_file: &str, output: &str) -> anyhow::Result<()> {
179 let (backend, backend_file) =
180 resolve_backend_file(&Box::from(output), Some(SparseIoBackend::Zarr))?;
181
182 if std::path::Path::new(backend_file.as_ref()).exists() {
183 info!("Removing existing backend file: {}", &backend_file);
184 remove_file(&backend_file)?;
185 }
186
187 if !std::path::Path::new(zarr_file).exists() {
188 let zip_variant = format!("{}.zip", zarr_file);
189 let hint: Box<str> = if std::path::Path::new(&zip_variant).exists() {
190 format!(" (did you mean {}?)", zip_variant).into()
191 } else {
192 Box::from("")
193 };
194 anyhow::bail!("Zarr file not found: {}{}", zarr_file, hint);
195 }
196
197 let store = open_zarr_store(zarr_file)?;
198 info!("Opened zarr store: {}", zarr_file);
199
200 let indices: Vec<u64> = read_zarr_numerics(store.clone(), "/cell_features/indices")?;
201 let indptr: Vec<u64> = read_zarr_numerics(store.clone(), "/cell_features/indptr")?;
202 let values: Vec<f32> = read_zarr_numerics(store.clone(), "/cell_features/data")?;
203 info!("Read the arrays");
204
205 let CooTripletsShape { triplets, shape } = ValuesIndicesPointers {
206 values: &values,
207 indices: &indices,
208 indptr: &indptr,
209 }
210 .to_coo(IndexPointerType::Column)?;
211
212 let TripletsShape { nrows, ncols, nnz } = shape;
213 info!("Read {} non-zero elements in {} x {}", nnz, nrows, ncols);
214
215 let row_id_field = "/cell_features/features/id";
216 let row_name_field = "/cell_features/features/name";
217 let row_type_field = "/cell_features/features/feature_type";
218 let column_name_field = "/cell_features/cell_id";
219
220 let mut row_ids = read_zarr_group_attr::<Vec<Box<str>>>(store.clone(), row_id_field)
221 .or_else(|_| read_zarr_strings(store.clone(), row_id_field))
222 .unwrap_or_else(|_| (0..nrows).map(|x| x.to_string().into_boxed_str()).collect());
223
224 let mut row_names = read_zarr_group_attr::<Vec<Box<str>>>(store.clone(), row_name_field)
225 .or_else(|_| read_zarr_strings(store.clone(), row_name_field))
226 .unwrap_or_else(|_| (0..nrows).map(|x| x.to_string().into_boxed_str()).collect());
227
228 info!("Read {} row names", row_ids.len());
229 if nrows < row_ids.len() {
230 row_ids.truncate(nrows);
231 }
232 if nrows < row_names.len() {
233 row_names.truncate(nrows);
234 }
235 assert_eq!(nrows, row_ids.len());
236 assert_eq!(nrows, row_names.len());
237
238 let row_ids: Vec<Box<str>> = row_ids
240 .into_iter()
241 .zip(row_names)
242 .map(|(id, name)| {
243 if !name.is_empty() {
244 format!("{}_{}", id, name).into_boxed_str()
245 } else {
246 id
247 }
248 })
249 .collect();
250
251 let mut row_types = read_zarr_group_attr::<Vec<Box<str>>>(store.clone(), row_type_field)
252 .or_else(|_| read_zarr_strings(store.clone(), row_type_field))
253 .unwrap_or_else(|_| vec!["Gene Expression".to_string().into_boxed_str(); nrows]);
254 if nrows < row_types.len() {
255 row_types.truncate(nrows);
256 }
257 assert_eq!(nrows, row_types.len());
258
259 let mut column_names =
260 parse_10x_cell_id(read_zarr_ndarray::<u32>(store.clone(), column_name_field)?.view())
261 .or_else(|_| read_zarr_group_attr::<Vec<Box<str>>>(store.clone(), column_name_field))
262 .or_else(|_| read_zarr_strings(store.clone(), column_name_field))
263 .unwrap_or_else(|_| (0..ncols).map(|x| x.to_string().into_boxed_str()).collect());
264
265 if ncols < column_names.len() {
266 column_names.truncate(ncols);
267 }
268 assert_eq!(ncols, column_names.len());
269
270 let mut out = create_sparse_from_triplets(
271 &triplets,
272 (nrows, ncols, nnz),
273 Some(&backend_file),
274 Some(&backend),
275 )?;
276 info!("Created sparse matrix: {}", backend_file);
277 out.register_row_names_vec(&row_ids);
278 out.register_column_names_vec(&column_names);
279
280 let select_pattern = "gene expression";
282 let remove_pattern = "aggregate";
283 let select_rows: Vec<usize> = row_types
284 .iter()
285 .enumerate()
286 .filter_map(|(i, x)| {
287 let lower = x.to_lowercase();
288 if lower.contains(select_pattern) && !lower.contains(remove_pattern) {
289 Some(i)
290 } else {
291 None
292 }
293 })
294 .collect();
295
296 if select_rows.len() < nrows {
297 info!(
298 "Filtering features: {} -> {} rows of gene expression type",
299 nrows,
300 select_rows.len()
301 );
302 out.subset_columns_rows(None, Some(&select_rows))?;
303 }
304
305 finalize_zarr_output(&backend_file, output)?;
306 info!("Conversion done: {}", output);
307 Ok(())
308}
309
310pub fn try_open_or_convert(
316 data_file: &str,
317) -> anyhow::Result<Box<dyn SparseIo<IndexIter = Vec<usize>>>> {
318 let ext = file_ext(data_file)?;
319 let backend = match ext.as_ref() {
320 "h5" | "h5ad" => SparseIoBackend::HDF5,
321 _ => SparseIoBackend::Zarr,
322 };
323
324 match open_sparse_matrix(data_file, &backend) {
325 Ok(data) => Ok(data),
326 Err(original_err) => {
327 let base = strip_backend_suffix(data_file);
328 let converted = format!("{}.db.zarr", base);
329
330 if std::path::Path::new(&converted).exists() {
331 info!("Using cached conversion: {}", converted);
332 return open_sparse_matrix(&converted, &SparseIoBackend::Zarr);
333 }
334
335 match ext.as_ref() {
336 "h5" | "h5ad" => {
337 #[cfg(feature = "hdf5")]
338 {
339 info!(
340 "Converting h5/h5ad to backend: {} -> {}",
341 data_file, converted
342 );
343 convert_h5_to_backend(data_file, &converted)?;
344 }
345 #[cfg(not(feature = "hdf5"))]
346 {
347 anyhow::bail!(
348 "{} is an HDF5 file but data-beans was built without the `hdf5` \
349 feature. Reinstall with `--features hdf5` (and a working libhdf5) \
350 to read .h5/.h5ad inputs.",
351 data_file
352 );
353 }
354 }
355 "zarr" | "zip" => {
356 info!("Converting zarr to backend: {} -> {}", data_file, converted);
357 convert_zarr_to_backend(data_file, &converted)?;
358 }
359 _ => return Err(original_err),
360 }
361
362 open_sparse_matrix(&converted, &SparseIoBackend::Zarr)
363 }
364 }
365}