1use crate::hdf5_io::*;
2use crate::sparse_io::*;
3use crate::sparse_util::*;
4use crate::utilities::name_matching::{
5 colon_peak_names, compose_id_name, filter_row_indices_by_type, make_names_unique,
6};
7use crate::zarr_io::*;
8
9use legume_numeric::matrix::common_io::*;
10use log::info;
11
12pub struct TenxMatrix {
15 pub triplets: Vec<(u64, u64, f32)>,
16 pub reach: (usize, usize),
19 pub row_ids: Option<Vec<Box<str>>>,
20 pub row_names: Option<Vec<Box<str>>>,
21 pub row_types: Option<Vec<Box<str>>>,
22 pub column_names: Option<Vec<Box<str>>>,
23 pub keep_rows: Option<Vec<bool>>,
26}
27
28pub(crate) fn axis_len(
35 declared: Option<usize>,
36 named: Option<usize>,
37 reach: usize,
38 compressed: bool,
39 what: &str,
40) -> anyhow::Result<usize> {
41 let (n, source) = match (declared, named) {
42 (Some(n), _) => (n, "declares"),
43 (None, Some(n)) => (n, "names"),
44 (None, None) => return Ok(reach),
45 };
46 anyhow::ensure!(
47 if compressed { reach == n } else { reach <= n },
48 "the file {source} {n} {what} but its data {} {reach}",
49 if compressed { "holds" } else { "reaches" }
50 );
51 Ok(n)
52}
53
54pub(crate) fn ensure_rows_kept(
60 n_kept: usize,
61 nrows: usize,
62 row_types: Option<&[Box<str>]>,
63 patterns: (&str, &str),
64 row_set: Option<(usize, usize)>,
65) -> anyhow::Result<()> {
66 if n_kept > 0 || nrows == 0 {
67 return Ok(());
68 }
69 let types: std::collections::BTreeSet<&str> = row_types
70 .iter()
71 .flat_map(|t| t.iter())
72 .map(|t| t.as_ref())
73 .collect();
74 let patterns = format!("select `{}`, remove `{}`", patterns.0, patterns.1);
75 match row_set {
76 Some((n_typed, n_set)) => anyhow::bail!(
77 "none of the {nrows} rows is kept: {n_typed} are of a kept type ({patterns}) and \
78 {n_set} are in `keep_rows`, the rows the file itself selects (a probe set's \
79 targets), but none is both; the file's feature types are {types:?}. Name the \
80 types to keep with --select-row-type"
81 ),
82 None => anyhow::bail!(
83 "none of the {nrows} rows is kept ({patterns}); the file's feature types are \
84 {types:?}. Name the types to keep with --select-row-type"
85 ),
86 }
87}
88
89pub fn write_10x_matrix(
95 m: TenxMatrix,
96 select_row_type: &str,
97 remove_row_type: &str,
98 backend_file: &str,
99 backend: &SparseIoBackend,
100) -> anyhow::Result<()> {
101 let TenxMatrix {
102 mut triplets,
103 reach,
104 row_ids,
105 row_names,
106 row_types,
107 column_names,
108 keep_rows,
109 } = m;
110 let nrows = axis_len(None, row_ids.as_ref().map(Vec::len), reach.0, false, "rows")?;
113 let named_columns = column_names.as_ref().map(Vec::len);
114 let ncols = axis_len(None, named_columns, reach.1, false, "columns")?;
115 let index = |n: usize| {
116 (0..n)
117 .map(|i| i.to_string().into_boxed_str())
118 .collect::<Vec<_>>()
119 };
120 let mut row_ids = row_ids.unwrap_or_else(|| index(nrows));
121 let mut row_names = row_names.unwrap_or_else(|| vec![Box::from(""); nrows]);
122 anyhow::ensure!(
123 row_names.len() == nrows
124 && row_types.as_ref().is_none_or(|t| t.len() == nrows)
125 && keep_rows.as_ref().is_none_or(|k| k.len() == nrows),
126 "the file lists {nrows} row ids but {} row names and {} row types",
127 row_names.len(),
128 row_types.as_ref().map_or(0, Vec::len)
129 );
130 let column_names = column_names.unwrap_or_else(|| index(ncols));
131
132 let n_peaks = colon_peak_names(&mut row_ids, &mut row_names, row_types.as_deref());
133 if n_peaks > 0 {
134 info!("{n_peaks} peak names rewritten in chr:start-end form");
135 }
136 let mut row_ids = compose_id_name(row_ids, row_names);
137 make_names_unique(&mut row_ids);
138 let mut keep = match &row_types {
139 Some(types) => filter_row_indices_by_type(types, select_row_type, remove_row_type),
140 None => (0..nrows).collect(),
141 };
142 let row_set = keep_rows.as_ref().map(|keep_rows| {
143 let n_typed = keep.len();
144 keep.retain(|&i| keep_rows[i]);
145 (n_typed, keep_rows.iter().filter(|&&k| k).count())
146 });
147 ensure_rows_kept(
148 keep.len(),
149 nrows,
150 row_types.as_deref(),
151 (select_row_type, remove_row_type),
152 row_set,
153 )?;
154 if keep.len() < nrows {
156 info!("Keeping {} of {nrows} rows", keep.len());
157 let mut new_row = vec![None; nrows];
158 for (new, &old) in keep.iter().enumerate() {
159 new_row[old] = Some(new as u64);
160 }
161 triplets.retain_mut(|(i, _, _)| match new_row[*i as usize] {
162 Some(new) => {
163 *i = new;
164 true
165 }
166 None => false,
167 });
168 row_ids = keep
169 .iter()
170 .map(|&i| std::mem::take(&mut row_ids[i]))
171 .collect();
172 }
173
174 let nnz = triplets.len();
175 info!("Matrix: {} x {ncols}, {nnz} non-zeros", row_ids.len());
176 let mut out = create_sparse_from_triplets_owned(
177 triplets,
178 (row_ids.len(), ncols, nnz),
179 Some(backend_file),
180 Some(backend),
181 )?;
182 info!("Created sparse matrix: {}", backend_file);
183 out.register_row_names_vec(&row_ids);
184 out.register_column_names_vec(&column_names);
185 Ok(())
186}
187
188#[derive(Clone, Debug)]
192pub struct MatrixLayout {
193 pub data_field: Box<str>,
194 pub indices_field: Box<str>,
195 pub indptr_field: Box<str>,
196 pub pointer_type: IndexPointerType,
197 pub row_id_field: Box<str>,
198 pub row_name_field: Box<str>,
199 pub row_type_field: Box<str>,
200 pub select_row_type: Box<str>,
202 pub remove_row_type: Box<str>,
204 pub column_name_field: Box<str>,
205}
206
207impl MatrixLayout {
208 pub const H5_ROOT: &'static str = "matrix";
211 pub const H5_DATA: &'static str = "data";
212 pub const H5_INDICES: &'static str = "indices";
213 pub const H5_INDPTR: &'static str = "indptr";
214 pub const H5_ROW_IDS: &'static str = "features/id";
215 pub const H5_ROW_NAMES: &'static str = "features/name";
216 pub const H5_ROW_TYPES: &'static str = "features/feature_type";
217 pub const H5_COLUMN_NAMES: &'static str = "barcodes";
218 pub const XENIUM_DATA: &'static str = "/cell_features/data";
219 pub const XENIUM_INDICES: &'static str = "/cell_features/indices";
220 pub const XENIUM_INDPTR: &'static str = "/cell_features/indptr";
221 pub const XENIUM_ROW_IDS: &'static str = "/cell_features/feature_ids";
222 pub const XENIUM_ROW_NAMES: &'static str = "/cell_features/feature_keys";
223 pub const XENIUM_ROW_TYPES: &'static str = "/cell_features/feature_types";
224 pub const XENIUM_COLUMN_NAMES: &'static str = "/cell_features/cell_id";
225 pub const SELECT_ROW_TYPES: &'static str = "gene,peak";
227 pub const REMOVE_ROW_TYPES: &'static str = "aggregate";
229
230 pub fn cell_ranger_h5() -> Self {
233 Self {
234 data_field: Self::H5_DATA.into(),
235 indices_field: Self::H5_INDICES.into(),
236 indptr_field: Self::H5_INDPTR.into(),
237 pointer_type: IndexPointerType::Column,
238 row_id_field: Self::H5_ROW_IDS.into(),
239 row_name_field: Self::H5_ROW_NAMES.into(),
240 row_type_field: Self::H5_ROW_TYPES.into(),
241 select_row_type: Self::SELECT_ROW_TYPES.into(),
242 remove_row_type: Self::REMOVE_ROW_TYPES.into(),
243 column_name_field: Self::H5_COLUMN_NAMES.into(),
244 }
245 }
246
247 pub fn xenium_zarr() -> Self {
250 Self {
251 data_field: Self::XENIUM_DATA.into(),
252 indices_field: Self::XENIUM_INDICES.into(),
253 indptr_field: Self::XENIUM_INDPTR.into(),
254 pointer_type: IndexPointerType::Row,
255 row_id_field: Self::XENIUM_ROW_IDS.into(),
256 row_name_field: Self::XENIUM_ROW_NAMES.into(),
257 row_type_field: Self::XENIUM_ROW_TYPES.into(),
258 select_row_type: Self::SELECT_ROW_TYPES.into(),
259 remove_row_type: Self::REMOVE_ROW_TYPES.into(),
260 column_name_field: Self::XENIUM_COLUMN_NAMES.into(),
261 }
262 }
263}
264
265#[cfg(feature = "hdf5")]
271pub fn build_from_h5_matrix(
272 h5_file: &str,
273 root_group: &str,
274 layout: &MatrixLayout,
275 backend_file: &str,
276 backend: &SparseIoBackend,
277) -> anyhow::Result<()> {
278 let file = hdf5::File::open(h5_file)?;
279 info!("Opened 10x HDF5 file: {}", h5_file);
280 let root = file
281 .group(root_group)
282 .map_err(|_| anyhow::anyhow!("no group `{root_group}` in {h5_file}"))?;
283 let dataset = |field: &str| {
284 root.dataset(field)
285 .map_err(|_| anyhow::anyhow!("no dataset `{root_group}/{field}` in {h5_file}"))
286 };
287 let CooTripletsShape { triplets, shape } = {
289 let values: Vec<f32> = dataset(&layout.data_field)?.read_raw()?;
290 let indices: Vec<u64> = dataset(&layout.indices_field)?.read_raw()?;
291 let indptr: Vec<u64> = dataset(&layout.indptr_field)?.read_raw()?;
292 ValuesIndicesPointers {
293 values: &values,
294 indices: &indices,
295 indptr: &indptr,
296 }
297 .to_coo(layout.pointer_type)?
298 };
299 info!(
300 "Read {} non-zero elements reaching {} x {}",
301 shape.nnz, shape.nrows, shape.ncols
302 );
303 let names = |field: &str| root.dataset(field).ok().map(read_hdf5_strings).transpose();
304 write_10x_matrix(
305 TenxMatrix {
306 triplets,
307 reach: (shape.nrows, shape.ncols),
308 row_ids: names(&layout.row_id_field)?,
309 row_names: names(&layout.row_name_field)?,
310 row_types: names(&layout.row_type_field)?,
311 column_names: names(&layout.column_name_field)?,
312 keep_rows: None,
313 },
314 &layout.select_row_type,
315 &layout.remove_row_type,
316 backend_file,
317 backend,
318 )
319}
320
321pub fn build_from_zarr_matrix(
327 zarr_file: &str,
328 layout: &MatrixLayout,
329 backend_file: &str,
330 backend: &SparseIoBackend,
331) -> anyhow::Result<()> {
332 if !std::path::Path::new(zarr_file).exists() {
333 let zip_variant = format!("{}.zip", zarr_file);
334 let hint: Box<str> = if std::path::Path::new(&zip_variant).exists() {
335 format!(" (did you mean {}?)", zip_variant).into()
336 } else {
337 Box::from("")
338 };
339 anyhow::bail!("Zarr file not found: {}{}", zarr_file, hint);
340 }
341 let store = open_zarr_store(zarr_file)?;
342 info!("Opened zarr store: {}", zarr_file);
343
344 let CooTripletsShape { triplets, shape } = {
346 let indices: Vec<u64> = read_zarr_numerics(store.clone(), &layout.indices_field)?;
347 let indptr: Vec<u64> = read_zarr_numerics(store.clone(), &layout.indptr_field)?;
348 let values: Vec<f32> = read_zarr_numerics(store.clone(), &layout.data_field)?;
349 ValuesIndicesPointers {
350 values: &values,
351 indices: &indices,
352 indptr: &indptr,
353 }
354 .to_coo(layout.pointer_type)?
355 };
356 let TripletsShape { nrows, ncols, nnz } = shape;
357 info!("Read {nnz} non-zero elements reaching {nrows} x {ncols}");
358
359 let names = |field: &str| {
361 read_zarr_group_attr::<Vec<Box<str>>>(store.clone(), field)
362 .or_else(|_| read_zarr_strings(store.clone(), field))
363 .ok()
364 };
365 let column_names = read_zarr_flat_u32(store.clone(), &layout.column_name_field)
366 .and_then(|(ids, shape)| {
367 anyhow::ensure!(shape.len() == 2 && shape[1] == 2, "cell_id must be [N, 2]");
368 parse_10x_cell_id_flat(&ids, shape[0] as usize)
369 })
370 .ok()
371 .or_else(|| names(&layout.column_name_field));
372 write_10x_matrix(
373 TenxMatrix {
374 triplets,
375 reach: (nrows, ncols),
376 row_ids: names(&layout.row_id_field),
377 row_names: names(&layout.row_name_field),
378 row_types: names(&layout.row_type_field),
379 column_names,
380 keep_rows: None,
381 },
382 &layout.select_row_type,
383 &layout.remove_row_type,
384 backend_file,
385 backend,
386 )
387}
388
389fn convert_with(
391 output: &str,
392 build: impl FnOnce(&str, &SparseIoBackend) -> anyhow::Result<()>,
393) -> anyhow::Result<()> {
394 let (output, backend, backend_file) = prepare_output(output, SparseIoBackend::Zarr, false)?;
395 build(&backend_file, &backend)?;
396 finalize_output(&backend_file, &output)?;
397 info!("Conversion done: {}", output);
398 Ok(())
399}
400
401#[cfg(feature = "hdf5")]
406pub fn convert_h5_to_backend(h5_file: &str, output: &str) -> anyhow::Result<()> {
407 convert_with(output, |backend_file, backend| {
408 let layout = MatrixLayout::cell_ranger_h5();
409 build_from_h5_matrix(
410 h5_file,
411 MatrixLayout::H5_ROOT,
412 &layout,
413 backend_file,
414 backend,
415 )
416 })
417}
418
419pub fn convert_zarr_to_backend(zarr_file: &str, output: &str) -> anyhow::Result<()> {
423 convert_with(output, |backend_file, backend| {
424 build_from_zarr_matrix(
425 zarr_file,
426 &MatrixLayout::xenium_zarr(),
427 backend_file,
428 backend,
429 )
430 })
431}
432
433fn cached_conversion(converted: &str) -> Option<Box<dyn SparseIo<IndexIter = Vec<usize>>>> {
436 if !std::path::Path::new(converted).exists() {
437 return None;
438 }
439 let data = open_sparse_matrix(converted, &SparseIoBackend::Zarr).ok()?;
440 let whole =
441 data.num_rows().is_some() && data.num_columns().is_some() && data.num_non_zeros().is_some();
442 whole.then_some(data)
443}
444
445fn staged_conversion(base: &str) -> String {
449 use std::sync::atomic::{AtomicUsize, Ordering};
450 static NTH: AtomicUsize = AtomicUsize::new(0);
451 format!(
452 "{base}.db.partial-{}-{}.zarr",
453 std::process::id(),
454 NTH.fetch_add(1, Ordering::Relaxed)
455 )
456}
457
458fn install_conversion(staged: &str, converted: &str) -> anyhow::Result<()> {
467 let rename = || Ok(std::fs::rename(staged, converted)?);
468 let occupied = || std::path::Path::new(converted).exists();
469 let usable = || cached_conversion(converted).is_some();
470 let mut renamed: anyhow::Result<()> = rename();
471 if renamed.is_err() && occupied() && !usable() {
472 let stale = format!("{staged}.stale");
473 if std::fs::rename(converted, &stale).is_ok() {
474 renamed = rename();
475 remove_file(&stale)?;
476 }
477 }
478 let Err(e) = renamed else {
479 return Ok(());
480 };
481 remove_file(staged)?;
482 anyhow::ensure!(
483 usable(),
484 "cannot install the conversion at {converted}: {e}"
485 );
486 Ok(())
487}
488
489pub fn try_open_or_convert(
500 data_file: &str,
501) -> anyhow::Result<Box<dyn SparseIo<IndexIter = Vec<usize>>>> {
502 let ext = file_ext(data_file)?;
503 let backend = match ext.as_ref() {
504 "h5" | "h5ad" => SparseIoBackend::HDF5,
505 _ => SparseIoBackend::Zarr,
506 };
507
508 let original_err = match open_sparse_matrix(data_file, &backend) {
509 Ok(data) => return Ok(data),
510 Err(e) => e,
511 };
512 let base = strip_backend_suffix(data_file);
513 let converted = format!("{}.db.zarr", base);
514
515 if let Some(data) = cached_conversion(&converted) {
516 info!("Using cached conversion: {}", converted);
517 return Ok(data);
518 }
519 if std::path::Path::new(&converted).exists() {
520 log::warn!(
521 "the cached conversion {} is incomplete; converting {} again",
522 converted,
523 data_file
524 );
525 }
526
527 type Convert = fn(&str, &str) -> anyhow::Result<()>;
529 let (convert, command): (Convert, &str) = match ext.as_ref() {
530 #[cfg(feature = "hdf5")]
531 "h5" => (convert_h5_to_backend, "from-10x-matrix"),
532 #[cfg(feature = "hdf5")]
533 "h5ad" => anyhow::bail!(
534 "{data_file} does not open as a data-beans backend ({original_err}), and an \
535 AnnData .h5ad is not converted automatically; run `data-beans from-h5ad \
536 {data_file} -o <output>` and use its output"
537 ),
538 #[cfg(not(feature = "hdf5"))]
539 "h5" | "h5ad" => anyhow::bail!(
540 "{} is an HDF5 file but data-beans was built without the `hdf5` \
541 feature. Reinstall with `--features hdf5` (and a working libhdf5) \
542 to read .h5/.h5ad inputs.",
543 data_file
544 ),
545 "zarr" | "zip" => (convert_zarr_to_backend, "from-zarr"),
546 _ => return Err(original_err),
547 };
548
549 let staged = staged_conversion(base);
550 info!("Converting {} to backend: {}", data_file, converted);
551 if let Err(e) = convert(data_file, &staged) {
552 let _ = remove_file(&staged);
553 return Err(e.context(format!(
556 "{data_file} is not converted automatically with the default options; run \
557 `data-beans {command} {data_file} -o <output>` with its options, such as \
558 --select-row-type for the feature types to keep, and use its output"
559 )));
560 }
561 install_conversion(&staged, &converted)?;
562 open_sparse_matrix(&converted, &SparseIoBackend::Zarr)
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568 use std::path::Path;
569
570 fn write_u32_array(dir: &Path, name: &str, shape: &[usize], values: &[u32]) {
572 let d = dir.join(name);
573 std::fs::create_dir_all(&d).unwrap();
574 let meta = serde_json::json!({
575 "zarr_format": 2, "shape": shape, "chunks": shape, "dtype": "<u4",
576 "compressor": null, "fill_value": 0, "order": "C", "filters": null
577 });
578 std::fs::write(d.join(".zarray"), meta.to_string()).unwrap();
579 let chunk = vec!["0"; shape.len()].join(".");
580 let bytes: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
581 std::fs::write(d.join(chunk), bytes).unwrap();
582 }
583
584 fn xenium_like_store(dir: &Path) -> Vec<u32> {
588 let cf = dir.join("cell_features");
589 std::fs::create_dir_all(&cf).unwrap();
590 std::fs::write(dir.join(".zgroup"), r#"{"zarr_format": 2}"#).unwrap();
591 std::fs::write(cf.join(".zgroup"), r#"{"zarr_format": 2}"#).unwrap();
592 let attrs = serde_json::json!({
593 "feature_ids": ["FID1", "FID2", "FID3"],
594 "feature_keys": ["GENE1", "GENE2", "NEG1"],
595 "feature_types": ["gene", "gene", "negative_control_probe"],
596 "number_cells": 4, "number_features": 3
597 });
598 std::fs::write(cf.join(".zattrs"), attrs.to_string()).unwrap();
599 write_u32_array(&cf, "indptr", &[4], &[0, 2, 4, 5]);
601 write_u32_array(&cf, "indices", &[5], &[0, 2, 1, 3, 0]);
602 write_u32_array(&cf, "data", &[5], &[1, 2, 3, 4, 5]);
603 let ids = vec![16844, 1, 22527, 1, 16845, 1, 22528, 1];
604 write_u32_array(&cf, "cell_id", &[4, 2], &ids);
605 ids
606 }
607
608 #[test]
609 fn a_xenium_store_converts_to_genes_by_cells_with_their_names() {
610 let dir = tempfile::tempdir().unwrap();
611 let store = dir.path().join("matrix.zarr");
612 let ids = xenium_like_store(&store);
613 let out = dir.path().join("out.db.zarr");
614 let out = out.to_str().unwrap();
615 convert_zarr_to_backend(store.to_str().unwrap(), out).unwrap();
616
617 let data = open_sparse_matrix(out, &SparseIoBackend::Zarr).unwrap();
618 assert_eq!(
620 data.row_names().unwrap(),
621 vec![Box::from("FID1_GENE1"), Box::from("FID2_GENE2")]
622 );
623 assert_eq!(
624 data.column_names().unwrap(),
625 parse_10x_cell_id_flat(&ids, 4).unwrap()
626 );
627 let m = data.read_columns_dmatrix((0..4).collect()).unwrap();
628 assert_eq!(m.shape(), (2, 4));
629 assert_eq!(
630 m.row(0).iter().copied().collect::<Vec<f32>>(),
631 [1., 0., 2., 0.]
632 );
633 assert_eq!(
634 m.row(1).iter().copied().collect::<Vec<f32>>(),
635 [0., 3., 0., 4.]
636 );
637 }
638
639 fn file_names(dir: &Path) -> Vec<String> {
640 let mut names: Vec<String> = std::fs::read_dir(dir)
641 .unwrap()
642 .map(|e| e.unwrap().file_name().into_string().unwrap())
643 .collect();
644 names.sort();
645 names
646 }
647
648 #[test]
649 fn a_partial_cached_conversion_is_converted_again() {
650 let dir = tempfile::tempdir().unwrap();
651 let store = dir.path().join("matrix.zarr");
652 xenium_like_store(&store);
653 let cache = dir.path().join("matrix.db.zarr");
655 std::fs::create_dir_all(cache.join("by_column")).unwrap();
656 std::fs::write(cache.join(".zgroup"), r#"{"zarr_format": 2}"#).unwrap();
657
658 let data = try_open_or_convert(store.to_str().unwrap()).unwrap();
659 assert_eq!(
660 data.row_names().unwrap(),
661 ["FID1_GENE1", "FID2_GENE2"].map(Box::from)
662 );
663 assert_eq!(data.num_columns(), Some(4));
664 drop(data);
665 assert_eq!(file_names(dir.path()), ["matrix.db.zarr", "matrix.zarr"]);
668 let stamp = |p: &Path| std::fs::metadata(p).unwrap().modified().unwrap();
669 let built = stamp(&cache);
670 let again = try_open_or_convert(store.to_str().unwrap()).unwrap();
671 assert_eq!(again.num_non_zeros(), Some(4));
672 assert_eq!(stamp(&cache), built);
673 }
674
675 #[test]
676 fn a_failed_conversion_leaves_no_cache() {
677 let dir = tempfile::tempdir().unwrap();
678 let store = dir.path().join("matrix.zarr");
679 xenium_like_store(&store);
680 let attrs = serde_json::json!({
682 "feature_ids": ["FID1", "FID2", "FID3"],
683 "feature_keys": ["A", "B", "C"],
684 "feature_types": vec!["negative_control_probe"; 3],
685 });
686 std::fs::write(store.join("cell_features/.zattrs"), attrs.to_string()).unwrap();
687
688 let e = try_open_or_convert(store.to_str().unwrap())
689 .err()
690 .expect("no row is kept");
691 let e = format!("{e:#}");
694 assert!(e.contains("data-beans from-zarr"), "{e}");
695 assert!(e.contains("--select-row-type"), "{e}");
696 assert!(e.contains("negative_control_probe"), "{e}");
697 assert_eq!(file_names(dir.path()), ["matrix.zarr"]);
698 }
699
700 #[test]
701 fn a_cache_installed_meanwhile_stays_unless_it_is_unusable() {
702 let dir = tempfile::tempdir().unwrap();
703 let store = dir.path().join("matrix.zarr");
704 xenium_like_store(&store);
705 let path = |name: &str| dir.path().join(name).to_str().unwrap().to_string();
706 let (cache, staged) = (path("matrix.db.zarr"), path("staged.zarr"));
707 let convert = |to: &str| convert_zarr_to_backend(store.to_str().unwrap(), to).unwrap();
708
709 convert(&cache);
711 let marker = Path::new(&cache).join("installed-first");
712 std::fs::write(&marker, "").unwrap();
713 convert(&staged);
714 install_conversion(&staged, &cache).unwrap();
715 assert!(marker.exists());
716 assert!(!Path::new(&staged).exists());
717
718 remove_file(&cache).unwrap();
720 std::fs::create_dir_all(Path::new(&cache).join("by_column")).unwrap();
721 convert(&staged);
722 install_conversion(&staged, &cache).unwrap();
723 assert!(cached_conversion(&cache).is_some());
724 assert!(!Path::new(&staged).exists());
725 }
726
727 #[cfg(feature = "hdf5")]
728 #[test]
729 fn an_h5ad_file_is_sent_to_from_h5ad() {
730 let dir = tempfile::tempdir().unwrap();
731 let h5ad = dir.path().join("cells.h5ad");
732 let file = hdf5::File::create(&h5ad).unwrap();
733 file.create_group("X").unwrap();
734 file.create_group("obs").unwrap();
735 file.create_group("var").unwrap();
736 drop(file);
737
738 let e = try_open_or_convert(h5ad.to_str().unwrap())
739 .err()
740 .expect("an AnnData file is not a backend")
741 .to_string();
742 assert!(e.contains("from-h5ad"), "{e}");
743 assert_eq!(file_names(dir.path()), ["cells.h5ad"]);
744 }
745
746 fn names(xs: &[&str]) -> Option<Vec<Box<str>>> {
747 Some(xs.iter().map(|&x| Box::from(x)).collect())
748 }
749
750 #[test]
751 fn trailing_empty_rows_and_columns_are_kept() {
752 let dir = tempfile::tempdir().unwrap();
753 let out = dir.path().join("out.zarr");
754 let out = out.to_str().unwrap();
755 let m = TenxMatrix {
759 triplets: vec![(0, 0, 1.0), (1, 1, 2.0)],
760 reach: (2, 2),
761 row_ids: names(&["FID1", "FID2", "FID3"]),
762 row_names: names(&["GENE1", "FID2", ""]),
763 row_types: names(&["Gene Expression"; 3]),
764 column_names: names(&["BC1", "BC2", "BC3"]),
765 keep_rows: None,
766 };
767 write_10x_matrix(m, "gene", "", out, &SparseIoBackend::Zarr).unwrap();
768
769 let data = open_sparse_matrix(out, &SparseIoBackend::Zarr).unwrap();
770 assert_eq!(
771 data.row_names().unwrap(),
772 ["FID1_GENE1", "FID2", "FID3"].map(Box::from)
773 );
774 assert_eq!(
775 data.column_names().unwrap(),
776 ["BC1", "BC2", "BC3"].map(Box::from)
777 );
778 let m = data.read_columns_dmatrix((0..3).collect()).unwrap();
779 assert_eq!(m.shape(), (3, 3));
780 assert_eq!(
781 m.row(2).iter().sum::<f32>() + m.column(2).iter().sum::<f32>(),
782 0.
783 );
784 }
785
786 #[test]
787 fn data_reaching_past_the_names_is_an_error() {
788 let dir = tempfile::tempdir().unwrap();
789 let out = dir.path().join("out.zarr");
790 let m = TenxMatrix {
791 triplets: vec![(2, 0, 1.0)],
792 reach: (3, 1),
793 row_ids: names(&["FID1", "FID2"]),
794 row_names: None,
795 row_types: None,
796 column_names: None,
797 keep_rows: None,
798 };
799 let e = write_10x_matrix(m, "gene", "", out.to_str().unwrap(), &SparseIoBackend::Zarr);
800 assert!(e.unwrap_err().to_string().contains("names 2 rows"));
801 }
802
803 #[test]
804 fn keeping_no_row_is_an_error_naming_the_types() {
805 let dir = tempfile::tempdir().unwrap();
806 let out = dir.path().join("out.zarr");
807 let m = TenxMatrix {
808 triplets: vec![(0, 0, 1.0), (1, 1, 2.0)],
809 reach: (2, 2),
810 row_ids: names(&["HTO1", "CD3"]),
811 row_names: None,
812 row_types: names(&["Multiplexing Capture", "Antibody Capture"]),
813 column_names: None,
814 keep_rows: None,
815 };
816 let e = write_10x_matrix(
817 m,
818 MatrixLayout::SELECT_ROW_TYPES,
819 MatrixLayout::REMOVE_ROW_TYPES,
820 out.to_str().unwrap(),
821 &SparseIoBackend::Zarr,
822 )
823 .unwrap_err()
824 .to_string();
825 assert!(
826 e.contains("Antibody Capture") && e.contains("gene,peak"),
827 "{e}"
828 );
829 assert!(!out.exists());
830 }
831
832 #[test]
833 fn keeping_no_row_names_the_row_set_that_leaves_none() {
834 let dir = tempfile::tempdir().unwrap();
835 let out = dir.path().join("out.zarr");
836 let m = TenxMatrix {
839 triplets: vec![(0, 0, 1.0), (1, 1, 2.0), (2, 0, 3.0)],
840 reach: (3, 2),
841 row_ids: names(&["G1", "G2", "CD3"]),
842 row_names: None,
843 row_types: names(&["Gene Expression", "Gene Expression", "Antibody Capture"]),
844 column_names: None,
845 keep_rows: Some(vec![false, false, true]),
846 };
847 let e = write_10x_matrix(m, "gene", "", out.to_str().unwrap(), &SparseIoBackend::Zarr)
848 .unwrap_err()
849 .to_string();
850 assert!(e.contains("2 are of a kept type"), "{e}");
851 assert!(e.contains("1 are in `keep_rows`"), "{e}");
852 assert!(!out.exists());
853 }
854}