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 = read_zarr_flat_u32(store.clone(), column_name_field)
260 .and_then(|(ids, shape)| {
261 anyhow::ensure!(shape.len() == 2 && shape[1] == 2, "cell_id must be [N, 2]");
262 parse_10x_cell_id_flat(&ids, shape[0] as usize)
263 })
264 .or_else(|_| read_zarr_group_attr::<Vec<Box<str>>>(store.clone(), column_name_field))
265 .or_else(|_| read_zarr_strings(store.clone(), column_name_field))
266 .unwrap_or_else(|_| (0..ncols).map(|x| x.to_string().into_boxed_str()).collect());
267
268 if ncols < column_names.len() {
269 column_names.truncate(ncols);
270 }
271 assert_eq!(ncols, column_names.len());
272
273 let mut out = create_sparse_from_triplets(
274 &triplets,
275 (nrows, ncols, nnz),
276 Some(&backend_file),
277 Some(&backend),
278 )?;
279 info!("Created sparse matrix: {}", backend_file);
280 out.register_row_names_vec(&row_ids);
281 out.register_column_names_vec(&column_names);
282
283 let select_pattern = "gene expression";
285 let remove_pattern = "aggregate";
286 let select_rows: Vec<usize> = row_types
287 .iter()
288 .enumerate()
289 .filter_map(|(i, x)| {
290 let lower = x.to_lowercase();
291 if lower.contains(select_pattern) && !lower.contains(remove_pattern) {
292 Some(i)
293 } else {
294 None
295 }
296 })
297 .collect();
298
299 if select_rows.len() < nrows {
300 info!(
301 "Filtering features: {} -> {} rows of gene expression type",
302 nrows,
303 select_rows.len()
304 );
305 out.subset_columns_rows(None, Some(&select_rows))?;
306 }
307
308 finalize_zarr_output(&backend_file, output)?;
309 info!("Conversion done: {}", output);
310 Ok(())
311}
312
313pub fn try_open_or_convert(
319 data_file: &str,
320) -> anyhow::Result<Box<dyn SparseIo<IndexIter = Vec<usize>>>> {
321 let ext = file_ext(data_file)?;
322 let backend = match ext.as_ref() {
323 "h5" | "h5ad" => SparseIoBackend::HDF5,
324 _ => SparseIoBackend::Zarr,
325 };
326
327 match open_sparse_matrix(data_file, &backend) {
328 Ok(data) => Ok(data),
329 Err(original_err) => {
330 let base = strip_backend_suffix(data_file);
331 let converted = format!("{}.db.zarr", base);
332
333 if std::path::Path::new(&converted).exists() {
334 info!("Using cached conversion: {}", converted);
335 return open_sparse_matrix(&converted, &SparseIoBackend::Zarr);
336 }
337
338 match ext.as_ref() {
339 "h5" | "h5ad" => {
340 #[cfg(feature = "hdf5")]
341 {
342 info!(
343 "Converting h5/h5ad to backend: {} -> {}",
344 data_file, converted
345 );
346 convert_h5_to_backend(data_file, &converted)?;
347 }
348 #[cfg(not(feature = "hdf5"))]
349 {
350 anyhow::bail!(
351 "{} is an HDF5 file but data-beans was built without the `hdf5` \
352 feature. Reinstall with `--features hdf5` (and a working libhdf5) \
353 to read .h5/.h5ad inputs.",
354 data_file
355 );
356 }
357 }
358 "zarr" | "zip" => {
359 info!("Converting zarr to backend: {} -> {}", data_file, converted);
360 convert_zarr_to_backend(data_file, &converted)?;
361 }
362 _ => return Err(original_err),
363 }
364
365 open_sparse_matrix(&converted, &SparseIoBackend::Zarr)
366 }
367 }
368}